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.
This commit is contained in:
garrytan committed 2026-10-03 02:14:37 +00:00
1 parent f7958d8d5d
commit 569cdae04f
9 files changed
+19 -15

No files matched your search

+2 -1
View File
@@ -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).
+1
View File
@@ -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}}
+1
View File
@@ -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}}
+1
View File
@@ -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}}
+7 -6
View File
@@ -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/<slug>/, falling back to newest project-scoped
* under $GSTACK_STATE_ROOT/projects/<slug>/, 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=""
+1
View File
@@ -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}
+1 -1
View File
@@ -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);
+5 -3
View File
@@ -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');
-4
View File
@@ -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/<slug>/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/<slug>/<branch>-evidence.jsonl` — Step 16 cites this', reason: PENDING },
{ 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: 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 },