From 7a50eb250ece1aba2f9f5d99058fceaede1ec4ef Mon Sep 17 00:00:00 2001 From: garrytan Date: Wed, 30 Sep 2026 06:44:24 +0000 Subject: [PATCH] refactor: apply W1 state-root edits in W2/W3/W5-owned files; one moved-code touchfile golden for all workstreams --- browse/src/routes/core.ts | 4 +- design-consultation/SKILL.md | 5 +- design-html/SKILL.md | 2 +- design-review/SKILL.md | 7 +- design-shotgun/SKILL.md | 5 +- office-hours/SKILL.md | 2 +- office-hours/sections/design-and-handoff.md | 5 +- plan-ceo-review/SKILL.md | 5 +- plan-design-review/SKILL.md | 2 +- review/sections/plan-completion.md | 3 +- scripts/resolvers/design.ts | 12 ++- scripts/resolvers/plan-gates.ts | 3 +- scripts/resolvers/spec-review.ts | 10 +- scripts/test-free-shards.ts | 7 +- ship/sections/plan-completion.md | 3 +- .../fixtures/state-root-selection-golden.json | 64 ------------- .../touchfile-moved-code/w1-state-root.json | 94 +++++++++++++++++++ test/state-root-ratchet.allowlist.json | 5 +- test/state-root-touchfile-superset.test.ts | 35 ------- 19 files changed, 142 insertions(+), 131 deletions(-) delete mode 100644 test/fixtures/state-root-selection-golden.json create mode 100644 test/fixtures/touchfile-moved-code/w1-state-root.json delete mode 100644 test/state-root-touchfile-superset.test.ts diff --git a/browse/src/routes/core.ts b/browse/src/routes/core.ts index c08074fa5..491fc5f76 100644 --- a/browse/src/routes/core.ts +++ b/browse/src/routes/core.ts @@ -5,6 +5,8 @@ */ import * as fs from 'fs'; +import * as path from 'path'; +import { resolveStateRoot } from '../../../lib/state-root'; import { json, type RouteEntry } from './table'; import { handleCookiePickerRoute } from '../cookie-picker-routes'; import { sanitizeReplacer } from '../sanitize'; @@ -16,7 +18,7 @@ function resolveWelcomePath(): string | null { const rawSlug = process.env.GSTACK_SLUG || 'unknown'; const slug = /^[a-z0-9_-]+$/.test(rawSlug) ? rawSlug : 'unknown'; const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp'; - const projectWelcome = `${homeDir}/.gstack/projects/${slug}/designs/welcome-page-20260331/finalized.html`; + const projectWelcome = path.join(resolveStateRoot(), 'projects', slug, 'designs', 'welcome-page-20260331', 'finalized.html'); if (fs.existsSync(projectWelcome)) return projectWelcome; // Fallback: built-in welcome page from gstack install. Reject SKILL_ROOT // values containing '..' for the same defense-in-depth reason. diff --git a/design-consultation/SKILL.md b/design-consultation/SKILL.md index 09667b1aa..a25908997 100644 --- a/design-consultation/SKILL.md +++ b/design-consultation/SKILL.md @@ -586,7 +586,7 @@ Commands: `generate` returns `sessionFile`; `iterate` requires that existing session. `variants` returns `paths` but creates no session: regenerate with an updated brief instead. **CRITICAL PATH RULE:** Design artifacts belong in `$GSTACK_STATE_ROOT/projects/$SLUG/designs/`. -Use `bin/gstack-paths`: GSTACK_HOME → plugin storage → ~/.gstack. Keep it even if temporary; never substitute +Use `bin/gstack-paths` (docs/state-root.md). Keep it even if temporary; never substitute .context/, docs/designs/ or another directory. These are user files, not application source. @@ -669,7 +669,8 @@ Read this project's taste profile: ```bash eval "$("~/.claude/skills/gstack/bin/gstack-slug" 2>/dev/null)" [ -n "${SLUG:-}" ] || { echo "NO_TASTE_PROFILE"; exit 0; } -_TASTE_PROFILE=~/.gstack/projects/$SLUG/taste-profile.json +eval "$("~/.claude/skills/gstack/bin/gstack-paths")"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" +_TASTE_PROFILE="$GSTACK_STATE_ROOT/projects/$SLUG/taste-profile.json" if [ -f "$_TASTE_PROFILE" ]; then # Schema v1: { dimensions: { fonts, colors, layouts, aesthetics }, sessions: [] } # Each dimension has approved[] and rejected[] entries with diff --git a/design-html/SKILL.md b/design-html/SKILL.md index aaa45be9b..4fff17ada 100644 --- a/design-html/SKILL.md +++ b/design-html/SKILL.md @@ -433,7 +433,7 @@ Commands: - `$D iterate --session /path/session.json --feedback "..." --output /path.png` — iterate **CRITICAL PATH RULE:** Design artifacts belong in `$GSTACK_STATE_ROOT/projects/$SLUG/designs/`. -Use `bin/gstack-paths`: GSTACK_HOME → plugin storage → ~/.gstack. Keep it even if temporary; never substitute +Use `bin/gstack-paths` (docs/state-root.md). Keep it even if temporary; never substitute .context/, docs/designs/ or another directory. These are user files, not application source. diff --git a/design-review/SKILL.md b/design-review/SKILL.md index b03a1dd45..1497370ac 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -769,7 +769,7 @@ Commands: - `$D iterate --session /path/session.json --feedback "..." --output /path.png` — iterate **CRITICAL PATH RULE:** Design artifacts belong in `$GSTACK_STATE_ROOT/projects/$SLUG/designs/`. -Use `bin/gstack-paths`: GSTACK_HOME → plugin storage → ~/.gstack. Keep it even if temporary; never substitute +Use `bin/gstack-paths` (docs/state-root.md). Keep it even if temporary; never substitute .context/, docs/designs/ or another directory. These are user files, not application source. @@ -1387,9 +1387,10 @@ Compare screenshots and observations across pages for: **Project-scoped:** ```bash -eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p "$GSTACK_STATE_ROOT/projects/$SLUG" && echo "PROJECT_DIR: $GSTACK_STATE_ROOT/projects/$SLUG" ``` -Write to: `~/.gstack/projects/{slug}/{user}-{branch}-design-audit-{datetime}.md` +Write to: `/{user}-{branch}-design-audit-{datetime}.md` (`PROJECT_DIR` printed above) **Baseline:** Write `design-baseline.json` for regression mode (temp file then `mv`, and a per-run copy `design-baseline..json` beside it): ```json diff --git a/design-shotgun/SKILL.md b/design-shotgun/SKILL.md index 4241cc7f3..bf88156fa 100644 --- a/design-shotgun/SKILL.md +++ b/design-shotgun/SKILL.md @@ -444,7 +444,7 @@ Commands: - `$D iterate --session /path/session.json --feedback "..." --output /path.png` — iterate **CRITICAL PATH RULE:** Design artifacts belong in `$GSTACK_STATE_ROOT/projects/$SLUG/designs/`. -Use `bin/gstack-paths`: GSTACK_HOME → plugin storage → ~/.gstack. Keep it even if temporary; never substitute +Use `bin/gstack-paths` (docs/state-root.md). Keep it even if temporary; never substitute .context/, docs/designs/ or another directory. These are user files, not application source. @@ -556,7 +556,8 @@ Read this project's taste profile: ```bash eval "$("~/.claude/skills/gstack/bin/gstack-slug" 2>/dev/null)" [ -n "${SLUG:-}" ] || { echo "NO_TASTE_PROFILE"; exit 0; } -_TASTE_PROFILE=~/.gstack/projects/$SLUG/taste-profile.json +eval "$("~/.claude/skills/gstack/bin/gstack-paths")"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" +_TASTE_PROFILE="$GSTACK_STATE_ROOT/projects/$SLUG/taste-profile.json" if [ -f "$_TASTE_PROFILE" ]; then # Schema v1: { dimensions: { fonts, colors, layouts, aesthetics }, sessions: [] } # Each dimension has approved[] and rejected[] entries with diff --git a/office-hours/SKILL.md b/office-hours/SKILL.md index fd3da9cac..0ae9a29ca 100644 --- a/office-hours/SKILL.md +++ b/office-hours/SKILL.md @@ -971,7 +971,7 @@ Generating visual mockups of the proposed design... (say "skip" if you don't nee ```bash eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" -eval "$(~/.claude/skills/gstack/bin/gstack-paths)" +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" _DESIGN_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/designs/mockup-$(date +%Y%m%d)" mkdir -p "$_DESIGN_DIR" echo "DESIGN_DIR: $_DESIGN_DIR" diff --git a/office-hours/sections/design-and-handoff.md b/office-hours/sections/design-and-handoff.md index 71a0576fe..e80018eed 100644 --- a/office-hours/sections/design-and-handoff.md +++ b/office-hours/sections/design-and-handoff.md @@ -256,8 +256,9 @@ inventing duplicate counts. Preserve the Assignment, coaching, approval, and Han Append the helper's actual metrics to the existing analytics log (telemetry is best-effort and must not block approval): ```bash -mkdir -p ~/.gstack/analytics -echo '{"skill":"office-hours","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","iterations":ITERATIONS,"issues_found":FOUND,"issues_fixed":FIXED,"remaining":REMAINING,"quality_score":SCORE}' >> ~/.gstack/analytics/spec-review.jsonl 2>/dev/null || true +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" +mkdir -p "$GSTACK_STATE_ROOT/analytics" +echo '{"skill":"office-hours","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","iterations":ITERATIONS,"issues_found":FOUND,"issues_fixed":FIXED,"remaining":REMAINING,"quality_score":SCORE}' >> "$GSTACK_STATE_ROOT/analytics/spec-review.jsonl" 2>/dev/null || true ``` Use iterations, issues_found, issues_fixed, remaining, and quality_score from the helper. FOUND counts finding observations across rounds; FIXED counts only diff --git a/plan-ceo-review/SKILL.md b/plan-ceo-review/SKILL.md index 27eec7f80..985c81bd0 100644 --- a/plan-ceo-review/SKILL.md +++ b/plan-ceo-review/SKILL.md @@ -1221,8 +1221,9 @@ forbidden, show the actual fields as not persisted and continue without writing. If the reviewer fails, report that limit and continue after recording the outcome; if a required save fails, stop before claiming completion. ```bash -mkdir -p ~/.gstack/analytics || exit 1 -echo '{"skill":"plan-ceo-review","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","iterations":ITERATIONS,"issues_found":FOUND,"issues_fixed":FIXED,"remaining":REMAINING,"quality_score":SCORE}' >> ~/.gstack/analytics/spec-review.jsonl || exit 1 +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" +mkdir -p "$GSTACK_STATE_ROOT/analytics" || exit 1 +echo '{"skill":"plan-ceo-review","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","iterations":ITERATIONS,"issues_found":FOUND,"issues_fixed":FIXED,"remaining":REMAINING,"quality_score":SCORE}' >> "$GSTACK_STATE_ROOT/analytics/spec-review.jsonl" || exit 1 ``` ITERATIONS counts actual reviewer launches. FOUND, FIXED and REMAINING count reported issues, reviewer-confirmed fixes and reported unresolved issues. Use actual counts, never estimates. diff --git a/plan-design-review/SKILL.md b/plan-design-review/SKILL.md index bc1edba87..0f4356161 100644 --- a/plan-design-review/SKILL.md +++ b/plan-design-review/SKILL.md @@ -688,7 +688,7 @@ Commands: - `$D iterate --session /path/session.json --feedback "..." --output /path.png` — iterate **CRITICAL PATH RULE:** Design artifacts belong in `$GSTACK_STATE_ROOT/projects/$SLUG/designs/`. -Use `bin/gstack-paths`: GSTACK_HOME → plugin storage → ~/.gstack. Keep it even if temporary; never substitute +Use `bin/gstack-paths` (docs/state-root.md). Keep it even if temporary; never substitute .context/, docs/designs/ or another directory. These are user files, not application source. diff --git a/review/sections/plan-completion.md b/review/sections/plan-completion.md index d06f7bc75..b35f8524e 100644 --- a/review/sections/plan-completion.md +++ b/review/sections/plan-completion.md @@ -14,7 +14,8 @@ BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-' | tr -cd 'a-zA-Z0-9. REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)") _PLAN_SLUG=$(git remote get-url origin 2>/dev/null | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-' | tr -cd 'a-zA-Z0-9._-') || true _PLAN_SLUG="${_PLAN_SLUG:-$(basename "$PWD" | tr -cd 'a-zA-Z0-9._-')}" -for PLAN_DIR in "$HOME/.gstack/projects/$_PLAN_SLUG" "$HOME/.claude/plans" "$HOME/.codex/plans" ".gstack/plans"; do +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" +for PLAN_DIR in "$GSTACK_STATE_ROOT/projects/$_PLAN_SLUG" "$HOME/.claude/plans" "$HOME/.codex/plans" ".gstack/plans"; do [ -d "$PLAN_DIR" ] || continue PLAN=$(ls -t "$PLAN_DIR"/*.md 2>/dev/null | xargs grep -l "$BRANCH" 2>/dev/null | head -1) [ -z "$PLAN" ] && PLAN=$(ls -t "$PLAN_DIR"/*.md 2>/dev/null | xargs grep -l "$REPO" 2>/dev/null | head -1) diff --git a/scripts/resolvers/design.ts b/scripts/resolvers/design.ts index 11ba4f49e..30f051242 100644 --- a/scripts/resolvers/design.ts +++ b/scripts/resolvers/design.ts @@ -489,9 +489,10 @@ Compare screenshots and observations across pages for: **Project-scoped:** \`\`\`bash -eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "\${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p "$GSTACK_STATE_ROOT/projects/$SLUG" && echo "PROJECT_DIR: $GSTACK_STATE_ROOT/projects/$SLUG" \`\`\` -Write to: \`~/.gstack/projects/{slug}/{user}-{branch}-design-audit-{datetime}.md\` +Write to: \`/{user}-{branch}-design-audit-{datetime}.md\` (\`PROJECT_DIR\` printed above) **Baseline:** Write \`design-baseline.json\` for regression mode (temp file then \`mv\`, and a per-run copy \`design-baseline..json\` beside it): \`\`\`json @@ -1148,7 +1149,7 @@ Commands: \`generate\` returns \`sessionFile\`; \`iterate\` requires that existing session. \`variants\` returns \`paths\` but creates no session: regenerate with an updated brief instead.` : ''} **CRITICAL PATH RULE:** Design artifacts belong in \`$GSTACK_STATE_ROOT/projects/$SLUG/designs/\`. -Use \`bin/gstack-paths\`: GSTACK_HOME → plugin storage → ~/.gstack. Keep it even if temporary; never substitute +Use \`bin/gstack-paths\` (docs/state-root.md). Keep it even if temporary; never substitute .context/, docs/designs/ or another directory. These are user files, not application source.`; } @@ -1175,7 +1176,7 @@ Generating visual mockups of the proposed design... (say "skip" if you don't nee \`\`\`bash eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" -eval "$(~/.claude/skills/gstack/bin/gstack-paths)" +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "\${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" _DESIGN_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/designs/mockup-$(date +%Y%m%d)" mkdir -p "$_DESIGN_DIR" echo "DESIGN_DIR: $_DESIGN_DIR" @@ -1382,7 +1383,8 @@ export function generateTasteProfile(ctx: TemplateContext): string { \`\`\`bash eval "$("${ctx.paths.binDir}/gstack-slug" 2>/dev/null)" [ -n "\${SLUG:-}" ] || { echo "NO_TASTE_PROFILE"; exit 0; } -_TASTE_PROFILE=~/.gstack/projects/$SLUG/taste-profile.json +eval "$("${ctx.paths.binDir}/gstack-paths")"; : "\${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" +_TASTE_PROFILE="$GSTACK_STATE_ROOT/projects/$SLUG/taste-profile.json" if [ -f "$_TASTE_PROFILE" ]; then # Schema v1: { dimensions: { fonts, colors, layouts, aesthetics }, sessions: [] } # Each dimension has approved[] and rejected[] entries with diff --git a/scripts/resolvers/plan-gates.ts b/scripts/resolvers/plan-gates.ts index a238a058f..89265d2fd 100644 --- a/scripts/resolvers/plan-gates.ts +++ b/scripts/resolvers/plan-gates.ts @@ -201,7 +201,8 @@ BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-' | tr -cd 'a-zA-Z0-9. REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)") _PLAN_SLUG=$(git remote get-url origin 2>/dev/null | sed 's|.*[:/]\\([^/]*/[^/]*\\)\\.git$|\\1|;s|.*[:/]\\([^/]*/[^/]*\\)$|\\1|' | tr '/' '-' | tr -cd 'a-zA-Z0-9._-') || true _PLAN_SLUG="\${_PLAN_SLUG:-$(basename "$PWD" | tr -cd 'a-zA-Z0-9._-')}" -for PLAN_DIR in "$HOME/.gstack/projects/$_PLAN_SLUG" "$HOME/.claude/plans" "$HOME/.codex/plans" ".gstack/plans"; do +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "\${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" +for PLAN_DIR in "$GSTACK_STATE_ROOT/projects/$_PLAN_SLUG" "$HOME/.claude/plans" "$HOME/.codex/plans" ".gstack/plans"; do [ -d "$PLAN_DIR" ] || continue PLAN=$(ls -t "$PLAN_DIR"/*.md 2>/dev/null | xargs grep -l "$BRANCH" 2>/dev/null | head -1) [ -z "$PLAN" ] && PLAN=$(ls -t "$PLAN_DIR"/*.md 2>/dev/null | xargs grep -l "$REPO" 2>/dev/null | head -1) diff --git a/scripts/resolvers/spec-review.ts b/scripts/resolvers/spec-review.ts index a1e6fff40..0c5d2f9f1 100644 --- a/scripts/resolvers/spec-review.ts +++ b/scripts/resolvers/spec-review.ts @@ -114,8 +114,9 @@ inventing duplicate counts. Preserve the Assignment, coaching, approval, and Han Append the helper's actual metrics to the existing analytics log (telemetry is best-effort and must not block approval): \`\`\`bash -mkdir -p ~/.gstack/analytics -echo '{"skill":"office-hours","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","iterations":ITERATIONS,"issues_found":FOUND,"issues_fixed":FIXED,"remaining":REMAINING,"quality_score":SCORE}' >> ~/.gstack/analytics/spec-review.jsonl 2>/dev/null || true +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "\${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" +mkdir -p "$GSTACK_STATE_ROOT/analytics" +echo '{"skill":"office-hours","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","iterations":ITERATIONS,"issues_found":FOUND,"issues_fixed":FIXED,"remaining":REMAINING,"quality_score":SCORE}' >> "$GSTACK_STATE_ROOT/analytics/spec-review.jsonl" 2>/dev/null || true \`\`\` Use iterations, issues_found, issues_fixed, remaining, and quality_score from the helper. FOUND counts finding observations across rounds; FIXED counts only @@ -209,8 +210,9 @@ if a required save fails, stop before claiming completion.` : `After the loop co 3. Append metrics:`} \`\`\`bash -mkdir -p ~/.gstack/analytics${ceo ? ' || exit 1' : ''} -echo '{"skill":"${_ctx.skillName}","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","iterations":ITERATIONS,"issues_found":FOUND,"issues_fixed":FIXED,"remaining":REMAINING,"quality_score":SCORE}' >> ~/.gstack/analytics/spec-review.jsonl${ceo ? ' || exit 1' : ' 2>/dev/null || true'} +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "\${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" +mkdir -p "$GSTACK_STATE_ROOT/analytics"${ceo ? ' || exit 1' : ''} +echo '{"skill":"${_ctx.skillName}","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","iterations":ITERATIONS,"issues_found":FOUND,"issues_fixed":FIXED,"remaining":REMAINING,"quality_score":SCORE}' >> "$GSTACK_STATE_ROOT/analytics/spec-review.jsonl"${ceo ? ' || exit 1' : ' 2>/dev/null || true'} \`\`\` ${ceo ? 'ITERATIONS counts actual reviewer launches. FOUND, FIXED and REMAINING count reported issues, reviewer-confirmed fixes and reported unresolved issues. Use actual counts, never estimates.' : 'Replace ITERATIONS, FOUND, FIXED, REMAINING, SCORE with actual values from the review.'}`; } diff --git a/scripts/test-free-shards.ts b/scripts/test-free-shards.ts index 83911f630..4bd9b6590 100755 --- a/scripts/test-free-shards.ts +++ b/scripts/test-free-shards.ts @@ -85,6 +85,7 @@ import { spawn, spawnSync } from 'child_process'; import { StringDecoder } from 'node:string_decoder'; import { createHash, randomUUID } from 'node:crypto'; import { isPaidTestFile } from '../test/helpers/paid-test-set'; +import { resolveStateRoot } from '../lib/state-root'; import { BunTestOutputClassifier, createShardSandbox, @@ -353,6 +354,10 @@ export const KNOWN_WINDOWS_INCOMPATIBLE: Array<{ file: string; reason: string }> // pattern hit is a false positive — the point of these files is Windows // coverage, so auto-excluding them defeats the regression tests they carry. const KNOWN_WINDOWS_SAFE: Array<{ file: string; reason: string }> = [ + { + file: 'test/state-root-parity.test.ts', + reason: 'runs the bash twin and lib/state-root.ts over an env table with PATH empty; no shebang execution, raw-string comparison is platform-neutral', + }, { file: 'test/qa-evidence.test.ts', reason: 'invokes the production helper through Bun argv and exercises native Windows job cleanup, private file captures and backpressured receipt output', @@ -1310,7 +1315,7 @@ export function flakeLedgerPath(env: NodeJS.ProcessEnv = process.env): string { const slug = spawnSync('bash', ['-c', '~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null'], { stdio: 'pipe', timeout: 3000 }) .stdout?.toString().match(/^SLUG=(.+)$/m)?.[1]; if (slug) { - const dir = path.join(os.homedir(), '.gstack', 'projects', slug); + const dir = path.join(resolveStateRoot(env), 'projects', slug); fs.mkdirSync(dir, { recursive: true }); return path.join(dir, 'flake-ledger.jsonl'); } diff --git a/ship/sections/plan-completion.md b/ship/sections/plan-completion.md index 82dd2c5df..1e658748a 100644 --- a/ship/sections/plan-completion.md +++ b/ship/sections/plan-completion.md @@ -33,7 +33,8 @@ BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-' | tr -cd 'a-zA-Z0-9. REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)") _PLAN_SLUG=$(git remote get-url origin 2>/dev/null | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-' | tr -cd 'a-zA-Z0-9._-') || true _PLAN_SLUG="${_PLAN_SLUG:-$(basename "$PWD" | tr -cd 'a-zA-Z0-9._-')}" -for PLAN_DIR in "$HOME/.gstack/projects/$_PLAN_SLUG" "$HOME/.claude/plans" "$HOME/.codex/plans" ".gstack/plans"; do +eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" +for PLAN_DIR in "$GSTACK_STATE_ROOT/projects/$_PLAN_SLUG" "$HOME/.claude/plans" "$HOME/.codex/plans" ".gstack/plans"; do [ -d "$PLAN_DIR" ] || continue PLAN=$(ls -t "$PLAN_DIR"/*.md 2>/dev/null | xargs grep -l "$BRANCH" 2>/dev/null | head -1) [ -z "$PLAN" ] && PLAN=$(ls -t "$PLAN_DIR"/*.md 2>/dev/null | xargs grep -l "$REPO" 2>/dev/null | head -1) diff --git a/test/fixtures/state-root-selection-golden.json b/test/fixtures/state-root-selection-golden.json deleted file mode 100644 index 71d41558f..000000000 --- a/test/fixtures/state-root-selection-golden.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "recordedAt": "96764e80", - "note": "paid evals selected by touching each W1 source file, recorded on the base commit before the change", - "modules": { - "lib/state-root.ts": { - "sources": [ - "browse/src/config.ts", - "lib/cso/state.ts" - ], - "e2e": [ - "aside-browse-basic", - "aside-browse-flow", - "benchmark-workflow", - "browse-basic", - "browse-snapshot", - "canary-workflow", - "carve-section-loading", - "cso-diff-mode", - "cso-full-audit", - "cso-infra-scope", - "design-review-detector-shim-dom", - "design-review-fix", - "diagram-authoring-quality", - "diagram-triplet", - "qa-b6-static", - "qa-b7-spa", - "qa-b8-checkout", - "qa-fix-loop", - "qa-only-no-fix", - "qa-quick" - ], - "llmJudge": [ - "browse/SKILL.md reference" - ] - }, - "bin/gstack-state-root.sh": { - "sources": [ - "bin/gstack-paths", - "careful/bin/hook-extract.sh" - ], - "e2e": [ - "investigate-owned-abort", - "investigate-owned-completion", - "investigate-owned-ending-error", - "plan-design-with-ui-scope" - ], - "llmJudge": [] - }, - "hosts/claude/hooks/hook-log.ts": { - "sources": [ - "hosts/claude/hooks/question-log-hook.ts", - "hosts/claude/hooks/question-preference-hook.ts", - "hosts/claude/hooks/auq-error-fallback-hook.ts", - "hosts/claude/hooks/timeline-stop-hook.ts", - "hosts/claude/hooks/memorable-user-prompt-hook.ts" - ], - "e2e": [ - "auto-decide-preserved", - "docsync-spawned" - ], - "llmJudge": [] - } - } -} diff --git a/test/fixtures/touchfile-moved-code/w1-state-root.json b/test/fixtures/touchfile-moved-code/w1-state-root.json new file mode 100644 index 000000000..c43352be6 --- /dev/null +++ b/test/fixtures/touchfile-moved-code/w1-state-root.json @@ -0,0 +1,94 @@ +{ + "workstream": "W1 state-root owners", + "recordedAt": "96764e80", + "sources": { + "browse/src/config.ts": { + "e2e": [ + "aside-browse-basic", + "aside-browse-flow", + "benchmark-workflow", + "browse-basic", + "browse-snapshot", + "canary-workflow", + "carve-section-loading", + "design-review-detector-shim-dom", + "design-review-fix", + "diagram-authoring-quality", + "diagram-triplet", + "qa-b6-static", + "qa-b7-spa", + "qa-b8-checkout", + "qa-fix-loop", + "qa-only-no-fix", + "qa-quick" + ], + "llmJudge": [ + "browse/SKILL.md reference" + ] + }, + "lib/cso/state.ts": { + "e2e": [ + "cso-diff-mode", + "cso-full-audit", + "cso-infra-scope" + ], + "llmJudge": [] + }, + "bin/gstack-paths": { + "e2e": [ + "plan-design-with-ui-scope" + ], + "llmJudge": [] + }, + "careful/bin/hook-extract.sh": { + "e2e": [ + "investigate-owned-abort", + "investigate-owned-completion", + "investigate-owned-ending-error" + ], + "llmJudge": [] + }, + "hosts/claude/hooks/question-log-hook.ts": { + "e2e": [], + "llmJudge": [] + }, + "hosts/claude/hooks/question-preference-hook.ts": { + "e2e": [ + "auto-decide-preserved", + "docsync-spawned" + ], + "llmJudge": [] + }, + "hosts/claude/hooks/auq-error-fallback-hook.ts": { + "e2e": [ + "docsync-spawned" + ], + "llmJudge": [] + }, + "hosts/claude/hooks/timeline-stop-hook.ts": { + "e2e": [], + "llmJudge": [] + }, + "hosts/claude/hooks/memorable-user-prompt-hook.ts": { + "e2e": [], + "llmJudge": [] + } + }, + "modules": { + "lib/state-root.ts": [ + "browse/src/config.ts", + "lib/cso/state.ts" + ], + "bin/gstack-state-root.sh": [ + "bin/gstack-paths", + "careful/bin/hook-extract.sh" + ], + "hosts/claude/hooks/hook-log.ts": [ + "hosts/claude/hooks/question-log-hook.ts", + "hosts/claude/hooks/question-preference-hook.ts", + "hosts/claude/hooks/auq-error-fallback-hook.ts", + "hosts/claude/hooks/timeline-stop-hook.ts", + "hosts/claude/hooks/memorable-user-prompt-hook.ts" + ] + } +} diff --git a/test/state-root-ratchet.allowlist.json b/test/state-root-ratchet.allowlist.json index f828584c5..03e5053f8 100644 --- a/test/state-root-ratchet.allowlist.json +++ b/test/state-root-ratchet.allowlist.json @@ -8,8 +8,5 @@ { "path": "bin/gstack-state-root.sh", "match": "*", "reason": "the bash owner of the chain" }, { "path": "careful/bin/check-careful.sh", "match": "_GSTACK_HOME_DIR=\"${GSTACK_HOME:-$HOME/.gstack}\"", "reason": "partial-upgrade fallback, reached only when an older hook-extract.sh lacks gstack_hook_state_root (legacy order kept on purpose)" }, { "path": "gstack-upgrade/SKILL.md.tmpl", "match": "elif [ -d \"$HOME/.gstack/repos/gstack/.git\" ]; then", "reason": "the relocated gstack checkout is an install location, not state" }, - { "path": "gstack-upgrade/SKILL.md.tmpl", "match": "INSTALL_DIR=\"$HOME/.gstack/repos/gstack\"", "reason": "the relocated gstack checkout is an install location, not state" }, - { "path": "scripts/resolvers/review.ts", "match": "*", "reason": "PENDING CROSS-OWNER (W5): delete this entry once the W1 handoff's review.ts / new-module edits are applied" }, - { "path": "scripts/resolvers/design.ts", "match": "*", "reason": "PENDING CROSS-OWNER (W5): delete this entry once the W1 handoff's design.ts edits are applied" }, - { "path": "scripts/test-free-shards.ts", "match": "*", "reason": "PENDING CROSS-OWNER (W2): delete this entry once the W1 handoff's test-free-shards.ts edit is applied" } + { "path": "gstack-upgrade/SKILL.md.tmpl", "match": "INSTALL_DIR=\"$HOME/.gstack/repos/gstack\"", "reason": "the relocated gstack checkout is an install location, not state" } ] diff --git a/test/state-root-touchfile-superset.test.ts b/test/state-root-touchfile-superset.test.ts deleted file mode 100644 index 890657796..000000000 --- a/test/state-root-touchfile-superset.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Touchfile coverage for W1's new modules: touching lib/state-root.ts, - * bin/gstack-state-root.sh or hosts/claude/hooks/hook-log.ts must select at - * least the paid evals their source files selected on the base commit - * (test/fixtures/state-root-selection-golden.json, recorded before the move). - */ -import { describe, test, expect } from 'bun:test'; -import * as fs from 'fs'; -import * as path from 'path'; -import { E2E_TOUCHFILES, LLM_JUDGE_TOUCHFILES, GLOBAL_TOUCHFILES, selectTests } from './helpers/touchfiles'; -import { resolveStateRoot } from '../lib/state-root'; - -const ROOT = path.resolve(import.meta.dir, '..'); -const GOLDEN = 'test/fixtures/state-root-selection-golden.json'; -const golden: { modules: Record } = - JSON.parse(fs.readFileSync(path.join(ROOT, GOLDEN), 'utf-8')); - -describe('W1 touchfile superset', () => { - test('the golden covers every new state-root module', () => { - expect(Object.keys(golden.modules).sort()).toEqual(['bin/gstack-state-root.sh', 'hosts/claude/hooks/hook-log.ts', 'lib/state-root.ts']); - expect(typeof resolveStateRoot).toBe('function'); - }); - - for (const [mod, rec] of Object.entries(golden.modules)) { - test(`touching ${mod} selects a superset of ${rec.sources.join(', ')}`, () => { - const e2e = new Set(selectTests([mod], E2E_TOUCHFILES, GLOBAL_TOUCHFILES).selected); - const llm = new Set(selectTests([mod], LLM_JUDGE_TOUCHFILES, GLOBAL_TOUCHFILES).selected); - const missing = [...rec.e2e.filter((t) => !e2e.has(t)), ...rec.llmJudge.filter((t) => !llm.has(t))]; - expect(missing, [ - `${mod} selects fewer paid evals than its source files did: ${missing.join(', ')}`, - `Fix: add ${mod} to those entries in test/helpers/touchfiles-data.ts (golden: ${GOLDEN}).`, - ].join('\n')).toEqual([]); - }); - } -});