gbrain context-load: list-query filter: blocks in SKILL.md manifests are silently dropped, causing unfiltered cross-repo queries at skill startup
- Dominant language
- TypeScript
- Stars
- 133k
- Forks
- 19.9k
- Avg merge
- 18h 46m
- Merged PRs (30d)
- 26
Description
## Summary
`parseSkillManifest` (the runtime read path in `lib/gstack-memory-helpers.ts`) parses every field of a `gbrain.context_queries` item **except** `filter:`. As a result, any `kind: list` query that declares a `filter:` block in its `SKILL.md` manifest runs **unfiltered** at skill startup, pulling pages of every type from every repo into the agent's context instead of the repo scoped set the manifest asked for.
This silently defeats the F7 design intent documented in `bin/gstack-brain-context-load.ts` (lines 18-20): "every default query carries an explicit `repo:` filter so cross-repo contamination is the non-default path."
## Affected skills
Four shipped skills declare a `filter:` block on a `kind: list` query in their generated `SKILL.md` (the file the runtime actually reads):
- `investigate/SKILL.md` (`prior-investigations`: `type: timeline`, `tags_contains: "repo:{repo_slug}"`, `content_contains: "investigate"`)
- `office-hours/SKILL.md`
- `plan-ceo-review/SKILL.md`
- `design-consultation/SKILL.md`
## Current behavior on upstream main
On `origin/main` (`920a13a1`):
```bash
bun -e '
import { parseSkillManifest } from "./lib/gstack-memory-helpers.ts";
const m = parseSkillManifest("investigate/SKILL.md");
const q = m.context_queries.find(q => q.id === "prior-investigations");
console.log("kind:", q.kind, "| sort:", q.sort, "| limit:", q.limit);
console.log("filter:", JSON.stringify(q.filter));
'
```
prints:
```
kind: list | sort: updated_at_desc | limit: 5
filter: undefined
```
The manifest declares:
```yaml
- id: prior-investigations
kind: list
filter:
type: timeline
tags_contains: "repo:{repo_slug}"
content_contains: "investigate"
sort: updated_at_desc
limit: 5
render_as: "## Prior investigations in this repo"
```
but `filter` comes back `undefined`. Downstream, `dispatchList` in `bin/gstack-brain-context-load.ts` only adds `--filter` args when `q.filter` is set:
```ts
if (q.filter) {
for (const [k, v] of Object.entries(q.filter)) {
const { resolved: rv } = substituteTemplateVars(String(v), args);
cliArgs.push("--filter", `${k}=${rv}`);
}
}
```
So the query that ships is `gbrain list_pages --limit 5 --sort updated_at_desc` with no filter, returning the 5 most recently updated pages of any type from any repo, then wrapping and injecting them into the agent's startup context.
## Root cause
`extractGbrainBlock` in `lib/gstack-memory-helpers.ts` (around lines 388-410) matches `id`, `kind`, `render_as`, `query`, `limit`, `glob`, `sort`, and `tail`, but never `filter`. The `GbrainManifestQuery.filter` field exists in the type, `defaultManifest` populates it, and `dispatchList` consumes it. Only the manifest parser fails to read it, so a declared filter is dropped on the floor.
This is worse than a skip: the query still "succeeds", so no `(unavailable)` is rendered. Wrong, cross-repo data is injected with no signal that anything went wrong.
## Expected behavior
`parseSkillManifest` should return `q.filter` populated from the `filter:` sub-block, so `dispatchList` emits the declared `--filter k=v` args and the query stays repo scoped, matching the behavior of the built in `defaultManifest` queries.
For the example above:
```
filter: {"type":"timeline","tags_contains":"repo:{repo_slug}","content_contains":"investigate"}
```
## Notes
The fixtures in `test/gstack-memory-helpers.test.ts` ("parses a multi-kind manifest", "ignores incomplete query items") contain no `filter:` block, so filter parsing is entirely uncovered by tests today.
Contributor guide
Assessment
This issue has not been assessed yet.