Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions hosts/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],

Expand All @@ -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: {
Expand Down
10 changes: 5 additions & 5 deletions test/fixtures/golden/codex-ship-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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/<base>` to get the full diff (scoped to feature changes against the freshly-fetched base branch).

Expand Down Expand Up @@ -1991,7 +1991,7 @@ source <($GSTACK_BIN/gstack-diff-scope <base> 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.

Expand Down Expand Up @@ -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.
>
Expand Down Expand Up @@ -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.

Expand Down
89 changes: 69 additions & 20 deletions test/gen-skill-docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'], {
Expand Down Expand Up @@ -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');
}
});

Expand All @@ -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)
Expand 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 });
}
});

Expand Down
3 changes: 3 additions & 0 deletions test/host-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down