diff --git a/design-consultation/SKILL.md.tmpl b/design-consultation/SKILL.md.tmpl index a48b49a21..5052941d0 100644 --- a/design-consultation/SKILL.md.tmpl +++ b/design-consultation/SKILL.md.tmpl @@ -33,7 +33,7 @@ gbrain: render_as: "## Existing DESIGN.md (if any)" - id: prior-design-decisions kind: filesystem - glob: "~/.gstack/projects/{repo_slug}/*-design-*.md" + glob: "{gstack_state_root}/projects/{repo_slug}/*-design-*.md" sort: mtime_desc limit: 3 render_as: "## Prior design decisions for this project" diff --git a/design-shotgun/SKILL.md.tmpl b/design-shotgun/SKILL.md.tmpl index 16c7a8ccb..22bc077b3 100644 --- a/design-shotgun/SKILL.md.tmpl +++ b/design-shotgun/SKILL.md.tmpl @@ -36,7 +36,7 @@ gbrain: render_as: "## DESIGN.md (project design system)" - id: recent-design-docs kind: filesystem - glob: "~/.gstack/projects/{repo_slug}/*-design-*.md" + glob: "{gstack_state_root}/projects/{repo_slug}/*-design-*.md" sort: mtime_desc limit: 3 render_as: "## Recent design docs" diff --git a/investigate/SKILL.md.tmpl b/investigate/SKILL.md.tmpl index eb0846c24..ab57e5fcd 100644 --- a/investigate/SKILL.md.tmpl +++ b/investigate/SKILL.md.tmpl @@ -51,12 +51,12 @@ gbrain: render_as: "## Prior investigations in this repo" - id: project-learnings kind: filesystem - glob: "~/.gstack/projects/{repo_slug}/learnings.jsonl" + glob: "{gstack_state_root}/projects/{repo_slug}/learnings.jsonl" tail: 10 render_as: "## Recent learnings (patterns + pitfalls)" - id: recent-eureka kind: filesystem - glob: "~/.gstack/analytics/eureka.jsonl" + glob: "{gstack_state_root}/analytics/eureka.jsonl" tail: 5 render_as: "## Recent eureka moments (cross-project)" --- diff --git a/office-hours/SKILL.md.tmpl b/office-hours/SKILL.md.tmpl index 1300ce7a0..c682ee1b8 100644 --- a/office-hours/SKILL.md.tmpl +++ b/office-hours/SKILL.md.tmpl @@ -41,17 +41,17 @@ gbrain: render_as: "## Prior office-hours sessions in this repo" - id: builder-profile kind: filesystem - glob: "~/.gstack/developer-profile.json" + glob: "{gstack_state_root}/developer-profile.json" render_as: "## Your builder profile snapshot" - id: design-doc-history kind: filesystem - glob: "~/.gstack/projects/{repo_slug}/*-design-*.md" + glob: "{gstack_state_root}/projects/{repo_slug}/*-design-*.md" sort: mtime_desc limit: 3 render_as: "## Recent design docs for this project" - id: prior-eureka kind: filesystem - glob: "~/.gstack/analytics/eureka.jsonl" + glob: "{gstack_state_root}/analytics/eureka.jsonl" tail: 5 render_as: "## Recent eureka moments" --- diff --git a/plan-ceo-review/SKILL.md.tmpl b/plan-ceo-review/SKILL.md.tmpl index 878bb35eb..72eae1195 100644 --- a/plan-ceo-review/SKILL.md.tmpl +++ b/plan-ceo-review/SKILL.md.tmpl @@ -36,7 +36,7 @@ gbrain: render_as: "## Prior CEO plans for this project" - id: recent-design-docs kind: filesystem - glob: "~/.gstack/projects/{repo_slug}/*-design-*.md" + glob: "{gstack_state_root}/projects/{repo_slug}/*-design-*.md" sort: mtime_desc limit: 3 render_as: "## Recent design docs for this project" diff --git a/retro/SKILL.md.tmpl b/retro/SKILL.md.tmpl index fea06db3b..2fb207723 100644 --- a/retro/SKILL.md.tmpl +++ b/retro/SKILL.md.tmpl @@ -30,12 +30,12 @@ gbrain: render_as: "## Prior retros for this project" - id: recent-timeline kind: filesystem - glob: "~/.gstack/projects/{repo_slug}/timeline.jsonl" + glob: "{gstack_state_root}/projects/{repo_slug}/timeline.jsonl" tail: 30 render_as: "## Recent timeline events" - id: recent-learnings kind: filesystem - glob: "~/.gstack/projects/{repo_slug}/learnings.jsonl" + glob: "{gstack_state_root}/projects/{repo_slug}/learnings.jsonl" tail: 10 render_as: "## Recent learnings" --- diff --git a/test/gstack-brain-context-load.test.ts b/test/gstack-brain-context-load.test.ts index c47d31db4..9fbc49d2a 100644 --- a/test/gstack-brain-context-load.test.ts +++ b/test/gstack-brain-context-load.test.ts @@ -606,6 +606,51 @@ gbrain: } }); +describe("gstack-brain-context-load — shipped state-root manifest files read the configured root", () => { + const cases = [ + { skill: "investigate", id: "project-learnings", file: ["projects", "test-repo", "learnings.jsonl"] }, + { skill: "investigate", id: "recent-eureka", file: ["analytics", "eureka.jsonl"] }, + { skill: "office-hours", id: "design-doc-history", file: ["projects", "test-repo", "alice-main-design-20260914.md"] }, + ]; + for (const c of cases) it(`${c.skill}:${c.id} reads only the configured root`, () => { + const dir = mkdtempSync(join(tmpdir(), "gstack-state-manifest-")); + const home = join(dir, "home"); + const configured = join(dir, "configured"); + try { + const target = join(configured, ...c.file); + mkdirSync(join(target, ".."), { recursive: true }); + writeFileSync(target, "{}\n"); + mkdirSync(home, { recursive: true }); + const query = parseSkillManifest(join(import.meta.dir, "..", c.skill, "SKILL.md.tmpl"))! + .context_queries.find(item => item.id === c.id)!; + expect(query.glob!.startsWith("{gstack_state_root}/")).toBe(true); + const skillFile = join(dir, "SKILL.md"); + writeFileSync(skillFile, `--- +name: state-manifest-fixture +gbrain: + schema: 1 + context_queries: + - id: ${query.id} + kind: filesystem + glob: "${query.glob}" + render_as: "${query.render_as}" +--- +`); + const env = { HOME: home, USERPROFILE: home, GSTACK_HOME: configured, CLAUDE_PLUGIN_DATA: "", CLAUDE_PLUGIN_ROOT: "" }; + const r = runScript(["--skill-file", skillFile, "--repo", "test-repo", "--explain"], env); + expect(r.exitCode).toBe(0); + expect(r.stderr).toContain(`OK ${c.id}`); + expect(r.stdout).toContain(c.file[c.file.length - 1]!); + rmSync(target); + mkdirSync(join(home, ".gstack", ...c.file.slice(0, -1)), { recursive: true }); + writeFileSync(join(home, ".gstack", ...c.file), "{}\n"); + const legacyOnly = runScript(["--skill-file", skillFile, "--repo", "test-repo", "--explain"], env); + expect(legacyOnly.exitCode).toBe(0); + expect(legacyOnly.stderr).toContain(`SKIP ${c.id}`); + } finally { rmSync(dir, { recursive: true, force: true }); } + }); +}); + describe("gstack-brain-context-load — real gbrain CLI contract (#2883)", () => { const ROWS = [ "plans/brand-plan\\tceo-plan\\t2026-10-01\\tBrand plan", diff --git a/test/instruction-facts.test.ts b/test/instruction-facts.test.ts index 64a74e0cb..481942ad1 100644 --- a/test/instruction-facts.test.ts +++ b/test/instruction-facts.test.ts @@ -160,37 +160,27 @@ describe('/ship step references resolve to the step they mean', () => { // Every literal default-root state path in templates and resolvers. Writers // resolve $GSTACK_STATE_ROOT (docs/state-root.md); a literal ~/.gstack path -// only matches them when the state root is the default. Rows are either -// user-facing descriptions, manifest/legacy forms read literally, or -// conversions still pending (tracked in TODOS.md). New hits fail. +// only matches them when the state root is the default. Rows are user-facing +// descriptions, source comments, or readers that must match a writer outside +// the state root. Manifest globs use the `{gstack_state_root}/` prefix. New +// hits fail. const STATE_ROOT_LITERAL = /~\/\.gstack\/(?:projects|analytics)\//g; const RATCHET_ALLOWLIST = JSON.parse(read('test/state-root-ratchet.allowlist.json')) as Array<{ path: string; match: string; reason: string }>; const readinessEvalReason = RATCHET_ALLOWLIST.find(entry => entry.path === 'land-and-deploy/sections/readiness-gate.md.tmpl')!.reason; -const MANIFEST_GLOB = 'gbrain context_queries manifest glob: bin/gstack-brain-context-load.ts reads the legacy `~` form literally (default state root only); `{gstack_state_root}/` is the state-root form. Conversion pending (TODOS.md: state-root literals in templates)'; const DESCRIPTION = 'user-facing description of where the data lives; no command reads or writes this literal'; -const PENDING = 'agent reads or writes this literal default-root path; it diverges from $GSTACK_STATE_ROOT writers under a non-default state root. Conversion pending (TODOS.md: state-root literals in templates)'; 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: '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-consultation/SKILL.md.tmpl', match: 'glob: "~/.gstack/projects/{repo_slug}/*-design-*.md"', reason: MANIFEST_GLOB }, - { file: 'design-shotgun/SKILL.md.tmpl', match: 'glob: "~/.gstack/projects/{repo_slug}/*-design-*.md"', reason: MANIFEST_GLOB }, { file: 'design-shotgun/SKILL.md.tmpl', match: 'v1 schema at `~/.gstack/projects/$SLUG/taste-profile.json`', reason: DESCRIPTION }, - { file: 'investigate/SKILL.md.tmpl', match: 'glob: "~/.gstack/projects/{repo_slug}/learnings.jsonl"', reason: MANIFEST_GLOB }, - { file: 'investigate/SKILL.md.tmpl', match: 'glob: "~/.gstack/analytics/eureka.jsonl"', reason: MANIFEST_GLOB }, { file: 'land-and-deploy/sections/readiness-gate.md.tmpl', match: 'eval store (`~/.gstack/projects//evals/`', reason: readinessEvalReason }, { file: 'land-and-deploy/sections/readiness-gate.md.tmpl', match: 'EVAL_DIR=~/.gstack/projects/$SLUG/evals', reason: readinessEvalReason }, - { file: 'office-hours/SKILL.md.tmpl', match: 'glob: "~/.gstack/projects/{repo_slug}/*-design-*.md"', reason: MANIFEST_GLOB }, - { file: 'office-hours/SKILL.md.tmpl', match: 'glob: "~/.gstack/analytics/eureka.jsonl"', reason: MANIFEST_GLOB }, { file: 'office-hours/SKILL.md.tmpl', match: "see each other's design docs in `~/.gstack/projects/`", reason: DESCRIPTION }, { file: 'office-hours/sections/design-and-handoff.md.tmpl', match: 'The design doc at `~/.gstack/projects/` is automatically discoverable', reason: DESCRIPTION }, - { file: 'plan-ceo-review/SKILL.md.tmpl', match: 'glob: "~/.gstack/projects/{repo_slug}/*-design-*.md"', reason: MANIFEST_GLOB }, { 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: 'retro/SKILL.md.tmpl', match: 'glob: "~/.gstack/projects/{repo_slug}/timeline.jsonl"', reason: MANIFEST_GLOB }, - { file: 'retro/SKILL.md.tmpl', match: 'glob: "~/.gstack/projects/{repo_slug}/learnings.jsonl"', reason: MANIFEST_GLOB }, { 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 }, ];