From 569cdae04f18db79763cd3c1fc9fe8f6a9b7554c Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 3 Oct 2026 02:14:37 +0000 Subject: [PATCH] fix(state-root): design-doc discovery reads $GSTACK_STATE_ROOT/projects DESIGN_DOC_DISCOVERY and autoplan's Step 2 design-doc lookup read the literal ~/.gstack/projects path, so under a non-default state root they missed the docs office-hours writes. The block now globs "$GSTACK_STATE_ROOT"/projects, and each caller's bash block (autoplan, plan-ceo/devex/eng-review, the spec-review prerequisite recheck) resolves the root with the guarded gstack-paths eval first. Allowlist: drops the autoplan ls row, both _LOCALDOC rows and the resolver source-comment row from STATE_ROOT_ALLOWLIST. Tests: the eng Design Doc Check fixture installs gstack-paths and seeds docs under a non-default root; the CEO discovery fixture passes an explicit GSTACK_STATE_ROOT. --- autoplan/SKILL.md.tmpl | 3 ++- plan-ceo-review/SKILL.md.tmpl | 1 + plan-devex-review/SKILL.md.tmpl | 1 + plan-eng-review/SKILL.md.tmpl | 1 + scripts/resolvers/design-doc-discovery.ts | 13 +++++++------ scripts/resolvers/spec-review.ts | 1 + test/ceo-finding-fixture.test.ts | 2 +- test/gen-skill-docs.test.ts | 8 +++++--- test/instruction-facts.test.ts | 4 ---- 9 files changed, 19 insertions(+), 15 deletions(-) diff --git a/autoplan/SKILL.md.tmpl b/autoplan/SKILL.md.tmpl index 0cd4b4406..581136c17 100644 --- a/autoplan/SKILL.md.tmpl +++ b/autoplan/SKILL.md.tmpl @@ -39,6 +39,7 @@ allowed-tools: ```bash setopt +o nomatch 2>/dev/null || true # zsh compat +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') {{DESIGN_DOC_DISCOVERY}} @@ -224,7 +225,7 @@ Re-run: copy RESTORE_PATH's bytes to SOURCE_PLAN, then /autoplan. ### Step 2: Read context - Read CLAUDE.md, TODOS.md, git log -30, git diff against the base branch --stat -- Discover design docs: `ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1` +- Discover design docs: `ls -t "$GSTACK_STATE_ROOT"/projects/$SLUG/*-design-*.md 2>/dev/null | head -1`, resolving the state root and slug as the Design Doc Check block does - Detect UI scope: grep the plan for view/rendering terms (component, screen, form, button, modal, layout, dashboard, sidebar, nav, dialog). Require 2+ matches. Exclude false positives ("page" alone, "UI" in acronyms). diff --git a/plan-ceo-review/SKILL.md.tmpl b/plan-ceo-review/SKILL.md.tmpl index 6eaf3530f..878bb35eb 100644 --- a/plan-ceo-review/SKILL.md.tmpl +++ b/plan-ceo-review/SKILL.md.tmpl @@ -112,6 +112,7 @@ Then read CLAUDE.md, TODOS.md, and any existing architecture docs. **Design doc check:** ```bash setopt +o nomatch 2>/dev/null || true # zsh compat +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') {{DESIGN_DOC_DISCOVERY}} diff --git a/plan-devex-review/SKILL.md.tmpl b/plan-devex-review/SKILL.md.tmpl index cfd6ee8f9..4cfa16642 100644 --- a/plan-devex-review/SKILL.md.tmpl +++ b/plan-devex-review/SKILL.md.tmpl @@ -106,6 +106,7 @@ No background exploration before those questions; record unknowns for later. **Design doc check:** ```bash setopt +o nomatch 2>/dev/null || true +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') {{DESIGN_DOC_DISCOVERY}} diff --git a/plan-eng-review/SKILL.md.tmpl b/plan-eng-review/SKILL.md.tmpl index b9f406b6b..45e2b6b9b 100644 --- a/plan-eng-review/SKILL.md.tmpl +++ b/plan-eng-review/SKILL.md.tmpl @@ -126,6 +126,7 @@ Apply throughout, not as extra checks: ### Design Doc Check ```bash setopt +o nomatch 2>/dev/null || true # zsh compat +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" if _REVIEW_SLUG=$(~/.claude/skills/gstack/bin/gstack-slug); then eval "$_REVIEW_SLUG" {{DESIGN_DOC_DISCOVERY}} diff --git a/scripts/resolvers/design-doc-discovery.ts b/scripts/resolvers/design-doc-discovery.ts index 757d85efe..4394d10ee 100644 --- a/scripts/resolvers/design-doc-discovery.ts +++ b/scripts/resolvers/design-doc-discovery.ts @@ -2,10 +2,10 @@ * {{DESIGN_DOC_DISCOVERY}} — the canonical design-doc discovery block (#703). * * Finds the design doc a plan review should read: newest branch-scoped doc - * under ~/.gstack/projects//, falling back to newest project-scoped + * under $GSTACK_STATE_ROOT/projects//, falling back to newest project-scoped * doc, then lets a repo-local doc (DESIGN.md or docs/designs/*.md) win when * it is at least as fresh. office-hours dual-writes docs/designs/ alongside - * ~/.gstack, and the committed copy is what teammates see — but a stale old + * the state root, and the committed copy is what teammates see — but a stale old * repo doc must never shadow a newer private session. * * Single source of truth for the freshness-preference logic that previously @@ -16,7 +16,8 @@ * * The fragment carries no code fences — the {{DESIGN_DOC_DISCOVERY}} token * sits inside each caller's ```bash block. Callers must set $SLUG and - * $BRANCH first (and `setopt +o nomatch` for zsh); the block sets $DESIGN + * $BRANCH and resolve $GSTACK_STATE_ROOT through the guarded gstack-paths + * eval first (and `setopt +o nomatch` for zsh); the block sets $DESIGN * and prints "Design doc found: ..." or "No design doc found". */ @@ -27,10 +28,10 @@ import type { TemplateContext } from './types'; * re-check) can interpolate it into their own template strings instead of * embedding a drifting copy. */ -export const DESIGN_DOC_DISCOVERY_BLOCK = `_LOCALDOC=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) -[ -z "$_LOCALDOC" ] && _LOCALDOC=$(ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1) +export const DESIGN_DOC_DISCOVERY_BLOCK = `_LOCALDOC=$(ls -t "$GSTACK_STATE_ROOT"/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) +[ -z "$_LOCALDOC" ] && _LOCALDOC=$(ls -t "$GSTACK_STATE_ROOT"/projects/$SLUG/*-design-*.md 2>/dev/null | head -1) # Repo-local docs win when at least as fresh (#703): office-hours dual-writes -# docs/designs/ alongside ~/.gstack, and the committed copy is what teammates +# docs/designs/ alongside the state root, and the committed copy is what teammates # see. A stale old repo doc never shadows a newer private session. _REPOTOP=$(git rev-parse --show-toplevel 2>/dev/null || echo "") _REPODOC="" diff --git a/scripts/resolvers/spec-review.ts b/scripts/resolvers/spec-review.ts index d4fb16a7d..52a734353 100644 --- a/scripts/resolvers/spec-review.ts +++ b/scripts/resolvers/spec-review.ts @@ -258,6 +258,7 @@ Read the resulting doc if found; otherwise continue the standard review. Do not rerun the preamble or re-offer the prerequisite.` : `After /${first} completes, re-run the design doc check: \`\`\`bash setopt +o nomatch 2>/dev/null || true # zsh compat +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "\${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') ${DESIGN_DOC_DISCOVERY_BLOCK} diff --git a/test/ceo-finding-fixture.test.ts b/test/ceo-finding-fixture.test.ts index cc6fc8832..21da98657 100644 --- a/test/ceo-finding-fixture.test.ts +++ b/test/ceo-finding-fixture.test.ts @@ -302,7 +302,7 @@ describe('CEO finding fixture establishes scope before launch', () => { 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}`], { - cwd, env: { PATH: process.env.PATH!, HOME: home }, encoding: 'utf8', timeout: 10_000, + cwd, env: { PATH: process.env.PATH!, HOME: home, GSTACK_STATE_ROOT: path.join(root, 'state') }, encoding: 'utf8', timeout: 10_000, }); expect(output).toBe(`Design doc found: ${path.join(cwd, 'DESIGN.md')}\n`); expect(execFileSync('git', ['show', 'HEAD:DESIGN.md'], { cwd, encoding: 'utf8', timeout: 30_000 })).toBe(design); diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 7ed82d5b7..990c53a6f 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -1905,13 +1905,15 @@ 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')); - 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'); + fs.copyFileSync(path.join(ROOT, 'bin/gstack-paths'), path.join(path.dirname(helper), 'gstack-paths')); + fs.chmodSync(path.join(path.dirname(helper), 'gstack-paths'), 0o755); + const expected = path.join(dir, 'state/projects/canonical-override/session-unknown-design-current.md'); + const wrong = path.join(dir, 'state/projects/project/session-unknown-design-wrong.md'); for (const file of [expected, wrong]) { fs.mkdirSync(path.dirname(file), {recursive: true}); fs.writeFileSync(file, '# Design fixture\n'); } - const env = {...process.env, HOME: home, GSTACK_HOME: path.join(home, '.gstack'), + const env = {...process.env, HOME: home, GSTACK_HOME: path.join(dir, 'state'), GSTACK_STATE_ROOT: path.join(dir, 'state'), GSTACK_PROJECT_SLUG: 'canonical-override', GIT_CEILING_DIRECTORIES: dir}; for (const command of [initial, recheck]) { expect(command).toContain('if _REVIEW_SLUG=$(~/.claude/skills/gstack/bin/gstack-slug); then'); diff --git a/test/instruction-facts.test.ts b/test/instruction-facts.test.ts index 898b4b790..ac3096595 100644 --- a/test/instruction-facts.test.ts +++ b/test/instruction-facts.test.ts @@ -172,7 +172,6 @@ const PENDING = 'agent reads or writes this literal default-root path; it diverg const SOURCE_COMMENT = 'source comment, not rendered into any skill'; 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: PENDING }, { file: 'autoplan/SKILL.md.tmpl', match: 'test plan on disk at ~/.gstack/projects/$SLUG/', reason: DESCRIPTION }, { file: 'canary/SKILL.md.tmpl', match: 'Append it to `~/.gstack/projects/$SLUG/canary-history.jsonl`', reason: PENDING }, { file: 'careful/SKILL.md.tmpl', match: '`~/.gstack/projects//careful-patterns.txt` (per-project)', reason: DESCRIPTION }, @@ -201,9 +200,6 @@ const STATE_ROOT_ALLOWLIST: Array<{ file: string; match: string; reason: string { file: 'retro/SKILL.md.tmpl', match: 'Read `~/.gstack/analytics/eureka.jsonl`', reason: PENDING }, { file: 'ship/sections/apple-release.md.tmpl', match: 'to `~/.gstack/projects/$SLUG/apple-effects.log`', reason: PENDING }, { file: 'ship/sections/tests.md.tmpl', match: '`~/.gstack/projects//-evidence.jsonl` — Step 16 cites this', reason: PENDING }, - { file: 'scripts/resolvers/design-doc-discovery.ts', match: '* under ~/.gstack/projects//, falling back', reason: SOURCE_COMMENT }, - { file: 'scripts/resolvers/design-doc-discovery.ts', match: '_LOCALDOC=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md', reason: PENDING }, - { file: 'scripts/resolvers/design-doc-discovery.ts', match: '_LOCALDOC=$(ls -t ~/.gstack/projects/$SLUG/*-design-*.md', reason: PENDING }, { file: 'scripts/resolvers/design.ts', match: 'Glob \\`~/.gstack/projects/$SLUG/designs/**/approved.json\\`', reason: PENDING }, { 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 },