Skip to content

Commit 1e90ad4

Browse files
NagyViktNagyViktOmX
authored
Link Claude guidance during setup (#568)
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: NagyVikt <nagy.viktordp@gmail.com> Co-authored-by: OmX <omx@oh-my-codex.dev>
1 parent 626c24b commit 1e90ad4

8 files changed

Lines changed: 108 additions & 1 deletion

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,8 @@ the compact layout everywhere.
212212
| --- | --- |
213213
| `AGENTS.md` **with** markers | Refreshes **only** the managed block. |
214214
| `AGENTS.md` **without** markers | Appends the managed block to the end. |
215-
| No `AGENTS.md` | Creates it with the managed block. |
215+
| No `AGENTS.md` | Creates it with the managed block, then links `CLAUDE.md` to it. |
216+
| No root `CLAUDE.md` | Creates a `CLAUDE.md` symlink to `AGENTS.md`. |
216217
| A root `CLAUDE.md` | Leaves it alone. |
217218

218219
---
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-05-12
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## Why
2+
3+
- Claude Code reads `CLAUDE.md`, while Guardex keeps the canonical repo guidance in `AGENTS.md`.
4+
- Fresh `gx setup` repos currently get `AGENTS.md` only, so Claude sessions can miss the same safety contract.
5+
6+
## What Changes
7+
8+
- Make `gx setup` / `gx doctor` create a root `CLAUDE.md` symlink to `AGENTS.md` when `CLAUDE.md` is absent.
9+
- Preserve existing root `CLAUDE.md` files unchanged.
10+
- Update setup regression coverage and README guidance.
11+
12+
## Impact
13+
14+
- Affected surface is setup/doctor scaffold output only.
15+
- Existing user-authored Claude guidance remains untouched.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Setup links Claude guidance to AGENTS when absent
4+
`gx setup` and `gx doctor` SHALL create a root `CLAUDE.md` symlink to `AGENTS.md` when the target repository has no root `CLAUDE.md`.
5+
6+
#### Scenario: Fresh setup creates both guidance entrypoints
7+
- **WHEN** `gx setup --target <repo>` runs in a repo with no root `AGENTS.md` or `CLAUDE.md`
8+
- **THEN** `AGENTS.md` contains the Guardex managed guidance block
9+
- **AND** `CLAUDE.md` is a symlink whose target is `AGENTS.md`.
10+
11+
#### Scenario: Existing Claude guidance is preserved
12+
- **GIVEN** the target repo already has a root `CLAUDE.md`
13+
- **WHEN** `gx setup --target <repo>` runs
14+
- **THEN** Guardex SHALL leave the existing `CLAUDE.md` content and file type unchanged.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
## Definition of Done
2+
3+
This change is complete only when **all** of the following are true:
4+
5+
- Every checkbox below is checked.
6+
- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff.
7+
- 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.
8+
9+
## Handoff
10+
11+
- 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`.
12+
- 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`.
13+
14+
## 1. Specification
15+
16+
- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-codex-task-2026-05-13-00-50`.
17+
- [x] 1.2 Define normative requirements in `specs/codex-task/spec.md`.
18+
19+
## 2. Implementation
20+
21+
- [x] 2.1 Implement scoped behavior changes.
22+
- [x] 2.2 Add/update focused regression coverage.
23+
24+
## 3. Verification
25+
26+
- [x] 3.1 Run targeted project verification commands.
27+
- `node --test --test-name-pattern "CLAUDE.md|setup provisions workflow files" test/setup.test.js` passed 2/2.
28+
- 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.
29+
- [x] 3.2 Run `openspec validate agent-codex-codex-task-2026-05-13-00-50 --type change --strict`.
30+
- [x] 3.3 Run `openspec validate --specs`.
31+
32+
## 4. Cleanup (mandatory; run before claiming completion)
33+
34+
- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/<your-name>/<branch-slug> --base dev --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation.
35+
- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff.
36+
- [ ] 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).

src/cli/main.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ const {
187187
installUserLevelAsset,
188188
removeLegacyManagedRepoFile,
189189
ensureAgentsSnippet,
190+
ensureClaudeAgentsLink,
190191
ensureManagedGitignore,
191192
buildRepoVscodeSettings,
192193
ensureRepoVscodeSettings,
@@ -1581,6 +1582,7 @@ function runInstallInternal(options) {
15811582

15821583
if (!options.skipAgents) {
15831584
operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
1585+
operations.push(ensureClaudeAgentsLink(repoRoot, Boolean(options.dryRun)));
15841586
}
15851587

15861588
const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
@@ -1664,6 +1666,7 @@ function runFixInternal(options) {
16641666

16651667
if (!options.skipAgents) {
16661668
operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
1669+
operations.push(ensureClaudeAgentsLink(repoRoot, Boolean(options.dryRun)));
16671670
}
16681671

16691672
const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));

src/scaffold/index.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,24 @@ function ensureAgentsSnippet(repoRoot, dryRun) {
539539
return { status: 'updated', file: 'AGENTS.md' };
540540
}
541541

542+
function ensureClaudeAgentsLink(repoRoot, dryRun) {
543+
const claudePath = path.join(repoRoot, 'CLAUDE.md');
544+
try {
545+
fs.lstatSync(claudePath);
546+
return { status: 'unchanged', file: 'CLAUDE.md', note: 'existing path preserved' };
547+
} catch (error) {
548+
if (error.code !== 'ENOENT') {
549+
throw error;
550+
}
551+
}
552+
553+
if (!dryRun) {
554+
fs.symlinkSync('AGENTS.md', claudePath);
555+
}
556+
557+
return { status: dryRun ? 'would-create' : 'created', file: 'CLAUDE.md', note: 'symlink to AGENTS.md' };
558+
}
559+
542560
function ensureManagedGitignore(repoRoot, dryRun) {
543561
const gitignorePath = path.join(repoRoot, '.gitignore');
544562
const managedBlock = [
@@ -763,6 +781,7 @@ module.exports = {
763781
installUserLevelAsset,
764782
removeLegacyManagedRepoFile,
765783
ensureAgentsSnippet,
784+
ensureClaudeAgentsLink,
766785
ensureManagedGitignore,
767786
parseJsonObjectLikeFile,
768787
buildRepoVscodeSettings,

test/setup.test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ test('setup provisions workflow files and repo config', () => {
9898
'.gitignore',
9999
'.vscode/settings.json',
100100
'AGENTS.md',
101+
'CLAUDE.md',
101102
];
102103

103104
for (const relativePath of requiredFiles) {
@@ -149,6 +150,10 @@ test('setup provisions workflow files and repo config', () => {
149150
assert.match(agentsContent, /### Caveman style/);
150151
assert.match(agentsContent, /Answer order stays fixed: answer first, cause next, fix or next step last\./);
151152

153+
const claudeStats = fs.lstatSync(path.join(repoDir, 'CLAUDE.md'));
154+
assert.equal(claudeStats.isSymbolicLink(), true, 'CLAUDE.md should link to AGENTS.md');
155+
assert.equal(fs.readlinkSync(path.join(repoDir, 'CLAUDE.md')), 'AGENTS.md');
156+
152157
const gitignoreContent = fs.readFileSync(path.join(repoDir, '.gitignore'), 'utf8');
153158
assert.match(gitignoreContent, /# multiagent-safety:START/);
154159
assert.match(gitignoreContent, /^scripts\/agent-session-state\.js$/m);
@@ -203,6 +208,18 @@ test('setup provisions workflow files and repo config', () => {
203208
}
204209
});
205210

211+
test('setup preserves an existing root CLAUDE.md instead of replacing it', () => {
212+
const repoDir = initRepo();
213+
fs.writeFileSync(path.join(repoDir, 'CLAUDE.md'), '# Existing Claude guidance\n', 'utf8');
214+
215+
const result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir);
216+
assert.equal(result.status, 0, result.stderr || result.stdout);
217+
218+
assert.equal(fs.readFileSync(path.join(repoDir, 'CLAUDE.md'), 'utf8'), '# Existing Claude guidance\n');
219+
assert.equal(fs.lstatSync(path.join(repoDir, 'CLAUDE.md')).isSymbolicLink(), false);
220+
assert.match(result.stdout, /existing path preserved/);
221+
});
222+
206223

207224
test('setup on a fresh compose repo prints onboarding hints and installs a working docker loader', () => {
208225
const repoDir = initRepoOnBranch('main');

0 commit comments

Comments
 (0)