mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-12 19:44:48 +01:00
fix(state-root): gbrain context_queries globs use {gstack_state_root}/
bin/gstack-brain-context-load.ts resolves the {gstack_state_root}/ prefix
through gstack-paths (configuredStateGlob); the legacy ~ form only matched the
default root. The nine projects/analytics manifest globs in design-consultation,
design-shotgun, investigate, office-hours, plan-ceo-review and retro now use the
prefix, as does office-hours' builder-profile developer-profile.json glob
(plan-tune writes it under $GSTACK_STATE_ROOT).
Allowlist: drops the nine MANIFEST_GLOB rows and the now-unused MANIFEST_GLOB
and PENDING reasons; the header lists the remaining row kinds.
Tests: new brain-context-load case loads the shipped investigate and
office-hours queries against a configured root and proves a file only under
~/.gstack is not read.
This commit is contained in:
1 parent
3215981a2e
commit
44e9c1ed3a
8 files changed
+59
-24
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
|
||||
+2
-2
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/<slug>/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/<slug>/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/<slug>/question-log.jsonl`)', reason: DESCRIPTION },
|
||||
{ file: 'plan-tune/SKILL.md.tmpl', match: '`~/.gstack/projects/<slug>/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 },
|
||||
];
|
||||
|
||||
Reference in new issue
Block a user