diff --git a/autoplan/SKILL.md.tmpl b/autoplan/SKILL.md.tmpl index 581136c17..0cd4b4406 100644 --- a/autoplan/SKILL.md.tmpl +++ b/autoplan/SKILL.md.tmpl @@ -39,7 +39,6 @@ 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}} @@ -225,7 +224,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_STATE_ROOT"/projects/$SLUG/*-design-*.md 2>/dev/null | head -1`, resolving the state root and slug as the Design Doc Check block does +- Discover design docs: `ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1` - 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 72eae1195..57f616ea6 100644 --- a/plan-ceo-review/SKILL.md.tmpl +++ b/plan-ceo-review/SKILL.md.tmpl @@ -112,7 +112,6 @@ 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 4cfa16642..cfd6ee8f9 100644 --- a/plan-devex-review/SKILL.md.tmpl +++ b/plan-devex-review/SKILL.md.tmpl @@ -106,7 +106,6 @@ 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 45e2b6b9b..b9f406b6b 100644 --- a/plan-eng-review/SKILL.md.tmpl +++ b/plan-eng-review/SKILL.md.tmpl @@ -126,7 +126,6 @@ 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 4394d10ee..757d85efe 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_STATE_ROOT/projects//, falling back to newest project-scoped + * under ~/.gstack/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 - * the state root, and the committed copy is what teammates see — but a stale old + * ~/.gstack, 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,8 +16,7 @@ * * The fragment carries no code fences — the {{DESIGN_DOC_DISCOVERY}} token * sits inside each caller's ```bash block. Callers must set $SLUG and - * $BRANCH and resolve $GSTACK_STATE_ROOT through the guarded gstack-paths - * eval first (and `setopt +o nomatch` for zsh); the block sets $DESIGN + * $BRANCH first (and `setopt +o nomatch` for zsh); the block sets $DESIGN * and prints "Design doc found: ..." or "No design doc found". */ @@ -28,10 +27,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_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) +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) # Repo-local docs win when at least as fresh (#703): office-hours dual-writes -# docs/designs/ alongside the state root, and the committed copy is what teammates +# docs/designs/ alongside ~/.gstack, 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 52a734353..d4fb16a7d 100644 --- a/scripts/resolvers/spec-review.ts +++ b/scripts/resolvers/spec-review.ts @@ -258,7 +258,6 @@ 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 21da98657..cc6fc8832 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, GSTACK_STATE_ROOT: path.join(root, 'state') }, encoding: 'utf8', timeout: 10_000, + 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`); 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 91939c052..900579352 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -1905,15 +1905,13 @@ 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')); - 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'); + 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]) { fs.mkdirSync(path.dirname(file), {recursive: true}); fs.writeFileSync(file, '# Design fixture\n'); } - const env = {...process.env, HOME: home, GSTACK_HOME: path.join(dir, 'state'), GSTACK_STATE_ROOT: path.join(dir, 'state'), + const env = {...process.env, HOME: home, GSTACK_HOME: path.join(home, '.gstack'), 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 481942ad1..2ffaaa158 100644 --- a/test/instruction-facts.test.ts +++ b/test/instruction-facts.test.ts @@ -169,8 +169,10 @@ 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, 4 of 4 runs); 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//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 }, @@ -181,6 +183,9 @@ 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//question-log.jsonl`)', reason: DESCRIPTION }, { file: 'plan-tune/SKILL.md.tmpl', match: '`~/.gstack/projects//question-log.jsonl` — nothing leaves your', reason: DESCRIPTION }, + { 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: 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 }, ];