W3 T3/T4/E2: design-doc discovery runs bin/gstack-design-doc-find on the state root; drop DESIGN_DOC_HELD rows

This commit is contained in:
garrytan committed 2026-10-03 20:44:55 +00:00
1 parent 769ed52eb1
commit dfb7330a8c
9 files changed
+183 -43

No files matched your search

@@ -23,7 +23,7 @@ function fixture(plan = original) {
const env = { PATH: process.env.PATH!, HOME: home, GSTACK_HOME: state, GSTACK_STATE_ROOT: state };
const start = () => execFileSync(join(root, 'bin/gstack-skill-start'), ['--skill', 'autoplan'],
{ cwd, env, encoding: 'utf8', timeout: 30_000 });
const discover = () => execFileSync('bash', ['-c', DESIGN_DOC_DISCOVERY_BLOCK],
const discover = () => execFileSync('bash', ['-c', DESIGN_DOC_DISCOVERY_BLOCK.replaceAll('~/.claude/skills/gstack/bin/', `${root}/bin/`)],
{ cwd, env: { ...env, SLUG: 'chain-fixture', BRANCH: 'main' }, encoding: 'utf8', timeout: 5000 }).trim();
return { cwd, home, state, planFile, start, discover, cleanup: () => rmSync(temp, { recursive: true, force: true }) };
}
+2 -1
View File
@@ -301,7 +301,8 @@ describe('CEO finding fixture establishes scope before launch', () => {
const plan = '# Export saved settings\nReview the CSV formatter before implementation.\n';
const design = '# Settings export design\n\n## Problem\nOperators need saved settings in a spreadsheet for offline comparison.\n\n## Approach\nReuse the settings API and escape commas, quotes, and newlines in a CSV formatter.\n';
seedCeoFindingProject(cwd, plan, design);
const output = execFileSync('bash', ['-c', `SLUG=fixture; BRANCH=main; ${DESIGN_DOC_DISCOVERY_BLOCK}`], {
const block = DESIGN_DOC_DISCOVERY_BLOCK.replaceAll('~/.claude/skills/gstack/bin/', `${path.resolve(import.meta.dir, '..')}/bin/`);
const output = execFileSync('bash', ['-c', `SLUG=fixture; BRANCH=main; ${block}`], {
cwd, env: { PATH: process.env.PATH!, HOME: home }, encoding: 'utf8', timeout: 10_000,
});
expect(output).toBe(`Design doc found: ${path.join(cwd, 'DESIGN.md')}\n`);
+4
View File
@@ -1927,6 +1927,10 @@ describe('BENEFITS_FROM resolver', () => {
fs.chmodSync(helper, 0o755);
fs.copyFileSync(path.join(ROOT, 'bin/gstack-state-root.sh'), path.join(path.dirname(helper), 'gstack-state-root.sh'));
fs.copyFileSync(path.join(ROOT, 'bin/gstack-remote-identity.sh'), path.join(path.dirname(helper), 'gstack-remote-identity.sh'));
for (const bin of ['gstack-paths', 'gstack-design-doc-find']) {
fs.copyFileSync(path.join(ROOT, 'bin', bin), path.join(path.dirname(helper), bin));
fs.chmodSync(path.join(path.dirname(helper), bin), 0o755);
}
const expected = path.join(home, '.gstack/projects/canonical-override/session-unknown-design-current.md');
const wrong = path.join(home, '.gstack/projects/project/session-unknown-design-wrong.md');
for (const file of [expected, wrong]) {
+111
View File
@@ -0,0 +1,111 @@
import { afterEach, describe, expect, test } from 'bun:test';
import { execFileSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
const ROOT = path.resolve(import.meta.dir, '..');
const BIN = path.join(ROOT, 'bin/gstack-design-doc-find');
const temps: string[] = [];
afterEach(() => {
for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
});
function fixture() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-doc-find-'));
temps.push(dir);
const home = path.join(dir, 'home');
const repo = path.join(dir, 'repo');
fs.mkdirSync(home, { recursive: true });
fs.mkdirSync(repo, { recursive: true });
execFileSync('git', ['init', '-q', '-b', 'main'], { cwd: repo, timeout: 10_000 });
const baseEnv = { PATH: process.env.PATH!, HOME: home, GIT_CEILING_DIRECTORIES: dir };
const write = (file: string, ageSeconds: number) => {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, '# Design\n');
const t = Date.now() / 1000 - ageSeconds;
fs.utimesSync(file, t, t);
return file;
};
const find = (env: Record<string, string> = {}, args = ['proj', 'feat-x']) =>
execFileSync(BIN, args, { cwd: repo, env: { ...baseEnv, ...env }, encoding: 'utf8', timeout: 10_000 }).trim();
return { dir, home, repo, write, find };
}
describe('gstack-design-doc-find', () => {
test('default root is ~/.gstack and prints nothing when no doc exists', () => {
const f = fixture();
expect(f.find()).toBe('');
const doc = f.write(path.join(f.home, '.gstack/projects/proj/user-main-design-20260101.md'), 100);
expect(f.find()).toBe(doc);
});
test('GSTACK_HOME moves the state root', () => {
const f = fixture();
f.write(path.join(f.home, '.gstack/projects/proj/user-main-design-a.md'), 10);
const custom = path.join(f.dir, 'custom-state');
const doc = f.write(path.join(custom, 'projects/proj/user-main-design-b.md'), 100);
expect(f.find({ GSTACK_HOME: custom })).toBe(doc);
});
test('a branch-specific doc wins over a newer generic doc', () => {
const f = fixture();
const branch = f.write(path.join(f.home, '.gstack/projects/proj/user-feat-x-design-1.md'), 500);
f.write(path.join(f.home, '.gstack/projects/proj/user-main-design-2.md'), 10);
expect(f.find()).toBe(branch);
expect(f.find({}, ['proj', 'other'])).toBe(path.join(f.home, '.gstack/projects/proj/user-main-design-2.md'));
});
test('repo DESIGN.md wins only when at least as fresh, and beats docs/designs', () => {
const f = fixture();
const local = f.write(path.join(f.home, '.gstack/projects/proj/user-feat-x-design-1.md'), 100);
const designs = f.write(path.join(f.repo, 'docs/designs/plan.md'), 10);
expect(f.find()).toBe(designs);
const top = f.write(path.join(f.repo, 'DESIGN.md'), 1000);
expect(f.find()).toBe(local);
fs.utimesSync(top, Date.now() / 1000, Date.now() / 1000);
expect(f.find()).toBe(top);
});
test('a repo doc is used when the state root has none', () => {
const f = fixture();
const designs = f.write(path.join(f.repo, 'docs/designs/plan.md'), 1000);
expect(f.find()).toBe(designs);
});
test('usage errors exit 2', () => {
const f = fixture();
expect(() => f.find({}, [])).toThrow();
});
});
describe('rendered Design Doc Check block', () => {
const rendered = fs.readFileSync(path.join(ROOT, 'plan-ceo-review/SKILL.md'), 'utf8');
const block = rendered.slice(rendered.indexOf('DESIGN=$(~/.claude/skills/gstack/bin/gstack-design-doc-find'))
.split('\n').slice(0, 2).join('\n');
test('the generated skill renders one helper invocation', () => {
expect(block).toContain('gstack-design-doc-find "$SLUG" "$BRANCH"');
expect(block).toContain('echo "Design doc found: $DESIGN"');
expect(block).toContain('echo "No design doc found"');
});
test('finds an office-hours doc under a custom GSTACK_HOME and keeps the default-root result', () => {
const f = fixture();
const installed = path.join(f.home, '.claude/skills/gstack/bin');
fs.mkdirSync(installed, { recursive: true });
for (const bin of ['gstack-design-doc-find', 'gstack-paths', 'gstack-state-root.sh']) {
fs.copyFileSync(path.join(ROOT, 'bin', bin), path.join(installed, bin));
fs.chmodSync(path.join(installed, bin), 0o755);
}
const run = (env: Record<string, string>) => execFileSync('bash', ['-c', `SLUG=proj; BRANCH=main; ${block}`],
{ cwd: f.repo, env: { PATH: process.env.PATH!, HOME: f.home, GIT_CEILING_DIRECTORIES: f.dir, ...env }, encoding: 'utf8', timeout: 10_000 }).trim();
const custom = path.join(f.dir, 'custom-state');
const doc = f.write(path.join(custom, 'projects/proj/garry-main-design-20261003-120000.md'), 60);
expect(run({ GSTACK_HOME: custom })).toBe(`Design doc found: ${doc}`);
expect(run({})).toBe('No design doc found');
const legacy = f.write(path.join(f.home, '.gstack/projects/proj/garry-main-design-20261001-120000.md'), 60);
expect(run({})).toBe(`Design doc found: ${legacy}`);
});
});
+2 -2
View File
@@ -269,7 +269,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
// (--max-concurrency, no retries), so worst-case cost is one pass of
// each, sharing the API budget with sibling tests — not the
// sequential ~+10min a local read suggests.
'plan-mode-no-op': ['bin/gstack-state-root.sh', 'lib/state-root.ts', 'test/fixtures/auto-decide-recommendation-361c.json',
'plan-mode-no-op': ['bin/gstack-state-root.sh', 'lib/state-root.ts', 'bin/gstack-design-doc-find', 'scripts/resolvers/design-doc-discovery.ts', 'scripts/resolvers/spec-review.ts', 'plan-devex-review/**', 'test/fixtures/auto-decide-recommendation-361c.json',
'test/fixtures/auto-decide-target-361c.json',
@@ -838,7 +838,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
'test/fixtures/outside-async-task-m-events.json',
'test/fixtures/autoplan-amend-input-77.json',
'test/fixtures/autoplan-phase-handoff-6714.json','scripts/resolvers/learnings.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/preamble/generate-preamble-bash.ts', 'test/fixtures/plan-scope-recovery-av.json', 'test/fixtures/design-scope-checkpoint-at.json', 'scripts/resolvers/composition.ts', 'autoplan/**', 'codex/**', 'bin/gstack-codex-probe', 'scripts/resolvers/review-dashboard.ts', 'scripts/resolvers/plan-gates.ts', 'scripts/resolvers/spec-review.ts', 'scripts/resolvers/outside-voice-steps.ts', 'scripts/resolvers/review-scope.ts', 'scripts/resolvers/outside-voice.ts', 'scripts/resolvers/design.ts', 'test/skill-e2e-autoplan-dual-voice.test.ts', 'bin/gstack-autoplan-snapshot.ts', 'test/fixtures/autoplan/t-ceo-omitted-obligations.json', 'test/fixtures/autoplan/u-ceo-original-loss.json', 'test/fixtures/autoplan/v-ceo-dangling-references.json',
'scripts/resolvers/design-doc-discovery.ts', 'plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/testing.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/tasks-section.ts', 'test/helpers/autoplan-phase-observer.ts', 'scripts/resolve-codex-generation-model.ts', 'scripts/resolvers/constants.ts'],
'scripts/resolvers/design-doc-discovery.ts', 'bin/gstack-design-doc-find', 'plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/testing.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/tasks-section.ts', 'test/helpers/autoplan-phase-observer.ts', 'scripts/resolve-codex-generation-model.ts', 'scripts/resolvers/constants.ts'],
// Multi-provider benchmark adapters — live API smoke against real claude/codex/gemini CLIs
'benchmark-providers-live': ['bin/gstack-model-benchmark', 'test/helpers/providers/**', 'test/helpers/benchmark-runner.ts', 'test/helpers/pricing.ts', 'test/skill-e2e-benchmark-providers.test.ts'],
-5
View File
@@ -169,10 +169,8 @@ const RATCHET_ALLOWLIST = JSON.parse(read('test/state-root-ratchet.allowlist.jso
const readinessEvalReason = RATCHET_ALLOWLIST.find(entry => entry.path === 'land-and-deploy/sections/readiness-gate.md.tmpl')!.reason;
const DESCRIPTION = 'user-facing description of where the data lives; no command reads or writes this literal';
const SOURCE_COMMENT = 'source comment, not rendered into any skill';
const DESIGN_DOC_HELD = 'held at the literal path: converting design-doc discovery to $GSTACK_STATE_ROOT made plan-eng-review ask its scope gate in prose instead of AskUserQuestion (plan-mode no-op eval: 5 of 5 runs failed with it, 6 of 6 passed without); see TODOS.md';
const STATE_ROOT_ALLOWLIST: Array<{ file: string; match: string; reason: string }> = [
{ file: 'autoplan/SKILL.md.tmpl', match: 'ls -t ~/.gstack/projects/$SLUG/*-design-*.md', reason: DESIGN_DOC_HELD },
{ file: 'autoplan/SKILL.md.tmpl', match: 'test plan on disk at ~/.gstack/projects/$SLUG/', reason: DESCRIPTION },
{ file: 'careful/SKILL.md.tmpl', match: '`~/.gstack/projects/<slug>/careful-patterns.txt` (per-project)', reason: DESCRIPTION },
{ file: 'design-shotgun/SKILL.md.tmpl', match: 'v1 schema at `~/.gstack/projects/$SLUG/taste-profile.json`', reason: DESCRIPTION },
@@ -183,9 +181,6 @@ const STATE_ROOT_ALLOWLIST: Array<{ file: string; match: string; reason: string
{ file: 'plan-ceo-review/sections/review-sections.md.tmpl', match: 'Keep in `~/.gstack/projects/` only (local, personal reference)', reason: DESCRIPTION },
{ file: 'plan-tune/SKILL.md.tmpl', match: 'Logs stay local (`~/.gstack/projects/<slug>/question-log.jsonl`)', reason: DESCRIPTION },
{ file: 'plan-tune/SKILL.md.tmpl', match: '`~/.gstack/projects/<slug>/question-log.jsonl` — nothing leaves your', reason: DESCRIPTION },
{ file: 'scripts/resolvers/design-doc-discovery.ts', match: '* under ~/.gstack/projects/<slug>/, falling back', reason: SOURCE_COMMENT },
{ file: 'scripts/resolvers/design-doc-discovery.ts', match: '_LOCALDOC=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md', reason: DESIGN_DOC_HELD },
{ file: 'scripts/resolvers/design-doc-discovery.ts', match: '_LOCALDOC=$(ls -t ~/.gstack/projects/$SLUG/*-design-*.md', reason: DESIGN_DOC_HELD },
{ file: 'scripts/resolvers/learnings.ts', match: '* Learnings are stored per-project at ~/.gstack/projects/{slug}/learnings.jsonl', reason: SOURCE_COMMENT },
{ file: 'scripts/resolvers/preamble.ts', match: 'local JSONL append to ~/.gstack/analytics/ (inline, inspectable)', reason: SOURCE_COMMENT },
];