Skip to content

feat(prompt): capability-gated assembly + the session transcript line (#421, #410) - #438

Merged
justrach merged 1 commit into
mainfrom
feat/prime-410-prompt
Aug 6, 2026
Merged

feat(prompt): capability-gated assembly + the session transcript line (#421, #410)#438
justrach merged 1 commit into
mainfrom
feat/prime-410-prompt

Conversation

@justrach

@justrach justrach commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Implements #421 and #410 together (both are buildSystemPrompt work), from the prime-agent adoption batch.

#421 — capability-conditional assembly

The root prompt is now 16 capability-scoped segments (prompt_text.zig), composed by gates that read the same predicates dispatch uses to refuse a hallucinated call (no_local_tools.blocks + tool_gates.blocks) — the prompt structurally cannot disagree with the tool catalog the provider was sent. At full capability the compose returns the comptime constant itself: zero allocation, byte-identical to what shipped.

Savings: --no-local-tools embedder sessions drop 28.5% of the base prompt (~482 tokens/turn); the floor config drops 43.2%. Full trim table with per-segment justification in the commit. Deliberately NOT trimmed: the destructive-git rail (an embedder still reaches a sandbox where git may run). The matrix test earned its keep immediately — it caught tool-name examples leaking through an otherwise-gated note, which got split so the instruction survives and only its illustration goes. Both prime doctrine lines adopted into the always-on intro.

#410 — with a premise correction

The issue said JSONL and assumed the transcript preserves what compaction discards. Neither is true in graff: .graff/sessions/<name>.session.json is one JSON object whose messages array is rewritten in place after compaction. The line shipped says what is TRUE (single object, lags the live turn, resume artifact not an archive), and a test asserts the string "JSONL" is absent — a wrong provenance claim would cost the model turns. Composes inside the #326 setSystemPrompts funnel so persona swaps can't drop it; suppressed for subagents and --no-local-tools.

Experiment

prompt_snapshot_tests.zig: a full-capability inline golden of the entire prompt (drift becomes a conscious choice), a capability matrix asserting absent capabilities contribute zero bytes (checked two ways, failing segment named), zero-alloc fast-path check, transcript-line exactness, and verified on the wire against a live mock request.

Gates

Tests 994 → 1003 (+9 exactly); all 9 golden eval files PASS — with the reason no eval moved documented (the request estimator floors above the prompt-size term); fmt clean; no **; all files ≤600 (startup.zig untouched at 600). examples/prepare_graff_tournament.py updated — its old prompt scrape was approximate, the new per-segment read reproduces the shipped prompt exactly.

Follow-ups flagged, not taken

  • sub_system_prompt has the same residue (subagents under --no-local-tools still see gated tool examples) — needs a runtime compose at spawn sites.
  • The trace paragraph tells the model to use /trace, a user-only REPL command — Capability-conditional system prompt assembly #421's failure class but a wording fix; left to avoid churning the golden twice.

…ranscript line (#421, #410)

## What changed

The root system prompt is no longer one frozen string. `src/prompt_text.zig`
holds it as 16 capability-scoped segments; `prompts.zig` owns the `segments`
table that gives each one a `Gate`, the comptime `main_system_prompt` those
segments still concatenate to (an `Agent` struct default, so it has to stay
comptime), and `composeBase()`, which drops every segment whose capability this
process does not have. `startup.buildSystemPrompt` now starts from
`prompts.baseForSession(arena)` instead of the constant.

Gates are read from the two predicates dispatch already refuses a hallucinated
call with (`no_local_tools.blocks` + `tool_gates.blocks`), so the prompt cannot
disagree with the catalog. What the gates remove today:

- `--no-local-tools` (#330) drops the read_file/edit_file/write_file/codedb/bash
  paragraph, the `.graff/traces` paragraph (its instruction is "read and analyze
  it", and the trace is a host file), the `gh issue create` paragraph, and the
  batching note's three tool examples: 1930 of 6777 chars, 28.5%;
- `subagent`/`workflow` absent drops the fan-out paragraph (413 chars);
- `todo_write` absent drops both todo paragraphs (197);
- `note_constraint` absent drops the record-a-rejection paragraph (388).

Two prompt-doctrine lines from the prime-agent analysis are adopted where they
had a home: "never invent a tool, a parameter, or a wrapper API" joins the
always-on intro, and "run the target project through its OWN environment"
joins the verify-your-work paragraph.

#410 adds one line naming this session's durable transcript. It composes inside
`setSystemPrompts`, not at the call site, so a persona swap or a
`set_system_prompt` cannot drop it; `buildRootAgent` settles `session_name`
before the funnel runs.

`prompt_snapshot_tests.zig` pins all of it: the full-capability prompt against
an inline golden, and a capability matrix asserting that an absent capability
contributes ZERO text (exact length AND the dropped segment's bytes being
unfindable), plus the already-gated MCP/skill/optional-tool/goal paths.

## Why

- Problem/failure mode: every session paid for every instruction. An embedder
  running `--no-local-tools` was told to `read_file` before editing and to
  prefer `codedb` over `bash grep` for tools the provider was never told exist
   - tokens on every turn, and an invitation to call something that cannot work.
- Reason for this approach: the segment table is the single source of both the
  comptime constant and the runtime composition, so the two cannot drift or
  reorder; and `composeBase` returns the constant itself at full capability, so
  the common path allocates nothing and is byte-identical to what shipped.
- Constraints or trade-offs: the transcript line says JSON, not JSONL as the
  issue assumed - `.graff/sessions/<name>.session.json` is one object, the JSONL
  files are the traces - and it does not claim to hold what compaction
  discarded, because compaction rewrites the retained history in place. A wrong
  format or provenance claim costs the model a turn discovering otherwise.
- Rejected alternatives: gating the git/PR/commit guidance too. An embedder
  still reaches a sandbox where git may run, and "never discard the user's work"
  is the wrong instruction to make optional.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants