From 086a54556f96b179b4275d0ba7a62ed919186bd8 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Wed, 13 May 2026 00:58:42 +0200 Subject: [PATCH] Link Claude guidance during setup Guardex already maintains the canonical agent contract in AGENTS.md, but fresh setup left Claude Code without a root CLAUDE.md entrypoint. This makes setup and doctor create a CLAUDE.md symlink to AGENTS.md only when the path is absent, while preserving any existing Claude guidance unchanged. Constraint: Existing root CLAUDE.md files must not be overwritten. Rejected: Copy AGENTS.md into CLAUDE.md | duplicated guidance would drift. Confidence: high Scope-risk: narrow Directive: Keep AGENTS.md as the canonical managed guidance surface; CLAUDE.md should stay a pointer unless the user already owns it. Tested: node --test --test-name-pattern "CLAUDE.md|setup provisions workflow files" test/setup.test.js Tested: openspec validate agent-codex-codex-task-2026-05-13-00-50 --type change --strict Tested: openspec validate --specs Not-tested: Full node --test test/setup.test.js still has unrelated baseline failures documented in openspec tasks.md. Co-authored-by: OmX --- README.md | 3 +- .../.openspec.yaml | 2 ++ .../proposal.md | 15 ++++++++ .../specs/codex-task/spec.md | 14 ++++++++ .../tasks.md | 36 +++++++++++++++++++ src/cli/main.js | 3 ++ src/scaffold/index.js | 19 ++++++++++ test/setup.test.js | 17 +++++++++ 8 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 openspec/changes/agent-codex-codex-task-2026-05-13-00-50/.openspec.yaml create mode 100644 openspec/changes/agent-codex-codex-task-2026-05-13-00-50/proposal.md create mode 100644 openspec/changes/agent-codex-codex-task-2026-05-13-00-50/specs/codex-task/spec.md create mode 100644 openspec/changes/agent-codex-codex-task-2026-05-13-00-50/tasks.md diff --git a/README.md b/README.md index 966ccae5..c605a32f 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,8 @@ the compact layout everywhere. | --- | --- | | `AGENTS.md` **with** markers | Refreshes **only** the managed block. | | `AGENTS.md` **without** markers | Appends the managed block to the end. | -| No `AGENTS.md` | Creates it with the managed block. | +| No `AGENTS.md` | Creates it with the managed block, then links `CLAUDE.md` to it. | +| No root `CLAUDE.md` | Creates a `CLAUDE.md` symlink to `AGENTS.md`. | | A root `CLAUDE.md` | Leaves it alone. | --- diff --git a/openspec/changes/agent-codex-codex-task-2026-05-13-00-50/.openspec.yaml b/openspec/changes/agent-codex-codex-task-2026-05-13-00-50/.openspec.yaml new file mode 100644 index 00000000..40cc12f4 --- /dev/null +++ b/openspec/changes/agent-codex-codex-task-2026-05-13-00-50/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-12 diff --git a/openspec/changes/agent-codex-codex-task-2026-05-13-00-50/proposal.md b/openspec/changes/agent-codex-codex-task-2026-05-13-00-50/proposal.md new file mode 100644 index 00000000..1b332c15 --- /dev/null +++ b/openspec/changes/agent-codex-codex-task-2026-05-13-00-50/proposal.md @@ -0,0 +1,15 @@ +## Why + +- Claude Code reads `CLAUDE.md`, while Guardex keeps the canonical repo guidance in `AGENTS.md`. +- Fresh `gx setup` repos currently get `AGENTS.md` only, so Claude sessions can miss the same safety contract. + +## What Changes + +- Make `gx setup` / `gx doctor` create a root `CLAUDE.md` symlink to `AGENTS.md` when `CLAUDE.md` is absent. +- Preserve existing root `CLAUDE.md` files unchanged. +- Update setup regression coverage and README guidance. + +## Impact + +- Affected surface is setup/doctor scaffold output only. +- Existing user-authored Claude guidance remains untouched. diff --git a/openspec/changes/agent-codex-codex-task-2026-05-13-00-50/specs/codex-task/spec.md b/openspec/changes/agent-codex-codex-task-2026-05-13-00-50/specs/codex-task/spec.md new file mode 100644 index 00000000..915369a1 --- /dev/null +++ b/openspec/changes/agent-codex-codex-task-2026-05-13-00-50/specs/codex-task/spec.md @@ -0,0 +1,14 @@ +## ADDED Requirements + +### Requirement: Setup links Claude guidance to AGENTS when absent +`gx setup` and `gx doctor` SHALL create a root `CLAUDE.md` symlink to `AGENTS.md` when the target repository has no root `CLAUDE.md`. + +#### Scenario: Fresh setup creates both guidance entrypoints +- **WHEN** `gx setup --target ` runs in a repo with no root `AGENTS.md` or `CLAUDE.md` +- **THEN** `AGENTS.md` contains the Guardex managed guidance block +- **AND** `CLAUDE.md` is a symlink whose target is `AGENTS.md`. + +#### Scenario: Existing Claude guidance is preserved +- **GIVEN** the target repo already has a root `CLAUDE.md` +- **WHEN** `gx setup --target ` runs +- **THEN** Guardex SHALL leave the existing `CLAUDE.md` content and file type unchanged. diff --git a/openspec/changes/agent-codex-codex-task-2026-05-13-00-50/tasks.md b/openspec/changes/agent-codex-codex-task-2026-05-13-00-50/tasks.md new file mode 100644 index 00000000..438375e2 --- /dev/null +++ b/openspec/changes/agent-codex-codex-task-2026-05-13-00-50/tasks.md @@ -0,0 +1,36 @@ +## Definition of Done + +This change is complete only when **all** of the following are true: + +- Every checkbox below is checked. +- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff. +- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline. + +## Handoff + +- Handoff: change=`agent-codex-codex-task-2026-05-13-00-50`; branch=`agent/codex/codex-task-2026-05-13-00-50`; scope=`gx setup creates CLAUDE.md link to AGENTS.md`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`. +- Copy prompt: Continue `agent-codex-codex-task-2026-05-13-00-50` on branch `agent/codex/codex-task-2026-05-13-00-50`. Work inside the existing sandbox, review `openspec/changes/agent-codex-codex-task-2026-05-13-00-50/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/codex-task-2026-05-13-00-50 --base dev --via-pr --wait-for-merge --cleanup`. + +## 1. Specification + +- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-codex-task-2026-05-13-00-50`. +- [x] 1.2 Define normative requirements in `specs/codex-task/spec.md`. + +## 2. Implementation + +- [x] 2.1 Implement scoped behavior changes. +- [x] 2.2 Add/update focused regression coverage. + +## 3. Verification + +- [x] 3.1 Run targeted project verification commands. + - `node --test --test-name-pattern "CLAUDE.md|setup provisions workflow files" test/setup.test.js` passed 2/2. + - Full `node --test test/setup.test.js` ran the new cases successfully, but the suite still has unrelated baseline failures in hook redirect, older git `worktree --orphan`, masterplan path, disabled OpenSpec slug, and Colony package rename expectations. +- [x] 3.2 Run `openspec validate agent-codex-codex-task-2026-05-13-00-50 --type change --strict`. +- [x] 3.3 Run `openspec validate --specs`. + +## 4. Cleanup (mandatory; run before claiming completion) + +- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent// --base dev --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation. +- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff. +- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch). diff --git a/src/cli/main.js b/src/cli/main.js index 2abe06c6..1046af20 100755 --- a/src/cli/main.js +++ b/src/cli/main.js @@ -187,6 +187,7 @@ const { installUserLevelAsset, removeLegacyManagedRepoFile, ensureAgentsSnippet, + ensureClaudeAgentsLink, ensureManagedGitignore, buildRepoVscodeSettings, ensureRepoVscodeSettings, @@ -1581,6 +1582,7 @@ function runInstallInternal(options) { if (!options.skipAgents) { operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) })); + operations.push(ensureClaudeAgentsLink(repoRoot, Boolean(options.dryRun))); } const hookResult = configureHooks(repoRoot, Boolean(options.dryRun)); @@ -1664,6 +1666,7 @@ function runFixInternal(options) { if (!options.skipAgents) { operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) })); + operations.push(ensureClaudeAgentsLink(repoRoot, Boolean(options.dryRun))); } const hookResult = configureHooks(repoRoot, Boolean(options.dryRun)); diff --git a/src/scaffold/index.js b/src/scaffold/index.js index 1c12980e..d5631551 100644 --- a/src/scaffold/index.js +++ b/src/scaffold/index.js @@ -539,6 +539,24 @@ function ensureAgentsSnippet(repoRoot, dryRun) { return { status: 'updated', file: 'AGENTS.md' }; } +function ensureClaudeAgentsLink(repoRoot, dryRun) { + const claudePath = path.join(repoRoot, 'CLAUDE.md'); + try { + fs.lstatSync(claudePath); + return { status: 'unchanged', file: 'CLAUDE.md', note: 'existing path preserved' }; + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + + if (!dryRun) { + fs.symlinkSync('AGENTS.md', claudePath); + } + + return { status: dryRun ? 'would-create' : 'created', file: 'CLAUDE.md', note: 'symlink to AGENTS.md' }; +} + function ensureManagedGitignore(repoRoot, dryRun) { const gitignorePath = path.join(repoRoot, '.gitignore'); const managedBlock = [ @@ -763,6 +781,7 @@ module.exports = { installUserLevelAsset, removeLegacyManagedRepoFile, ensureAgentsSnippet, + ensureClaudeAgentsLink, ensureManagedGitignore, parseJsonObjectLikeFile, buildRepoVscodeSettings, diff --git a/test/setup.test.js b/test/setup.test.js index 426fd81d..3ac9ffde 100644 --- a/test/setup.test.js +++ b/test/setup.test.js @@ -98,6 +98,7 @@ test('setup provisions workflow files and repo config', () => { '.gitignore', '.vscode/settings.json', 'AGENTS.md', + 'CLAUDE.md', ]; for (const relativePath of requiredFiles) { @@ -149,6 +150,10 @@ test('setup provisions workflow files and repo config', () => { assert.match(agentsContent, /### Caveman style/); assert.match(agentsContent, /Answer order stays fixed: answer first, cause next, fix or next step last\./); + const claudeStats = fs.lstatSync(path.join(repoDir, 'CLAUDE.md')); + assert.equal(claudeStats.isSymbolicLink(), true, 'CLAUDE.md should link to AGENTS.md'); + assert.equal(fs.readlinkSync(path.join(repoDir, 'CLAUDE.md')), 'AGENTS.md'); + const gitignoreContent = fs.readFileSync(path.join(repoDir, '.gitignore'), 'utf8'); assert.match(gitignoreContent, /# multiagent-safety:START/); assert.match(gitignoreContent, /^scripts\/agent-session-state\.js$/m); @@ -203,6 +208,18 @@ test('setup provisions workflow files and repo config', () => { } }); +test('setup preserves an existing root CLAUDE.md instead of replacing it', () => { + const repoDir = initRepo(); + fs.writeFileSync(path.join(repoDir, 'CLAUDE.md'), '# Existing Claude guidance\n', 'utf8'); + + const result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + + assert.equal(fs.readFileSync(path.join(repoDir, 'CLAUDE.md'), 'utf8'), '# Existing Claude guidance\n'); + assert.equal(fs.lstatSync(path.join(repoDir, 'CLAUDE.md')).isSymbolicLink(), false); + assert.match(result.stdout, /existing path preserved/); +}); + test('setup on a fresh compose repo prints onboarding hints and installs a working docker loader', () => { const repoDir = initRepoOnBranch('main');