garrytan / garrytan/gstack

gbrain context-load: list-query filter: blocks in SKILL.md manifests are silently dropped, causing unfiltered cross-repo queries at skill startup

Open
#1,687 2 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.