diff --git a/hosts/codex.ts b/hosts/codex.ts index 7dc80ea877..41842c6952 100644 --- a/hosts/codex.ts +++ b/hosts/codex.ts @@ -27,7 +27,7 @@ const codex: HostConfig = { pathRewrites: [ { from: '~/.claude/skills/gstack', to: '$GSTACK_ROOT' }, { from: '.claude/skills/gstack', to: '.agents/skills/gstack' }, - { from: '.claude/skills/review', to: '.agents/skills/gstack/review' }, + { from: '.claude/skills/review', to: '$GSTACK_ROOT/review' }, { from: '.claude/skills', to: '.agents/skills' }, ], @@ -44,7 +44,7 @@ const codex: HostConfig = { runtimeRoot: { globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'], globalFiles: { - 'review': ['checklist.md', 'TODOS-format.md'], + 'review': ['checklist.md', 'design-checklist.md', 'greptile-triage.md', 'TODOS-format.md'], }, }, sidecar: { diff --git a/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md index d99630c4b3..92988de28c 100644 --- a/test/fixtures/golden/codex-ship-SKILL.md +++ b/test/fixtures/golden/codex-ship-SKILL.md @@ -1231,7 +1231,7 @@ Use AskUserQuestion: - Continue with the workflow. **If "Add as P0 TODO":** -- If `TODOS.md` exists, add the entry following the format in `review/TODOS-format.md` (or `.agents/skills/gstack/review/TODOS-format.md`). +- If `TODOS.md` exists, add the entry following the format in `review/TODOS-format.md` (or `$GSTACK_ROOT/review/TODOS-format.md`). - If `TODOS.md` does not exist, create it with the standard header and add the entry. - Entry should include: title, the error output, which branch it was noticed on, and priority P0. - Continue with the workflow — treat the pre-existing failure as non-blocking. @@ -1907,7 +1907,7 @@ Before reviewing code quality, check: **did they build what was requested — no Review the diff for structural issues that tests don't catch. -1. Read `.agents/skills/gstack/review/checklist.md`. If the file cannot be read, **STOP** and report the error. +1. Read `$GSTACK_ROOT/review/checklist.md`. If the file cannot be read, **STOP** and report the error. 2. Run `git diff origin/` to get the full diff (scoped to feature changes against the freshly-fetched base branch). @@ -1991,7 +1991,7 @@ source <($GSTACK_BIN/gstack-diff-scope 2>/dev/null) 1. **Check for DESIGN.md.** If `DESIGN.md` or `design-system.md` exists in the repo root, read it. All design findings are calibrated against it — patterns blessed in DESIGN.md are not flagged. If not found, use universal design principles. -2. **Read `.agents/skills/gstack/review/design-checklist.md`.** If the file cannot be read, skip design review with a note: "Design checklist not found — skipping design review." +2. **Read `$GSTACK_ROOT/review/design-checklist.md`.** If the file cannot be read, skip design review with a note: "Design checklist not found — skipping design review." 3. **Read each changed frontend file** (full file, not just diff hunks). Frontend files are identified by the patterns listed in the checklist. @@ -2088,7 +2088,7 @@ Save the review output — it goes into the PR body in Step 19. **Subagent prompt:** -> You are classifying Greptile review comments for a /ship workflow. Read `.agents/skills/gstack/review/greptile-triage.md` and follow the fetch, filter, classify, and **escalation detection** steps. Do NOT fix code, do NOT reply to comments, do NOT commit — report only. +> You are classifying Greptile review comments for a /ship workflow. Read `$GSTACK_ROOT/review/greptile-triage.md` and follow the fetch, filter, classify, and **escalation detection** steps. Do NOT fix code, do NOT reply to comments, do NOT commit — report only. > > For each comment, assign: `classification` (`valid_actionable`, `already_fixed`, `false_positive`, `suppressed`), `escalation_tier` (1 or 2), the file:line or [top-level] tag, body summary, and permalink URL. > @@ -2263,7 +2263,7 @@ stay agent judgment; the slot pick stays `gstack-next-version`. Cross-reference the project's TODOS.md against the changes being shipped. Mark completed items automatically; prompt only if the file is missing or disorganized. -Read `.agents/skills/gstack/review/TODOS-format.md` for the canonical format reference. +Read `$GSTACK_ROOT/review/TODOS-format.md` for the canonical format reference. **1. Check if TODOS.md exists** in the repository root. diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 2fb783ffd0..f0765f4975 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -1665,6 +1665,7 @@ describe('DESIGN_REVIEW_LITE extended with Codex', () => { describe('Codex generation (--host codex)', () => { const AGENTS_DIR = path.join(ROOT, '.agents', 'skills'); + const REVIEW_RESOURCES = ['checklist.md', 'design-checklist.md', 'greptile-triage.md', 'TODOS-format.md']; // .agents/ is gitignored (v0.11.2.0) — generate on demand for tests Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'codex'], { @@ -1874,31 +1875,25 @@ describe('Codex generation (--host codex)', () => { // ─── Path rewriting regression tests ───────────────────────── - test('sidecar paths point to .agents/skills/gstack/review/ (not gstack-review/)', () => { - // Regression: gen-skill-docs rewrote .claude/skills/review → .agents/skills/gstack-review - // but setup puts sidecars under .agents/skills/gstack/review/. Must match setup layout. + test('review resources resolve through the dynamic GSTACK_ROOT', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-review', 'SKILL.md'), 'utf-8'); - // Correct: references to sidecar files use gstack/review/ path - expect(content).toContain('.agents/skills/gstack/review/checklist.md'); - // design-checklist.md is now referenced via Review Army specialist (Claude only, stripped for Codex) - // Wrong: must NOT reference gstack-review/checklist.md (file doesn't exist there) - expect(content).not.toContain('.agents/skills/gstack-review/checklist.md'); + expect(content).toContain('$GSTACK_ROOT/review/checklist.md'); + expect(content).not.toContain('.agents/skills/gstack/review/checklist.md'); }); - test('sidecar paths in ship skill point to gstack/review/ for pre-landing review', () => { + test('ship review resources resolve through the dynamic GSTACK_ROOT', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'), 'utf-8'); - // Ship references the review checklist in its pre-landing review step - if (content.includes('checklist.md')) { - expect(content).toContain('.agents/skills/gstack/review/'); - expect(content).not.toContain('.agents/skills/gstack-review/checklist'); + for (const resource of REVIEW_RESOURCES) { + expect(content).toContain(`$GSTACK_ROOT/review/${resource}`); } + expect(content).not.toContain('.agents/skills/gstack/review/'); }); - test('greptile-triage sidecar path is correct', () => { + test('greptile-triage resource resolves through the dynamic GSTACK_ROOT', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-review', 'SKILL.md'), 'utf-8'); if (content.includes('greptile-triage')) { - expect(content).toContain('.agents/skills/gstack/review/greptile-triage.md'); - expect(content).not.toContain('.agents/skills/gstack-review/greptile-triage'); + expect(content).toContain('$GSTACK_ROOT/review/greptile-triage.md'); + expect(content).not.toContain('.agents/skills/gstack/review/greptile-triage.md'); } }); @@ -1913,7 +1908,7 @@ describe('Codex generation (--host codex)', () => { // Rule 2: .claude/skills/gstack → .agents/skills/gstack expect(content).not.toContain('.claude/skills/gstack'); - // Rule 3: .claude/skills/review → .agents/skills/gstack/review + // Rule 3: .claude/skills/review → $GSTACK_ROOT/review expect(content).not.toContain('.claude/skills/review'); // Rule 4: .claude/skills → .agents/skills (catch-all) @@ -1930,10 +1925,64 @@ describe('Codex generation (--host codex)', () => { if (content.includes('gstack-config') || content.includes('gstack-update-check') || content.includes('gstack-telemetry-log')) { expect(content).toContain('$GSTACK_ROOT'); } - // If a skill references checklist.md, it must use the correct sidecar path - if (content.includes('checklist.md') && !content.includes('design-checklist.md')) { - expect(content).not.toContain('gstack-review/checklist.md'); + expect(content).not.toContain('.agents/skills/gstack/review/'); + } + }); + + test('consumer repos resolve Codex review resources from global and repo-local installs', () => { + const reviewContent = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-review', 'SKILL.md'), 'utf-8'); + const shipContent = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'), 'utf-8'); + const combined = `${reviewContent}\n${shipContent}`; + for (const resource of REVIEW_RESOURCES) { + expect(combined).toContain(`$GSTACK_ROOT/review/${resource}`); + } + + const preamble = reviewContent.match(/```bash\n(_ROOT=.*\nGSTACK_ROOT=.*\n\[ -n .*\n)/)?.[1]; + expect(preamble).toBeDefined(); + + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-codex-consumer-')); + const homeDir = path.join(tmpDir, 'home'); + const consumerRepo = path.join(tmpDir, 'consumer'); + const globalRoot = path.join(homeDir, '.codex', 'skills', 'gstack'); + const localRoot = path.join(consumerRepo, '.agents', 'skills', 'gstack'); + + const installResources = (root: string) => { + fs.mkdirSync(path.join(root, 'review'), { recursive: true }); + for (const resource of REVIEW_RESOURCES) { + fs.copyFileSync(path.join(ROOT, 'review', resource), path.join(root, 'review', resource)); } + }; + const resolveRuntimeRoot = (): string => { + const result = Bun.spawnSync(['bash', '-c', `${preamble}printf '%s' "$GSTACK_ROOT"`], { + cwd: consumerRepo, + env: { ...process.env, HOME: homeDir }, + stdout: 'pipe', + stderr: 'pipe', + }); + expect(result.exitCode).toBe(0); + return result.stdout.toString(); + }; + + try { + fs.mkdirSync(homeDir, { recursive: true }); + fs.mkdirSync(consumerRepo, { recursive: true }); + const gitInit = Bun.spawnSync(['git', 'init', '-q'], { cwd: consumerRepo, stdout: 'pipe', stderr: 'pipe' }); + expect(gitInit.exitCode).toBe(0); + + installResources(globalRoot); + expect(resolveRuntimeRoot()).toBe(globalRoot); + for (const resource of REVIEW_RESOURCES) { + expect(fs.existsSync(path.join(globalRoot, 'review', resource))).toBe(true); + } + + installResources(localRoot); + const resolvedLocalRoot = resolveRuntimeRoot(); + expect(fs.realpathSync(resolvedLocalRoot)).toBe(fs.realpathSync(localRoot)); + for (const resource of REVIEW_RESOURCES) { + expect(fs.existsSync(path.join(resolvedLocalRoot, 'review', resource))).toBe(true); + } + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); } }); diff --git a/test/host-config.test.ts b/test/host-config.test.ts index 1c939d7be2..bc7a9c49c5 100644 --- a/test/host-config.test.ts +++ b/test/host-config.test.ts @@ -379,6 +379,9 @@ describe('host-config-export.ts CLI', () => { expect(lines).toContain('bin'); expect(lines).toContain('ETHOS.md'); expect(lines).toContain('review/checklist.md'); + expect(lines).toContain('review/design-checklist.md'); + expect(lines).toContain('review/greptile-triage.md'); + expect(lines).toContain('review/TODOS-format.md'); }); test('opencode symlinks returns nested runtime assets', () => {