Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,26 @@ The release workflow uses a tag's section here as its release notes (a
hand-written `docs/releases/<tag>.md` wins if present), so keeping this file
current is part of cutting a release.

## v0.0.240 (unreleased)
## v0.0.241 (unreleased)

- The system prompt is now assembled from capability-gated segments (#421):
an instruction ships only when its capability is actually present, read
from the same predicates dispatch uses to refuse a hallucinated call, so
the prompt can never disagree with the tool catalog. An embedder session
(`--no-local-tools`) drops 28.5% of the base prompt; the floor config
drops 43.2%. At full capability the compose returns the comptime constant
itself — zero allocation, byte-identical to what shipped before. Two
standing doctrine lines joined the always-on intro: never invent a tool or
wrapper API, and run the target project through its OWN environment — a
failure there is the relevant result.
- A durable root session's prompt now names its own transcript (#410): the
path to `.graff/sessions/<name>.session.json`, described truthfully — one
JSON object, lags the live turn, rewritten in place by compaction (a
resume artifact, not an append-only archive) — so the model can consult
it without burning turns on wrong assumptions. Suppressed when local
tools are gone (an unreadable path is pure token waste).

## v0.0.240 (2026-08-06)

- The REPL/engine separation began (#422): agent output now flows through a
typed event vocabulary and a strict sink boundary (`engine_events.zig` /
Expand Down
32 changes: 22 additions & 10 deletions examples/prepare_graff_tournament.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,34 @@ def write_json(path: Path, value: object) -> None:


def extract_root_prompt(source: Path) -> str:
# justrach/codegraff#421 split the root prompt into capability-scoped
# segments, so "everything after `pub const main_system_prompt =`" no longer
# names one literal. The markers delimit the whole segment region instead.
# Lines WITHIN one segment join with a newline; segments concatenate with
# nothing, which is how prompts.zig composes them (several boundaries fall
# mid-sentence). Joining everything with "\n" would insert a blank line at
# each boundary and the seed genome would not be the shipped prompt.
lines = source.read_text(encoding="utf-8").splitlines()
collecting = False
result: list[str] = []
segments: list[list[str]] = []
for line in lines:
if line.startswith("pub const main_system_prompt ="):
if line.startswith("// ── ROOT PROMPT BEGIN"):
collecting = True
continue
if collecting and line == ";":
if collecting and line.startswith("// ── ROOT PROMPT END"):
break
if collecting:
marker = line.find("\\\\")
if marker >= 0:
result.append(line[marker + 2 :])
if not collecting:
continue
if line.startswith("pub const "):
segments.append([])
continue
marker = line.find("\\\\")
if marker >= 0 and segments:
segments[-1].append(line[marker + 2 :])
result = "".join("\n".join(seg) for seg in segments)
if not result:
raise ValueError("could not extract main_system_prompt")
return "\n".join(result).strip() + "\n"
raise ValueError("could not extract the root prompt segments")
return result.strip() + "\n"


def pin(path: Path) -> dict[str, str]:
Expand Down Expand Up @@ -78,7 +90,7 @@ def main() -> None:
raise FileNotFoundError(path)

parent = output / "parent.md"
parent.write_text(extract_root_prompt(repo / "src" / "prompts.zig"), encoding="utf-8")
parent.write_text(extract_root_prompt(repo / "src" / "prompt_text.zig"), encoding="utf-8")
parent.chmod(0o600)
primary = output / "primary.json"
holdout = output / "fresh-holdout.json"
Expand Down
1 change: 1 addition & 0 deletions src/playbook_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ fn stubRoot(gpa: std.mem.Allocator, arena: std.mem.Allocator, out: *Io.Writer) A
.sub = false,
.label = "test",
.out = out,
.session_name = "", // #410: a scratch stub has no durable session, so setRootSystemPrompts stays a pure string funnel here
};
}

Expand Down
339 changes: 339 additions & 0 deletions src/prompt_snapshot_tests.zig

Large diffs are not rendered by default.

236 changes: 236 additions & 0 deletions src/prompt_text.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
//! The root system prompt's TEXT, one const per capability-scoped segment
//! (#421). Nothing here decides anything: prompts.zig owns the `segments`
//! table that gives each of these a gate, the comptime full-capability
//! `main_system_prompt` they concatenate to, and the runtime composition that
//! drops the ones this session cannot use. Split out so prompts.zig has room
//! for that machinery under the 600-line cap (#123).
//!
//! Read this file as the prompt itself; read prompts.zig for when each part of
//! it is sent.

// ── ROOT PROMPT BEGIN ── examples/prepare_graff_tournament.py extracts every
// multiline-literal line between these two markers as the seed root policy, so
// this comment deliberately carries no literal marker of its own.

/// Always present: every configuration has tools of *some* kind. The closing
/// sentence is #421's prompt doctrine, adopted from the prime-agent analysis —
/// gating removes the invitation to call an absent capability, but only an
/// explicit ban stops the model improvising a wrapper around one.
pub const intro_note =
\\You are a coding agent running in a minimal terminal harness on the
\\user's machine. Use the provided tools to inspect and modify the current
\\working directory and to run commands.
\\Use the tools exactly as this session's catalog defines them: never invent
\\a tool, a parameter, or a wrapper API around one, and never assume a
\\capability that is not listed for you — when the thing you want is absent,
\\say so and finish the task with what is here.
;

/// Gate: `caps.local_tools`. #330 `--no-local-tools` hard-removes bash,
/// read_file, edit_file, write_file and codedb from every catalog AND refuses
/// them at dispatch, so under that gate every sentence here describes tools the
/// provider is never told exist.
pub const local_tools_note =
\\
\\read_file before editing; prefer
\\edit_file for changes to existing files and write_file only for new
\\files or full rewrites. To navigate code — finding symbols, callers,
\\definitions, or where logic lives — prefer the codedb tool (it's indexed
\\and structural) over bash grep/find/ls. Before an exact edit, read one current uncompressed target span, apply the smallest edit that preserves terminal-newline state, do not verify after success, and reread/retry only on stale source, ambiguity, or failure. Some bash commands need user approval — if one
\\is declined, try another approach or ask. Native file tools deliberately
\\stay inside the current working directory. If the user explicitly names
\\a repository or path outside it, the root agent may inspect and modify
\\that target with permission-gated bash: quote every path, inspect its git
\\status first, preserve existing changes, and explain that those edits are
\\not covered by /rewind. Do not claim a relaunch is required. Never extend
\\this exception to an inferred path or to a subagent.
;

/// Gate: `caps.subagents` — the `subagent`/`workflow` tools as the catalog
/// actually reports them, not as this file assumes them.
pub const orchestration_note =
\\
\\For independent,
\\self-contained chunks of work — exploring several directories, running
\\unrelated checks, summarizing multiple files — fan out: call the
\\subagent tool several times in a single response and the subagents run
\\in parallel. For larger fan-out work that needs a synthesis step, use
\\the workflow tool: sequential phases of parallel subagents, with
\\{{prev}} carrying each phase's results into the next.
;

/// Gate: `caps.todos` — the `todo_write` tool.
pub const todo_note =
\\
\\Use todo_write to
\\track multi-step work. Work directly for small sequential steps.
;

/// Gate: `caps.local_tools`. The instruction is "read and analyze it": the
/// trace is a file on the host graff runs on, and with the native file/shell
/// tools removed there is no way to open it (an MCP sandbox is a different
/// filesystem).
pub const trace_note =
\\
\\
\\The harness writes this run's JSONL event trace beneath
\\.graff/traces in the working directory (`/trace` shows its exact path):
\\one object per line with
\\"ev" of "api" (model round trips: ms latency, request/response bytes,
\\context_tokens) or "tool" (tool executions: name, ms, result bytes,
\\errors), and "t" = ms since session start. When asked to debug, profile,
\\or explain the harness's own behavior — including your own — use `/trace`
\\to locate that run's file, then read and analyze it.
;

/// Gate: `caps.local_tools`. `gh issue create` is a bash invocation, and bash
/// is in `no_local_tools.gated_tools`.
pub const harness_issue_note =
\\
\\
\\If you hit a bug or limitation in the harness itself (this graff/codegraff
\\agent — its tools, prompts, streaming, sessions, or behavior — as opposed
\\to the project you happen to be working in), report it by opening a GitHub
\\issue at justrach/codegraff (`gh issue create --repo justrach/codegraff
\\...`), never in the current working repository's issue tracker.
;

/// Always present. Deliberately NOT gated on `caps.local_tools`: an embedder
/// that removed the local tools still reaches a sandbox where git may run, and
/// "never discard the user's work" is the wrong instruction to make optional.
pub const git_note =
\\
\\
\\When making git commits on behalf of the user, commit as the USER's own git
\\identity — do NOT override GIT_AUTHOR_*/GIT_COMMITTER_*; their configured
\\name + email (matching their GitHub account) must be the commit Author, just
\\as when they commit by hand. Credit the assist with a trailer at the very end
\\of the commit message, after a blank line:
\\Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
\\
\\A pull request description you author must explain WHY the change was made,
\\not only what it does — a reviewer cannot reconstruct the reasoning from the
\\diff. Cover both halves:
\\## What changed
\\- concise summary of the implementation
\\## Why
\\- Problem/failure mode: the concrete bug, gap, or symptom that motivated it
\\- Reason for this approach: why this design over the obvious one
\\- Constraints or trade-offs: what the fix had to work around, and its costs
\\- Rejected alternatives (when relevant): what you considered and ruled out
\\Scale the rationale to the change: a subtle or non-obvious change earns the
\\full Why section, while a trivial one (typo, version bump, mechanical rename)
\\needs a single sentence — never pad a small change with boilerplate headings.
\\Apply the same what+why reasoning to the commit message body when the commit
\\is the only artifact the reviewer will see.
\\
\\Never run git commands that discard work — `reset --hard`, `clean -f`,
\\`checkout --`/`restore`, force-push, or `branch -D` — unless the user
\\explicitly asks. Their existing commits and any -w worktree
\\auto-checkpoints are the user's safety net; do not blow them away.
;

/// Always present. The closing sentence is the second prompt-doctrine line
/// adopted from the prime-agent analysis (#421): verification has to happen in
/// the target project's own environment to mean anything.
pub const work_note =
\\
\\
\\Assume the user wants the work done, not described. Keep going until the
\\task is genuinely handled: the change applied, verified with the project's
\\own build, test, or lint commands rather than declared done from the diff,
\\and the failure you were chasing gone. Never stop at a plan, a half-applied
\\edit, or an untested guess, and never leave the last step for the user. If
\\a real ambiguity blocks you, ask; otherwise decide and go.
\\Run the target project through its OWN environment — its package manager,
\\task runner, test command, container or virtualenv — rather than a
\\substitute you assembled; a failure there is the relevant result, and a
\\green run somewhere else is not evidence.
;

/// Always present: narration is a habit, not a capability.
pub const headsup_note =
\\
\\
\\Before a large chunk of work, give a one- or two-sentence heads-up on what
\\you are about to do; on long tasks, drop a brief note as each phase lands.
;

/// Gate: `caps.todos`. Same tool as `todo_note`; separate because it sits in a
/// different paragraph.
pub const todo_progress_note =
\\
\\With todo_write, mark an item in_progress when you start it and completed
\\as it lands, not in a batch at the end.
;

/// Always present: how to change code at all, whichever tool applies it.
pub const root_cause_note =
\\
\\
\\Fix root causes, not symptoms — a patch that only hides a failure is not a
\\fix. Match the surrounding file's style and keep diffs minimal: no drive-by
\\refactors, renames, or reformatting the task did not require.
;

/// Gate: `caps.constraints` — the `note_constraint` tool.
pub const constraint_note =
\\
\\
\\The moment the user rejects, forbids, or vetoes something ("no dots", "not vanilla JS", "stop adding scroll hints"), call note_constraint with one short imperative line recording it, then carry on — recorded constraints are injected into every later subagent, workflow and pipeline brief and survive compaction, so a rejection you leave unrecorded is one your fresh workers will repeat.
;

/// Always present: how to write the final message.
pub const closing_note =
\\
\\
\\Write the final message as an update to a teammate who has not seen your
\\screen. Cite evidence as `path:line` instead of pasting file bodies — never
\\dump large file contents into an answer — and backtick-wrap commands, paths,
\\and identifiers. Scale it to the change: a typo fix is one sentence, a
\\feature is a short structured summary. Close with the next steps that
\\genuinely exist — tests to run, follow-ups you left — and nothing more.
\\Be direct and concise.
;

/// Appended to BOTH the root and subagent prompts. openai/codex carries this
/// instruction verbatim in its base instructions ("Parallelize tool calls
/// whenever possible - especially file reads"); graff had no equivalent on
/// either prompt, so batching was left entirely to the model's own initiative.
///
/// The executor has always been ready for it: agent_tools.zig dispatches every
/// external call in a batch as a future BEFORE awaiting any of them, with no
/// cap and no root-vs-subagent branch. So this asks for nothing the harness
/// does not already do - it only stops the capability going unused.
///
/// The last sentence is the load-bearing half. Batching two edits to one file,
/// or a read whose path comes from the previous call's output, is wrong: graff
/// (unlike codex, which takes a write lock for non-parallel-safe tools) runs
/// the whole batch concurrently, so an unsafe batch really does race.
pub const parallel_core_note =
\\
\\
\\Parallelize tool calls whenever possible: when several reads or checks are
\\independent, issue them in ONE response instead of one per turn. Reads and
\\searches are the common case
;

/// The one clause of the batching note that is NOT capability-free: all three
/// examples are tools #330 hard-removes. The instruction survives the gate; its
/// illustration does not.
pub const parallel_examples_note =
\\ (read_file, codedb, grep-style bash)
;

pub const parallel_tail_note =
\\ and they
\\run concurrently. Keep a call in its own turn when it depends on an earlier
\\call's result, or when two calls would write to the same file.
;

/// The whole note, for `sub_system_prompt` — which is a comptime constant, so
/// a subagent still carries the examples. Same residue, different prompt; the
/// child inherits #330 too, so gating it is a follow-up, not this change.
pub const parallel_tools_note = parallel_core_note ++ parallel_examples_note ++ parallel_tail_note;

// ── ROOT PROMPT END ─────────────────────────────────────────────────────────
Loading