enhance(#4836): prefer the graphify CLI for planner and researcher graph queries (#4874)

* enhance(#4836): prefer the graphify CLI for planner and researcher graph queries

The planner gets one knowledge-graph query per phase and the researcher two
or three, and that single shot decides which modules the plan treats as
related — and therefore how tasks are ordered into waves. It was spent on
the built-in reader, which seeds by case-insensitive substring match over a
node's label and description and then expands a hardcoded two hops. The
phase "User Authentication" seeds on `author`, `authoring` and
`unauthorized` with the same weight as `authenticate`, and when the
inflated payload exceeds `--budget` the trimmer drops edges by confidence
tier — so the highest-confidence tier can be discarded to fit a payload
that bad seeding inflated in the first place.

The graphify CLI is already a hard dependency of /gsd-graphify build, and
it ranks seeds (IDF weighting, trigram fuzzy matching) and applies context
filters before traversal. Both prompts now prefer it and fall back to the
built-in reader, branching on `command -v graphify` — the same degradation
shape the repo already uses for Context7 to ctx7. Binary presence is a
self-satisfying gate: a graph can only exist if the binary built it, so the
fallback covers edge cases (a CI checkout with a committed graph, a binary
since removed), not the common path. No new config key and no new tool
grant — both agents already have Bash.

The planner additionally runs `graphify affected`. The reference states its
own goal as "which subsystems may be affected by changes in this phase",
which is literally reverse traversal by relation; the built-in reader only
approximates it with undirected two-hop expansion and has no equivalent
verb, so `affected` is skipped on the fallback path.

`graphify status` now reports `graph_path`, the resolved absolute graph
location, on both the present and the missing branch. The CLI takes the
graph location as `--graph`, and the prompts must not re-derive
`.planning/graphs/graph.json` for it: that would point the CLI at a
non-existent local mirror in exactly the umbrella multi-repo setup
`graphify.graph_path` (#1825) exists to serve. For the same reason the
presence gate in both prompts is now the `status` call itself rather than a
bare `ls` of the default location, which was already blind to the override.

Known limit, stated in both prompts rather than implied: the two paths
return different shapes. `graphify query` emits prose and has no `--json`
flag; the built-in emits JSON with per-edge confidence tiers and
budget_met/budget_estimate. `--budget` also counts rendered output on one
and estimated payload bytes on the other (#2738) — same flag name,
different unit. Both are read by a model and nothing machine-parses the
injected block. With graphify absent from PATH the injected context is
byte-identical to before.

Closes #4836

Emitted-Drift-Ack-Growth: gsd-phase-researcher.md — the CLI-first branch, the reason it is preferred, and the output-shape warning are the deliverable; a pointer to a part would not be read at the decision point.
Emitted-Drift-Ack-Growth: gsd-planner.md — one sentence in the load_graph_context step pointer, so it stops naming the default graph path the reference no longer assumes.

* docs(#4836): record the CLI-first graph query in the planner and researcher entries

* chore(#4836): add changeset fragment

* enhance(#4836): name the full domain word in the planner's query-term examples

The reference's own example — phase "User Authentication" → term "auth" — is
the exact collision the CLI-first path exists to avoid, and it stays a
collision whenever the fallback path runs, since that path matches the term as
a substring of label and description.

* fix(#4836): surface graph_path on the unparseable-graph status branch

graphifyStatus() returned graph_path on the exists:true and exists:false
outcomes but not on the third, error, outcome (graph.json present but
unparseable). The planner/researcher prompts gate CLI-first dispatch on
exists, not on this outcome, so a corrupt graph file made them fall
through to the CLI-first branch with the literal <graph> placeholder and
no real path to substitute.

* docs(#4836): note graph_path's trust boundary at the --graph interpolation

graph_path is reflected verbatim into a double-quoted --graph argument the
agent executes via Bash. It comes from graphify.graph_path, a config
surface already trusted elsewhere, so this isn't a new trust boundary --
but it is a new injection site (no --graph flag existed on this call
before). One-line caution for anyone hardening this later.

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
Michel Moreira
2026-09-22 20:59:45 -03:00
committed by GitHub
parent 585cab41cc
commit 76ef60ba25
12 changed files with 283 additions and 29 deletions

View File

@@ -566,26 +566,34 @@ if [ -e "${_CTX[0]}" ]; then cat "${_CTX[@]}"; fi
## Step 1.3: Load Graph Context
Check for knowledge graph:
```bash
ls .planning/graphs/graph.json 2>/dev/null
```
If graph.json exists, check freshness:
Check for a knowledge graph and read its freshness in one call. `status` resolves the
graph through `graphify.graph_path`, so it is also the presence gate — a bare `ls` of
the default location misses an umbrella graph shared across sibling repos:
```bash
gsd_run graphify status
```
If `exists` is `false`, continue to Step 1.5 without graph context.
If the status response has `stale: true`, note for later: "Graph is {age_hours}h old -- treat semantic relationships as approximate." Include this annotation inline with any graph context injected below.
Query the graph for each major capability in the phase scope (2-3 queries per D-05, discovery-focused):
The same response carries `graph_path` — the resolved graph location. Substitute it for `<graph>` below. `graph_path` comes from `graphify.graph_path` in `.planning/config.json`, a config surface already trusted elsewhere; if it ever carried attacker-controlled content, the literal double-quoted substitution below would need escaping.
Query the graph for each major capability in the phase scope (2-3 queries per D-05, discovery-focused). Prefer the `graphify` CLI when it is on PATH; fall back to the built-in reader otherwise:
```bash
gsd_run graphify query "<capability-keyword>" --budget 1500
if command -v graphify >/dev/null 2>&1; then
graphify query "<capability-keyword>" --graph "<graph>" --budget 1500
else
gsd_run graphify query "<capability-keyword>" --budget 1500
fi
```
Why the CLI is preferred: it ranks seeds (IDF weighting, fuzzy matching) and applies context filters before traversal, where the built-in reader seeds by case-insensitive substring over label and description — so a term like "auth" seeds equally on `author` and `authorize` — and then expands a fixed two hops.
The two paths return **different shapes**: the CLI emits prose, the built-in emits JSON with per-edge confidence tiers and `budget_met`/`budget_estimate`. `--budget` caps rendered output on the CLI and estimated payload bytes in the built-in — same flag name, different unit. Read whichever you get; do not assume a stable shape.
Derive query terms from the phase goal and requirement descriptions. Examples:
- Phase "user authentication and session management" -> query "authentication", "session", "token"
- Phase "payment integration" -> query "payment", "billing"
@@ -597,7 +605,7 @@ Use graph results to:
- Surface dependencies the phase description does not explicitly mention
- Inform which subsystems to investigate more deeply in subsequent research steps
If no results or graph.json absent, continue to Step 1.5 without graph context.
If nothing comes back, continue to Step 1.5 without graph context.
## Step 1.5: Architectural Responsibility Mapping

View File

@@ -620,9 +620,10 @@ If exists, load relevant documents by phase type:
<step name="load_graph_context">
Read `gsd-core/references/planner-load-graph-context.md` and execute it. It checks for a
knowledge graph and, if `.planning/graphs/graph.json` exists, reads freshness and
phase-relevant dependency context via the `gsd_run` launcher and incorporates the results
into planning. If the graph is absent, skip and continue without graph context.
knowledge graph and, if one exists, reads freshness and phase-relevant dependency context
— through the `graphify` CLI when it is on PATH, otherwise through the `gsd_run` launcher
— and incorporates the results into planning. If the graph is absent, skip and continue
without graph context.
</step>
<step name="identify_phase">