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', () => {