feat: ship harvested graph context and result augmentation - #4
Merged
Conversation
CBM's tools were reachable from OMP but nothing told a session the graph existed, delegated work started blank, and a `grep` ran without the structural answer the graph already held. A harvest pipeline derives the shipped skill, always-apply rule, and three parent-handoff agents from a CBM executable instead of hand-copying them, records the version it derived from in `harvest.json`, and fails the build on a hand edit. It refuses to run while a CBM daemon is active, because `install` drains active sessions before configuring. CI regenerates against the newest release and diffs, on pushes and weekly -- a new upstream release produces no activity here, so a push-only gate could not see it. A `tool_result` handler appends matching graph symbols to `grep` and `glob` results and index-coverage gaps to `read` results. It is a manifest feature with its own entry, `dist/augment.js`, because feature gating applies to manifest-declared entries and an extension cannot ask which of its own features an operator selected. `tool_call` stays unregistered: a throwing handler there is a refusal of the tool call. The graph is queried over one long-lived stdio MCP session rather than a `cli` subprocess per call. Measured against v0.10.8, every `cli` subcommand costs a fixed ~2.86 s before its first byte -- identical on the refusal path that does no work at all -- while `tools/call` on an initialized stdio session answers in 14 ms. A per-call subprocess under a deadline in the low hundreds of milliseconds could never have produced an answer. `/cbm status` additionally reports which indexed project covers the working directory. Nothing here indexes, deletes, or overrides CBM's cache.
End-to-end verification in a real OMP session found two defects that the unit suite could not see, and a third the design had reasoned about wrongly. `search_graph` answers a keyword search flat -- `cols` of `qn, label, file, lines, rank` with one top-level `rows` list -- and a pattern search grouped, with `groups` carrying `qn_prefix` and `file` and rows of `name, label, lines, in, out`. The reader handled only the grouped shape, so every `grep` appended nothing while every test passed: the fixtures held one shape. Both are read now, columns by declared name, and both are fixtured. `file_pattern` is a LIKE match, not a regex. Measured: `src` matches 276 nodes, `src/*` matches 275, `src/.*` matches none. The glob was translated to a regex and therefore selected nothing for every `glob`. It is translated to LIKE wildcards now, with consecutive ones collapsed so a globstar still matches the top level. The client cannot be opened lazily on the first search. The handshake is 2.9 s against a warm CBM daemon and 8.2-8.6 s when the daemon has to start, and a query refuses to wait for it -- so every search until it landed appended nothing, which for a short session is every search. It is opened on a zero-delay managed timer at session start instead, so the handshake overlaps the model's first turn, and the outcome is recorded once per session in the debug log rather than left invisible. The project resolution also cached a "graph did not answer" result, so one search arriving during the handshake disabled augmentation for the rest of the session. Only a definitive resolution is cached now. Transcripts for tasks 8.1 through 8.7 are in the change's evidence directory, with a summary of what each one shows.
Enabling a language server changed the answer to "what is the graph worth here", and the measurements overturned two decisions in this change. Asked where `resolveExecutable` is used, `lsp references` answered 19 exact references with no false positives, `grep` answered 16 hits needing manual classification into 10 calls, 4 imports, a comment and the definition, and `trace_path` answered 15 callers -- the same 10 at hop 1 plus 5 transitive ones neither of the others can produce in one call. The graph's edge is transitive reach and degree, not locating a definition. The append was doing neither. It listed the qualified name and the file and line range of symbols the grep had already shown, at +27-55% of the result's size, and `search_graph` was returning degree in columns this code discarded. Worse, the keyword mode it used answers with rows the search never matched: `query: "resolve"` returns 14 rows including `managedCopy`, `pathOption`, `OrderCase` and `Layout`, because BM25 indexes docstrings and file names -- under a heading claiming they matched the grep. That mode also substitutes a `rank` column for `in`/`out` and `fields` does not restore them, so it cannot supply degree at all. The `grep` path now sends an unanchored `name_pattern` built from the identifiers the pattern holds, which is the same substring test the operator's `grep` applied to lines. Every row carries the graph's selected degree, labelled as degree rather than as callers because CBM computes it over CALLS, USAGE, CALL_REFERENCE, INHERITS and IMPLEMENTS and directs callers to `trace_path`. Container nodes are filtered, since the keyword mode had been filtering them upstream, and rows are ordered by in-degree because this mode does not rank. The rule stops being always-apply. Its central instruction -- always prefer graph tools over grep/glob/file-search -- is false where a language server exists, and it never mentions `lsp`, so a model obeying it every turn is steered away from the better tool. It also duplicates the MCP entry: CBM's `initialize` returns 808 bytes of `instructions` that OMP injects once per session, in wording CBM calibrated more carefully. Shipping 2988 bytes per turn to restate that more absolutely than CBM does is not a trade worth making. The rule now ships with a `description` and no `alwaysApply`, so it is listed in the rulebook and read on demand through `rule://codebase-memory`, and a hand edit reinstating `alwaysApply: true` fails the build gate. Separately, `test/unit/graph.test.ts` was flaky under CPU contention, and had been before this change. Every test there writes a new fake server and runs it through its shebang; that first execution costs ~340 ms idle and seconds under load, against Bun's 5 s default budget, so `warm()` timed out. The spawning tests now carry an explicit 30 s budget, verified by saturating the machine with twice as many spinning processes as cores. Transcripts for the new checks are in the change's evidence directory.
Verification against e1c1bf3 returned two blockers and twelve majors that a 364/364 green suite had not caught. Three rounds closed them, and every finding was confirmed by a worker that had not written its fix. `/cbm status` could never report the index. The probe inherited the augmentation's 300 ms per-query deadline and raced it against a ~2.9 s MCP handshake, then killed in its `finally` the handshake it had just started, so every invocation answered `unavailable` in ~305 ms and two lifecycle scenarios were unreachable. Every status test injected a stub probe, which is why it shipped. The probe is now exported, waits for readiness, and is bounded by one wall-clock budget of 10 s. Charging each step its own deadline was rejected on measurement: three sequential steps at 10 s apiece sums to 30 s, worse than the 20 s freeze being fixed. A query that missed its deadline used to tear the session down without clearing the handshake, so `ready()` reported a dead session as ready and every later query answered null for the rest of it -- the poisoning the project resolver already avoids one layer up, relocated below it where its retry cannot see it. An established session now reopens at most twice, and teardown is scoped to its owning child, because an old drain's EOF was killing the replacement mid-handshake. The append bound covered only the rows. A 9,000-byte server-supplied caveat therefore produced a 9,053-byte block carrying no rows at all, dropping the coverage reason a partially covered read must report, and the symbol heading claimed a count it did not list. The block now bounds its frame and settles the count and the rows together. The `--clients` vocabulary probe ran CBM's `install` against the operator's real HOME and cache root, and ran it before the daemon guard -- the guard that exists because `install` drains every CBM session on the machine. It now creates its own scratch HOME, and the refusal is decided before it. Five context-harvest scenarios rested on code no test could reach, because the pure decision logic sat inside two modules the isolation gate bans by path; it moves to `src/harvest/guards.ts`. That gate read no helper outside `test/support` and skipped `.mjs` and `.cjs`, so a helper beside its own test reopened every banned mechanism while the gate stayed green. Both spellings now derive from one list. The suite grows from 364 to 419 tests and from 739 to 877 assertions. The growth is the point: at entry it was green and every one of the 45 findings survived it.
`README.md` told the operator the shipped rule is injected into every turn, so a fresh session and a post-compaction turn both carry it. The `alwaysApply` reversal made that false, and it contradicted the marketplace section 55 lines earlier, which already described the rule as a rulebook entry rather than an always-apply injection. An operator reading top to bottom got both positions. The same section miscounted the shipped surfaces -- a skill, a rule and three agents are three surfaces across five files, not four of anything -- and the `harvest.json` block was presented as the whole file while omitting `generated`. `CLAUDE.md` still described a single committed bundle. Two are committed, `dist/index.js` and `dist/augment.js`, and CI reads the bundle list from the manifest and gates both. Its own rule is that a change altering a normative decision updates the affected guidance in the same change, and this is the guidance that was missed.
Task 8.7 needed an install carrying `enabledFeatures: []`, and the plugin root that carried it was created under the repository's own `.omp/` -- visible in `8.7-feature-declined.json`, where the skill resolves from `<repo>/.omp/plugins/node_modules/omp-codebase-memory/skills/...`. That directory is not scratch: `.omp/` is the operator's project-local OMP directory, holding their `config.yml` and their project-local skills root, and a global ignore keeps it out of `git status`. Cleaning up the scratch plugin root removed `.omp/` wholesale, and the loss surfaced a session later as a skill registry that advertised roughly sixty skills while resolving five. The package itself cannot reach `.omp/`. Every destructive call in `src/` and `scripts/` is a scratch directory it created with `mkdtemp`, one of its own staging files, the MCP entry it wrote, `packageRoot(host)` on `uninstall`, or the harvest's own generated paths at the repository root. This was the verification procedure, so the boundary belongs where the procedure is written down. The removal command itself is not recorded: it ran in the orchestrating session rather than inside a recorded `omp -p` transcript. What the evidence proves is that the plugin root was inside `.omp/` during 8.7 and that `.omp/` was absent at the start of the next session; the evidence README states both and says which is inference.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
binary-lifecycle-and-mcp-wiringmade CBM's 15 graph tools reachable from OMP. Reachable is not used: nothing tells a session the graph exists, nothing carries that knowledge across a fresh session or a compaction, delegated work starts blank, and agrepstill runs without the structural answer the graph already holds. CBM solves all four for its own client surfaces with a skill, a durable instruction file, three tiered agent definitions, and non-blocking context hooks. None of that reached OMP.The content for the first three already exists inside the CBM executable and can be obtained from it rather than rewritten. Verified against v0.10.8:
install --skip-binary --clients=claude,augmentagainst a scratchHOMEemits the canonical skill (5137 B), the canonical instruction body (2862 B), and three agent definitions in the parent-handoff shape (933–969 B) that OMP's agent contract accepts unchanged apart from frontmatter. So this is mostly a derivation pipeline plus one small runtime handler — and the pipeline is what keeps the shipped copies from drifting away from the executable an operator actually runs.What Changes
installagainst a scratchHOME, collect, transform, record the version. CI regenerates and fails on any difference, so drift is a named test failure rather than silent staleness.skills/codebase-memory/SKILL.md, the harvested body verbatim under thedescriptionfrontmatter OMP's providers require.rules/codebase-memory.md, the harvested instruction body under adescriptionand deliberately noalwaysApply, so it is listed by name and read on demand throughrule://.agents/codebase-memory{,-scout,-auditor}.mdin the parent-handoff shape, declaring native tools as a plain CSV exactly as OMP's own bundled read-only agents do, and naming no MCP tool.tool_resultforgrep,globandread— matching symbols on a search, index-coverage gaps on a read. An optional manifest feature, so it can be declined at install and toggled afterwards. Never ontool_call: a throwing or blocking handler there is a refusal of the operator's call.A route-dependent limitation, not a breaking change. A marketplace-installed copy is discovered through a provider that contributes skills and agents but is not a rules provider, so the rule is not discovered on that route. The git spec stays the documented primary install and the README says so plainly.
Two decisions reversed on measurement
Both are recorded in the design with the numbers behind them.
clisubprocess per query was withdrawn: everyclisubcommand costs a fixed ~2.86 s before its first byte, identical on a refusal path that does no work. Queries now go over one persistent stdio MCP session, wheretools/callanswers in ~14 ms.grep/glob/file-search — is false where a language server exists, and it never mentionslsp. It also restated the 808 bytes ofinstructionsCBM's own MCP entry already delivers per session. It ships rulebook-only, and a hand edit reinstatingalwaysApply: truefails the build gate.Verification
rasen-verify-changereturnedBlocker:2 Major:12 Minor:24 Trivial:7against a suite that was 364/364 green — every finding survived it.rasen-review-cycleclosed both blockers and all twelve majors over three rounds, each finding confirmed by a worker that had not written its fix.The two blockers:
/cbm statuscould never report the index (the probe inherited the augmentation's 300 ms deadline and raced a ~2.9 s handshake, then killed the handshake it started), andrasen validatewas failing because the late reversal moved every scenario off one requirement.Gates on the merged tree:
tsc --noEmitclean, both committed bundles byte-identical to a fresh build,bun test419 pass / 0 fail (877→891 assertions, up from 364/739 at entry),rasen validatevalid.Review material from the planning tree (not in this diff)
The planning artifacts live in a separate Git repository at
rasen/, which the outer repository ignores, so this PR's diff carries no proposal, delta spec, or verification report. They are atrasen/changes/graph-context-and-agents/:proposal.mddesign.mdspecs/context-harvest/spec.mdHOME, the--clientsvocabulary check, the daemon guard, the transforms, the CI drift gatespecs/durable-context/spec.mdspecs/graph-augmentation/spec.mdtool_resulthandler: events, project resolution, the symbol and coverage appends, bounds and fail-open, feature gating, drift reportingspecs/binary-lifecycle/spec.mdevidence/verification-report.mdVERIFY VERDICT:lineevidence/review-cycle-report.mdevidence/*.jsonNew capabilities:
context-harvest,durable-context,graph-augmentation. No capability is modified beyond the onebinary-lifecyclerequirement above.Planning tree:
rasen/changes/graph-context-and-agents@de85ac0076c3aadc4eb3a79960d89f209062b086(tree dirty at ship time — an unrelated new change directory,changes/operator-documentation/, is untracked there; nothing belonging to this change is uncommitted).Accepted-known at ship time (no Blocker, no Major)
durable-context's marketplace-route scenario is verified nowhere and cannot be exercised from inside this repository. Not weakened, not falsely evidenced; recorded as a known open verification item.!!bool truebypasses thealwaysApplyguard. Upstream of the guard — the frontmatter reader stores the raw remainder of the line — and documented at the guard rather than papered over.installvalidates an unknown--clientstoken before draining sessions is not established. Determining it requires draining the developer machine's active daemon. Unknown options are rejected at parse time with no drain (measured).test/unit/acquire.test.tsflaked 6/25 once, mid-cycle, under a load spike with roughly ten concurrent agents each spawning 282 MB CBM binaries. Untouched by this change; passes alone and in every subsequent full run. Not fixed: raising a production timeout to accommodate a test-only load spike is the wrong direction.