Harness spec 3.1.0: the prefix stays still, in the CLI
ci / check (push) Canceled after 0s

The system prompt is made once per session (start, compaction, model
switch), kept in session_meta and resent byte for byte; it holds no
date. What changes mid-session -- the date, a mode the person chose, a
plan or memory or instruction file they edited, a personality, MCP
instructions that arrived later -- is a <session-note> on the next user
message, stored with it. The tool list is fixed for the session:
plan_submit is always offered and answers unavailable outside plan
mode, the wrap-up keeps the tools and runs no call, a withdrawn tool
stays listed and answers its refusal; the first request waits up to
mcp_wait_ms for MCP servers. Compaction asks for its summary as the
next turn of the same conversation. Cached input tokens are parsed
(incl. llama.cpp timings), stored per request and shown in /context.

Spec: context.md §7/§10, loop.json context.note (+ plan_ended,
plan_own, personal, empty, mcp_wait_ms), modes.json plan.refused,
compact.md without the transcript heading. Conformance area prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-05 13:22:02 +00:00
1 parent ecae6247fd
commit 8e06590d57
36 files changed
+1533 -169

No files matched your search

+535
View File
@@ -0,0 +1,535 @@
{
"about": "The prefix (context.md §10, spec 3.1.0): one session's requests, in order — which system prompt each sends, which tools it offers, and the note on a user message. `start` is the session as it begins: `date`, `mode`, and what the prompt is made from that may change (`plan` as rendered, `memory`, `personality`, `files` — instruction texts by path or heading — `mcp` — instructions by server; each \"\" or {} for none), `could` (the requirements the session could ever meet: `plan` for any session that may enter plan mode, `display`, `page`, `forge`) and `met` (those met now besides plan, which is met in plan mode; default: all it could). `tools`: the names a case looks at; a tool's `requires` is the spec's. Steps: `user` (a user message: the request that carries it), `tool` (a tool step's request: no user message), `set` (something changes — any of the start's fields, or `met`; `by: model` when the model's own tool did it, whose result told the model: `plan` from plan_write, `mode` from plan_submit's approval, `memory` and `files` it changed itself; otherwise the person or the clock did), `call` (a call of `tool` now), `compact` (the summary is asked for: `{{transcript}}` and `{{previous_summary}}` empty — an earlier summary is already the first message), `wrapup` (the request after a step limit or a budget: the same prompt and tools, the wrap-up message as its last user message, no note; a call in its reply is not run) and `model` (the session moves to another model). A request answers `{system, tools, note}`: `system` numbers the system prompts the session has made, from 1, the same number being the same bytes; `tools` the case's tools it offers; `note` the note after the person's words, or null. `call` answers the tool's `unavailable` text, or null where it runs; `compact` answers the request's prompt, its tools and `ask`, the user message that follows every message as it was; `wrapup` answers like a request; `set` and `model` answer null; a value that became empty reads `context.note.empty`. `plan_dir` fills modes/plan.md. LLeMbas: services/context.py; CLI: session/prefix.ts, session/engine.ts.",
"cases": [
{
"about": "A date that changes during a session is a note on the next user message; the system prompt and the tools stay the same bytes.",
"start": {
"date": "2026-11-04",
"mode": "manual",
"could": [
"plan"
]
},
"tools": [
"read",
"plan_submit"
],
"steps": [
{
"op": "user"
},
{
"op": "tool"
},
{
"op": "user"
},
{
"op": "set",
"date": "2026-11-05"
},
{
"op": "tool"
},
{
"op": "user"
}
],
"expect": [
{
"system": 1,
"tools": [
"read",
"plan_submit"
],
"note": "<session-note>\nToday is 2026-11-04.\n</session-note>"
},
{
"system": 1,
"tools": [
"read",
"plan_submit"
],
"note": null
},
{
"system": 1,
"tools": [
"read",
"plan_submit"
],
"note": null
},
null,
{
"system": 1,
"tools": [
"read",
"plan_submit"
],
"note": null
},
{
"system": 1,
"tools": [
"read",
"plan_submit"
],
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
}
]
},
{
"about": "plan_submit is offered in every mode and answers its unavailable text outside plan mode; a mode the person chose is a note with the mode's text, one the approval set is not.",
"start": {
"date": "2026-11-05",
"mode": "manual",
"could": [
"plan"
]
},
"tools": [
"read",
"plan_write",
"plan_submit"
],
"steps": [
{
"op": "user"
},
{
"op": "call",
"tool": "plan_submit"
},
{
"op": "set",
"mode": "plan"
},
{
"op": "user"
},
{
"op": "call",
"tool": "plan_submit"
},
{
"op": "set",
"mode": "edit",
"by": "model"
},
{
"op": "tool"
},
{
"op": "user"
}
],
"expect": [
{
"system": 1,
"tools": [
"read",
"plan_write",
"plan_submit"
],
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
},
"Not in plan mode: plan_submit is how a plan made in plan mode is put to the person for approval. Carry on with the work; keep the plan current with plan_update.",
null,
{
"system": 1,
"tools": [
"read",
"plan_write",
"plan_submit"
],
"note": "<session-note>\nThe permission mode is now plan:\nPermission mode: plan. You are planning, not implementing. Read, search and investigate as much as you need, but change nothing: no file but a plan under .agent/plans, no command that is not already allowed, nothing saved to the library.\n\nWrite the plan with plan_write, so that someone else could follow it: findings with file:line references, what will be true when it is done (objectives), and the work as phases of tasks in order, with the risks and how to verify it as the last phase. Ask with ask_user if a decision is the user's. When the plan is written, call plan_submit — that is how the user approves it, and after approval you carry on and implement it, keeping the plan current with plan_update.\n</session-note>"
},
null,
null,
{
"system": 1,
"tools": [
"read",
"plan_write",
"plan_submit"
],
"note": null
},
{
"system": 1,
"tools": [
"read",
"plan_write",
"plan_submit"
],
"note": null
}
]
},
{
"about": "The compaction request is the same prompt and tools, every message, then the compaction text with no transcript; after it the prompt is made again and the next user message says the date again.",
"start": {
"date": "2026-11-05",
"mode": "edit",
"could": [
"plan"
]
},
"tools": [
"read",
"plan_submit"
],
"steps": [
{
"op": "user"
},
{
"op": "tool"
},
{
"op": "compact"
},
{
"op": "user"
},
{
"op": "compact"
}
],
"expect": [
{
"system": 1,
"tools": [
"read",
"plan_submit"
],
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
},
{
"system": 1,
"tools": [
"read",
"plan_submit"
],
"note": null
},
{
"system": 1,
"tools": [
"read",
"plan_submit"
],
"ask": "Summarise the conversation below so it can be carried forward after the earlier turns are dropped from your context. This is a working record, not a report for a reader.\n\nKeep, under these headings and in this order:\n\n## What we are doing\nThe goal, and where we have got to.\n\n## Decisions\nAnything settled, and why. A decision without its reason gets argued again.\n\n## Facts established\nNames, numbers, versions, file paths, commands, URLs and identifiers, copied exactly. Do not round them, paraphrase them or reconstruct one from memory — if it is not in the transcript, leave it out.\n\n## Files touched\nEach file read or changed that still matters, and what was done to it.\n\n## Open threads\nWhat is unfinished, and what was about to happen next.\n\nLeave out pleasantries, retracted ideas and anything already superseded. Do not answer the conversation: you are recording it. Write in the language of the conversation, and stay under 600 words."
},
{
"system": 2,
"tools": [
"read",
"plan_submit"
],
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
},
{
"system": 2,
"tools": [
"read",
"plan_submit"
],
"ask": "Summarise the conversation below so it can be carried forward after the earlier turns are dropped from your context. This is a working record, not a report for a reader.\n\nKeep, under these headings and in this order:\n\n## What we are doing\nThe goal, and where we have got to.\n\n## Decisions\nAnything settled, and why. A decision without its reason gets argued again.\n\n## Facts established\nNames, numbers, versions, file paths, commands, URLs and identifiers, copied exactly. Do not round them, paraphrase them or reconstruct one from memory — if it is not in the transcript, leave it out.\n\n## Files touched\nEach file read or changed that still matters, and what was done to it.\n\n## Open threads\nWhat is unfinished, and what was about to happen next.\n\nLeave out pleasantries, retracted ideas and anything already superseded. Do not answer the conversation: you are recording it. Write in the language of the conversation, and stay under 600 words."
}
]
},
{
"about": "What the model changed itself is not said again; what the person changed is, once, in loop.json's order; an MCP server that connected later is said by its own instructions.",
"start": {
"date": "2026-11-05",
"mode": "manual",
"plan": "",
"memory": "Prefers tabs.",
"personality": "",
"files": {
"/p/AGENTS.md": "Build with make."
},
"mcp": {
"docs": "Search the docs first."
},
"could": [
"plan"
]
},
"tools": [
"read"
],
"steps": [
{
"op": "user"
},
{
"op": "set",
"by": "model",
"plan": "# Fix\n## p1 P (pending)\n[t1] [ ] a",
"memory": "Prefers tabs.\n§\nUses fish.",
"files": {
"/p/AGENTS.md": "Build with make; test with make test."
}
},
{
"op": "tool"
},
{
"op": "user"
},
{
"op": "set",
"plan": "",
"memory": "Uses fish.",
"personality": "Be brief.",
"files": {
"/p/AGENTS.md": "Build with make; test with make test.",
"/p/src/AGENTS.md": "Strict types."
},
"mcp": {
"docs": "Search the docs first.",
"tickets": "Name the ticket in every commit."
}
},
{
"op": "user"
},
{
"op": "set",
"files": {
"/p/AGENTS.md": "Build with make; test with make test."
}
},
{
"op": "user"
}
],
"expect": [
{
"system": 1,
"tools": [
"read"
],
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
},
null,
{
"system": 1,
"tools": [
"read"
],
"note": null
},
{
"system": 1,
"tools": [
"read"
],
"note": null
},
null,
{
"system": 1,
"tools": [
"read"
],
"note": "<session-note>\nThe person changed the plan; it is now:\n(none)\n\nThe person changed what is remembered; it is now:\nUses fish.\n\n/p/src/AGENTS.md changed; it now reads:\nStrict types.\n\nThe person chose a different personality:\nBe brief.\n\nInstructions from an MCP server that connected since the session began:\n<server name=\"tickets\">\nName the ticket in every commit.\n</server>\n</session-note>"
},
null,
{
"system": 1,
"tools": [
"read"
],
"note": "<session-note>\n/p/src/AGENTS.md changed; it now reads:\n(none)\n</session-note>"
}
]
},
{
"about": "A move to another model makes the prompt again; the date said in the conversation still holds.",
"start": {
"date": "2026-11-05",
"mode": "auto",
"could": [
"plan"
]
},
"tools": [
"read"
],
"steps": [
{
"op": "user"
},
{
"op": "model"
},
{
"op": "user"
}
],
"expect": [
{
"system": 1,
"tools": [
"read"
],
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
},
null,
{
"system": 2,
"tools": [
"read"
],
"note": null
}
]
},
{
"about": "A display the session could have is offered from the start and answers unavailable while there is none; one it never could have leaves show out.",
"start": {
"date": "2026-11-05",
"mode": "manual",
"could": [
"plan",
"display"
],
"met": []
},
"tools": [
"read",
"show"
],
"steps": [
{
"op": "user"
},
{
"op": "call",
"tool": "show"
},
{
"op": "set",
"met": [
"display"
]
},
{
"op": "call",
"tool": "show"
}
],
"expect": [
{
"system": 1,
"tools": [
"read",
"show"
],
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
},
"Nothing can be shown now: this session has no screen to show a file on. Give the user the path instead.",
null,
null
]
},
{
"start": {
"date": "2026-11-05",
"mode": "manual",
"could": [
"plan"
]
},
"tools": [
"read",
"show"
],
"steps": [
{
"op": "user"
}
],
"expect": [
{
"system": 1,
"tools": [
"read"
],
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
}
]
},
{
"about": "The wrap-up keeps the session's tool list and prompt, carries no note, and the next user message carries what changed meanwhile.",
"start": {
"date": "2026-11-05",
"mode": "edit",
"could": [
"plan"
]
},
"tools": [
"read",
"plan_submit"
],
"steps": [
{
"op": "user"
},
{
"op": "tool"
},
{
"op": "set",
"date": "2026-11-06"
},
{
"op": "wrapup"
},
{
"op": "user"
}
],
"expect": [
{
"system": 1,
"tools": [
"read",
"plan_submit"
],
"note": "<session-note>\nToday is 2026-11-05.\n</session-note>"
},
{
"system": 1,
"tools": [
"read",
"plan_submit"
],
"note": null
},
null,
{
"system": 1,
"tools": [
"read",
"plan_submit"
],
"note": null
},
{
"system": 1,
"tools": [
"read",
"plan_submit"
],
"note": "<session-note>\nToday is 2026-11-06.\n</session-note>"
}
]
}
]
}
+16 -4
View File
@@ -129,7 +129,8 @@ two results.
Compaction is the one deliberate break of the cache, and it already exists. **From 3.1.0 the
summary is asked for as the next turn of the same conversation**: the same system prompt, the same
tools, every message as it was, and then one more user message — `prompts/tasks/compact.md`, whose
`{{transcript}}` is empty in this form, since the model has the conversation itself. So the request
`{{transcript}}` and `{{previous_summary}}` are empty in this form, since the model has the
conversation, an earlier summary included, itself. So the request
that makes the summary costs only that message; a separate request with a pasted transcript was a
full re-read of everything, and a transcript capped to fit it lost the session's middle. A tool
call in the summary's reply is not run; its text before the call is the summary, and a reply with
@@ -187,14 +188,25 @@ model): a date that changed at midnight sent a 16,912-token session from 99 % ca
</session-note>
```
(`loop.json` `context.note`.) A note is part of that message from then on, like anything sent.
A request with no user message (a tool step) carries no note; it waits for the next one.
(`loop.json` `context.note`.) Each change is one block of the note, the blocks separated by a
blank line, in the order `context.note` lists them; a value that became empty reads
`context.note.empty`; the date is the person's own (their zone, else the machine's). A note is
part of that message from then on, like anything sent. A request with no user message (a tool
step), and words the person adds to a turn already running, carry no note; it waits for the next
message that starts a turn. A change the model made itself (its own `plan_write`, `memory`,
`settings`, a file it wrote) needs no note: its tool result already said it.
4. **The tool list is the same for the whole session.** A tool whose `requires` can come and go
during a session — `page`, `display`, `forge`, and plan mode for `plan_submit` — is offered from
the start whenever the session could ever meet it, and a call made while it is not met answers
the tool's `unavailable` text. Only what is fixed for the session may leave a tool out: the
session kind (`sessions`), the model (`vision`), a subagent's narrowing, what the project's
configuration or the person switched off before the session began.
configuration or the person switched off before the session began. The tools stay listed in
the wrap-up request too (`loop.json` `wrap_up`): its message says they are withdrawn, and a call
in that reply is not run. A tool withdrawn after its final refusal stays in the list and answers
that refusal. **The one exception is an MCP server**: a session waits up to `mcp_wait_ms` for
the servers it is configured with before its first request, so their tools are in the first
list; a server that connects later adds its tools when it connects — one re-read, once — and its
instructions come as a note.
5. **Nothing already sent is rewritten** — the one rule above, for results; old tool output is
never pruned in place.
6. **Every request's cache use is recorded** where the provider reports it (cached input tokens),
+9 -4
View File
@@ -23,7 +23,7 @@
}
},
"wrap_up": {
"about": "One more request with no tools and a user message saying which limit was reached: answer now — what is done, what is not, what would come next. The transcript says the prompt was stopped, so an answer given because the room ran out is never mistaken for one the model chose to give.",
"about": "One more request, with the session's tool list unchanged (context.md §10, rule 4) and a user message saying which limit was reached: answer now — what is done, what is not, what would come next. The transcript says the prompt was stopped, so an answer given because the room ran out is never mistaken for one the model chose to give. A tool call in that reply is not run.",
"message": "The {{limit}} for this prompt has been reached and the tools have been withdrawn. Answer now from what you have: what is done, what is not, and what would come next."
},
"nudges": {
@@ -107,16 +107,21 @@
},
"reasoning": "Earlier turns' reasoning is left to the model's chat template: the harness sends the history as it does, neither stripping reasoning the template keeps nor adding what it drops.",
"note": {
"about": "A note on the next user message (context.md §10, rule 3): what changed since the system prompt was made, after the person's words. Lines in this order, each only when it applies.",
"about": "A note on the next user message (context.md §10, rule 3): what changed since the system prompt was made, after the person's words. Lines in this order, each only when it applies. `plan_ended` is the mode block for a session that leaves plan mode for a mode with no text of its own (a chat). `plan_own` is the plan block for a session that does not resend earlier tool results (a chat): the model's own plan, as its tools left it. `personal` is the {{path}} of the instructions block for the person's own instructions.",
"open": "<session-note>",
"close": "</session-note>",
"date": "Today is {{date}}.",
"mode": "The permission mode is now {{mode}}:\n{{mode_text}}",
"plan_ended": "Plan mode has ended: make the changes the work needs; nothing is refused for planning any more.",
"plan": "The person changed the plan; it is now:\n{{plan}}",
"plan_own": "Your plan, as your plan tools left it:\n{{plan}}",
"memory": "The person changed what is remembered; it is now:\n{{memory}}",
"instructions": "{{path}} changed; it now reads:\n{{text}}",
"personal": "the person's own instructions",
"personality": "The person chose a different personality:\n{{personality}}",
"mcp": "Instructions from an MCP server that connected since the session began:\n{{instructions}}"
}
"mcp": "Instructions from an MCP server that connected since the session began:\n{{instructions}}",
"empty": "(none)"
},
"mcp_wait_ms": 3000
}
}
+2 -1
View File
@@ -32,7 +32,8 @@
"read": "rules",
"write": "allow only for files under the plans directory; anything else is refused",
"execute": "only what the rules already allow; anything else is refused",
"interact": "rules"
"interact": "rules",
"refused": "Plan mode: nothing changes but the plan. Put what this would do into the plan with plan_write or plan_update; it can be done once the person approves the plan."
}
},
"unattended": "With nobody to answer, whatever a mode would ask is refused instead, and the model is told so up front (prompts/modes/unattended.md)."
-2
View File
@@ -23,6 +23,4 @@ Leave out pleasantries, retracted ideas and anything already superseded. Do not
{{previous_summary}}
## Transcript
{{transcript}}