Skip to content

Restructure: agent-centric model + demolition - #673

Draft
Wirasm wants to merge 20 commits into
mainfrom
claude/buzz-block-kild-comparison-jdcub4
Draft

Restructure: agent-centric model + demolition#673
Wirasm wants to merge 20 commits into
mainfrom
claude/buzz-block-kild-comparison-jdcub4

Conversation

@Wirasm

@Wirasm Wirasm commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Reframes kild from a Room primitive to an agent primitive, and carries out the teardown. Draft while the remaining work lands; commits are targeted so they review one at a time.

The model

The engine has one inhabitant type: the agent. Everything else is either mechanism kild owns or intelligence that lives outside it.

  • kild = a worktree = a workstream. Membership is "who is in this tree." The root checkout is kild-0; children fork from a base branch.
  • agent = the sole inhabitant, on one mechanism axis: owned (kild runs the process) or attached (an external harness kild addresses but never spawns). Its role is a persona — invisible to the engine.
  • honryo = an agent wearing human-authority intelligence, in practice the CLI a human drives. Authority is trained deference, never an engine flag.
  • spawninto: <kild> (another agent in the same worktree) or a new kild forked from a base.
  • send — directed, peer-to-peer. The sender names the recipient, so "who is this addressed to?" stops being a question the engine can get wrong.

Hard authority (spawn/stop/kill, guardrails) stays with the human as the control seat — it cannot be delegated to something running inside what it may need to kill.

Why

kild's own rule is "ship mechanism, never intelligence," and the Room model broke it: the engine encoded an org chart (operator → lead → worker), a privileged @human, a report-and-idle norm, and a decision protocol parsed out of message prose. The recurring "who is this addressed to?" bug — which bit in three separate places — was the symptom.

VISION.md already described this model; the implementation drifted.

⚠️ operator note — $KILD_HOME state dir moved

$KILD_HOME/rooms/<id>.json$KILD_HOME/kilds/<id>.json. An engine with existing archives will not find them after this branch. One-time fix on any machine that has run kild before:

mv "${KILD_HOME:-~/.config/kild}/rooms" "${KILD_HOME:-~/.config/kild}/kilds"

No migration code ships for this — it is a single-operator tool and a one-line mv, and auto-migration is the kind of machinery this branch exists to delete. Recorded here so it is not a silent surprise.

⚠️ helm — the pin boundary is a63476d

Everything before it is the old API; everything after is the new one. docs/helm-migration.md has the full mapping, a checklist, and the pin/un-pin sequence.

Two corrections landed after that guide was first written, both in it now: collidesWith was never on the wire (derive it client-side from git.changedFiles — that is the correct approach, not a workaround), and the kild-scoped transcript route is documented as doomed, so do not build against it.

Commits

# Commit Gates
1 Design brief + demolition map
2 pi-extension reframed as an attached-harness
3 Stale docs removed (5 files, 544 lines)
4 Dead Flue lane removed (12 files, 449 lines, 2 deps) 260 ✓
5 Mechanism-that-must-remain corrections
6 Operator tier deleted 259 ✓
7 Process norms moved out (decisions ledger, idle nudge, implicit replies) 235 ✓
8 Canonical vocabulary + helm migration guide
9 collidesWith correction + worktree disposal leak recorded
10 Reshape surface settled
11 Cross-kild comms plan
12 Escalation ownership settled
13 a63476d — the atomic rename 235 ✓
14 kild-cli skill brought in line

Engine source: 6,710 → 5,761 lines, with the substrate untouched.

The rename

One commit, zero behavior change, so it reviews as names moving and crosses helm's pin boundary exactly once. It also resolves three collisions that could not be fixed piecemeal:

  • agent meant three things — the persona file (kild agent ls), the same data at /api/personas, and listAgents() — while the running thing had no stable name at all (worker / participant / session). Now persona is the file, agent is the process.
  • role meant twowithRole/<role> and KILD_ROLE. Now <persona> wraps personas.
  • session meant two — a running agent, and pi's conversation. Now it is pi's alone.

worker went with them: rank vocabulary implying a lead and an operator above it, plus the job-queue model of an interchangeable executor — neither true of an addressable agent that holds context and spawns peers. post went too: you post to a board, you send to a person.

235 tests pass across 26 files — identical to the pre-rename count, which is the check that matters for a rename. No test was deleted.

Two obligations the audit initially missed

Both surfaced in review, both now in docs/DEMOLITION.md:

  1. Moving intelligence out leaves an obligation. Deleting the intelligence and its trigger removes a capability rather than relocating it. Memory synthesis keeps its mechanism half — a lifecycle event on close/land plus a generic hooks.onClose the engine runs without knowing what it is for.
  2. The ledger needs a real outcome source. finalNonSystemPost is a tail-grab standing in for facts the engine already owns. A smoke test proved it live, writing outcome: second message from a throwaway test post.

Still to land

The router commit — replacing the router with directed send, which removes HUMAN, the lead default, and the lifecycle states. Those are renamed but functionally intact today; mixing their removal into the rename would have made the diff unreadable. Then hooks.onClose before recordMemory's synthesis spawn goes, the fact-based ledger before finalNonSystemPost goes, and the reshape surface from docs/api-surface.md.

The pi extension is kept and rebuilt as the pi implementation of the attached-harness contract — peer to Claude Code's hook + CLI, neither privileged. It currently references the removed operator capability; it is rebuilt once rather than patched three times.

Planning docs on this branch

DEMOLITION.md (full-tree audit) · VOCABULARY.md (canonical naming) · helm-migration.md (client guide) · api-surface.md (reshape decisions) · cross-kild-comms.md (agent-to-agent addressing) · escalation.md (who surfaces a blocked agent) · worktree-disposal.md (the 116-worktree leak)

🤖 Generated with Claude Code

https://claude.ai/code/session_01NGEZwcNKSJZmuGuQhuFmcU

claude added 18 commits July 27, 2026 17:37
Captures the reframe from Room-as-primitive to agent-as-primitive, and
audits the whole tree against it.

The model (docs/model.html): a kild is a worktree/workstream; the agent is
the sole inhabitant type on one mechanism axis (owned vs attached); the
honryo is an agent wearing human-authority intelligence — in practice the
CLI a human drives. Messaging is directed and peer-to-peer; spawn defaults
to sharing a tree, forking one is the deliberate act. All process and
persona lives outside the engine.

The audit (docs/DEMOLITION.md, docs/demolition.html) marks every file
keep/reshape/move/die. Two separate teardowns fall out: the Flue lane is
dead code no live path imports (kild/run.ts, kild/auth.ts, flue/,
workflows/, flue.config.ts, the @flue/runtime dep), and the room
choreography — router, lifecycle, decisions ledger, operator tier — is
intelligence living in the engine.

No code changes; these are design documents.
The extension stays and is rebuilt, not retired. It is not "the operator
tier for pi" — it is the pi implementation of the attached-harness
contract, peer to Claude Code's: attach a handle, drain an inbox at the
turn boundary, act through the same API every client uses.

pi's integration is the better of the two (a native turn-boundary drain
rather than a shell hook), which is why it earns its keep. A third harness
later is another implementation of the same contract, not another
mechanism in the engine.

Records what the rebuild deletes (the operator guide and prompt injection,
the decision vocabulary, @Human semantics) and what it keeps (the REST/WS
client, bootId restart detection, the event bridge that is pi's drain).
Five docs describing work that has either shipped or is being torn out:

- engine/COMPARISON.md — the Flue-vs-Rust decision record, defending a
  bet that is being deleted from the tree
- docs/fleet-observability.md — spec/tracker; phases 1-2 shipped, phase 3
  is superseded by the agent-centric reframe
- docs/room-mailbox-notes.md — review notes on the room module being
  dismantled
- docs/pi-extension-plan.md — plan realized by the extension, which is
  now being rebuilt in a new shape
- docs/upstream-worktree-sandbox.md — proposal to upstream the Flue
  sandbox that is going away

docs/DEMOLITION.md records why each one goes.
Nothing on the live path imported any of it. The engine runs
server.ts -> worker.ts, which resolves models through pi's ModelRuntime
directly; the Flue modules were reachable only from each other and from
frozen demos that workflows/README.md already described as off the hot
path.

Deleted kild/run.ts (Flue in-process runner), kild/auth.ts (bridged pi
credentials into Flue's configureProvider, imported only by a demo),
src/flue/ and src/workflows/ entire, and flue.config.ts. Dropped the
@flue/runtime dependency, the @flue/cli devDependency, and the flue
script.

DEFAULT_MODEL goes with them: its only importers were deleted files, and
model resolution reads KILD_MODEL directly. The one deleted test,
flue/worktree-sandbox.test.ts, covered a deleted module.

260 tests pass, typecheck and lint clean.
Every MOVE needs a mechanism counterpart. Deleting the intelligence and
its trigger together does not relocate a capability, it removes one — and
the first pass of the audit tracked only what leaves. Two cases it
coupled without noticing:

Memory synthesis loses its only trigger when room-close goes. The engine
keeps the seam: a lifecycle event on close/land carrying the facts it
holds, and a generic hooks.onClose the engine runs without knowing what
it is for. The charter and persona are what move to PRP; memory.synthesis
generalizes into that hook rather than disappearing.

finalNonSystemPost dies with room-events.ts, and it is what writes the
ledger's outcome line. It should not be ported: it is a tail-grab
standing in for facts the engine already owns. The ledger instead records
the land result, commits vs base, changed files, and per-agent model and
cost — all from git-review, worktree-status and the session bus. Prose
outcome belongs to PRP's synthesis.

Also records the cross-repo call: helm cannot land atomically with kild,
but it is unaffected until the one commit that renames /api/rooms to
/api/kilds. helm pins across that commit rather than the engine
dual-serving both route families.
kild had three baked-in ranks: operator, lead, worker. The operator was a
privileged session type the engine granted room-control tools to. In the
agent-centric model the human's agent CLI drives the REST API as an
ordinary client, so there is no tier to grant: kild ships no brain.

Removes the four operator tools, the KILD_OPERATOR capability in worker.ts
and server.ts, and the whole `kild operator` CLI group. fleetMemorySection
goes with them — it existed only to give operator sessions cross-project
memory and had no other caller.

engine-client.ts and rooms-status.ts move up out of operator/: neither is
an operator concern. rooms-status carries the cross-kild collision
computation, which is general observability that was only ever filed in
the wrong folder.

One test goes with its subject (fleetMemorySection); no other test failed
at any point. 259 tests pass, typecheck and lint clean.

pi-extension still references the removed capability. It is rebuilt
against the new shape in a later commit rather than patched twice.
Three things the engine enforced that are the intelligence layer's to
decide, not mechanism kild should ship.

The decisions ledger: needs-decision[key] markers folded from post text
into a room ledger that blocked close until resolved, with a force
escape hatch to bury it. A protocol, and one the engine had no business
parsing out of prose. Gone with its force parameter, its REST field and
its paragraph in the mechanism prompt.

The idle nudge: a participant that finished a turn without posting got
prompted to report. That is a reporting norm. Deleted — but note the line
drawn here: `idle` itself is kept. It is state, not a norm (it rides the
participant view so a client can see who is finished and waiting, and an
empty mailbox drain is how an attached participant signals the same
thing). What goes is the engine telling an agent how to behave.

Implicit replies: an agent's turn-final narration was auto-posted so the
human could see it, and the router then needed a rule to stop that
narration from waking other agents. Both halves go; reaching another
agent takes an explicit post, which is what the tool always claimed.

The mechanism prompt also advertised the nudge ("kild nudges IT to
report"), which would have become a false promise — removed.

24 tests go with their subjects, each listed in the review notes; no test
was removed to green a gate. 235 pass, typecheck and lint clean.
VOCABULARY.md fixes three collisions the codebase had grown. `agent` meant
the persona file in the CLI, the same data was `/api/personas` over REST,
and `listAgents()` in code — while the running thing had no stable name at
all (worker, participant, session, depending where you looked). `role`
meant both the persona wrapper tag and the process role. `session` meant
both a running agent and pi's conversation.

Now: persona is the file, agent is the process, session is pi's alone.
One word per concept, one concept per word, plus the rules that keep it
that way — no rank words, and upstream keeps its own vocabulary.

helm-migration.md is the client-side guide: every route, payload field and
WS frame that moves, what disappears entirely (implicit, decisions,
posted, stopped, force) and why, and the pin/un-pin sequence. helm and
kild cannot merge atomically, so the rename is deliberately one commit and
therefore one pin boundary.
The migration guide claimed git and totals were unchanged "including
collidesWith". Wrong: collidesWith is produced by computeCollisions inside
compactLiveKilds, which is the CLI's compact view. It is not on
GET /api/kilds and never was. What the endpoint does carry is
git.changedFiles, which is everything a client needs to derive collisions
itself — so deriving them client-side is the correct approach, not a
workaround. A guide claiming to be everything a client needs has to be
right about what is on the wire.

Also documents why /api/worktrees survives next to /api/kilds (a worktree
outlives its kild: listing and pruning a stranded tree must work when
there is no kild to ask about), flags the duplicate transcript routes as
resolved-before-you-migrate, and states that `from` is already
engine-derived — supplying it is a hard rejection, not silent acceptance.

worktree-disposal.md records a live bug found while auditing: any
untracked file marks a tree dirty, dirty trees refuse removal, and prune
swallows the failure in a bare catch. Tooling that writes residue into
every tree at creation therefore makes every tree unreclaimable from
birth, with nothing reporting it. Measured at 116 stranded trees. The
silent catch is the defect that let a policy disagreement become a leak.
The rename was 1:1 by design — 25 endpoints in, 25 out — which is right
for crossing a pin boundary but means the demolition has not yet reached
the surface clients touch. These are the calls for the step that does.

An agent is addressed by handle. This reverses helm-migration.md, which
said id should win because handle is "an addressing concept, not a routing
key" — but that distinction is exactly what produces two identifier
schemes for one object, which is the shape the reframe exists to delete.
Verified safe: an agent stays on the roster after it stops, so a handle
never rebinds. Prompting an owned agent is therefore just sending it a
message, and the two delivery paths collapse to one.

Disposal gets a verb, guarded on authored commits rather than a clean
tree. Measured: 116 worktrees, zero reclaimable, and 27 of 27 sibling
trees refused as dirty from provisioning litter written before any agent
ran. A branch carrying unmerged commits is real work and the branch is the
safety net; uncommitted litter is not work, and treating it as work is
what produced 116 permanent trees.

/api/worktrees folds into /api/kilds, with the requirement that the
listing enumerate from git rather than the registry — otherwise a tree
whose kild record is gone has no id to address, which is the same
stranding being fixed.

Attribution moves to a credential minted at attach, sequenced with the
router commit since send is where from is resolved. Notes two corrections:
the engine's session id is engine-assigned, not a pi detail, and `from` is
already rejected on request bodies rather than caller-supplied.
Agents in different kilds cannot reach each other, so a human copies
messages between them. Not only a cross-repo problem: the agent in
kild/auth that finds an API change affecting kild/api has no way to say
so either. Parallel workstreams that cannot talk are half-parallel.

The tempting design is a kild-to-kild channel. Rejected — the endpoint is
an agent, not a kild, and what is missing is address space, not a channel.
A second messaging system would grow its own delivery rules, its own
guard, and its own log, and drift. This whole reframe exists because a
second answer to "who is this addressed to?" was allowed to exist.

So: widen the address on the existing send. Structured {kild, handle},
kild always an id, never a name — names are not unique across an engine,
and resolving them in the engine would reintroduce exactly the
name-or-path guessing server.ts already removed after it opened a kild
whose worktree could not be created and still returned success. Friendly
name/handle resolution belongs at the caller.

One real change falls out: the wake cap must stop being attached-only.
It guards attached inboxes today because only attached agents could
ping-pong; once any agent can address any other, two owned agents loop
just as easily and cost more, since nobody watches a kild the way a human
watches their own terminal.

Also records the stopgap that works today with no new mechanism (attached
agents do not run in their kild's tree, so a kild can serve as a pure
channel), and why removing the human from the transport makes the keyed
decision protocol in PRP more load-bearing rather than less.
cross-kild-comms.md ended by naming this gap without answering it.

PRP raises, kild carries, helm surfaces — and helm owns the surfacing
permanently rather than until something better exists. A decision is the
one thing the engine produces that is worthless if nobody sees it;
everything else is designed to run unattended. Reaching a screen and
persisting until acknowledged is a client's job by definition.

kild adds nothing for this. `idle` already identifies an agent that went
idle after asking — which is exactly why it survived the process-norms cut
as observable state rather than a norm — and the planned monotonic `seq`
covers "new since I last looked". No escalation type, no engine verb:
deciding that a message is a question rather than a status update is
interpretation, and the keyed-decision ledger was just removed for parsing
exactly that out of prose.

The requirement helm must meet is borrowed from the disposal leak, which
only became visible when someone counted at 116 trees: unanswered must be
loud and countable. If helm cannot answer "how many agents are blocked on
me right now" at a glance, the mechanism is not finished.

A second app is deferred rather than rejected — it is the right answer if
escalations must reach someone when helm is closed, but that is a
conclusion to reach after the cheap version shows how often this fires.
One atomic rename, no behavior change, so it reviews as names moving and
crosses helm's pin boundary exactly once.

Room becomes Kild — the unit of work is a git worktree, not a social
construct with its own lifecycle. Participants become agents, the sole
inhabitant type. The room/ slice flattens into the domain root, since
Kild is now the domain rather than one slice within it.

Three collisions are resolved in the same pass, because they were the
reason the vocabulary could not be made consistent piecemeal:

`agent` meant three things — the persona file in the CLI (`kild agent
ls`), the same data as /api/personas over REST, and listAgents() in code
— while the running thing had no stable name at all, being variously a
worker, a participant, or a session. Now persona is the file and agent is
the process.

`role` meant both the persona wrapper tag (`withRole`, `<role>`) and the
process role (KILD_ROLE). Now `<persona>` wraps personas and KILD_ROLE is
the only role.

`session` meant both a running agent and pi's conversation. It now means
only pi's, which is whose word it is.

worker goes with them. It is rank vocabulary — a worker implies a lead and
an operator above it, which is the org chart this branch deleted — and it
imports the job-queue model of an interchangeable executor pulled from a
pool, which an addressable agent that holds context and spawns peers is
not. The tell was server.ts having to explain that KILD_ROLE=worker "runs
a single agent session".

post goes too: you post to a board, you send to a person. Broadcast
vocabulary in a system whose whole point is that the sender names the
recipient.

REST moves to /api/kilds and /api/agents, WS frames follow, and the CLI
loses its room subcommand group. HUMAN, the lead default and the lifecycle
states are renamed but deliberately intact — they are removed in the
router commit, and mixing that into this diff would make it unreadable.

235 tests pass across 26 files, exactly the count before: a rename must
not change it, and no test was deleted. Typecheck and lint clean. Engine
boots and serves /api/kilds and /api/agents; /api/rooms/live is a 404.
The skill is the contract every agent drives kild through, and it
documented commands that no longer exist: the whole `kild operator` group
(deleted with the operator tier), `kild room *` (now top-level verbs),
`kild sessions` (now `kild agents`), and `kild agent ls` (now `kild
persona ls` — the word `agent` moved to the running process).

Two claims were not just renamed but false. It described delegation via
`invite_agent`/`post_message`, which are now `spawn`/`send`. And it
promised "if a delegate finishes a turn without posting, kild nudges IT to
report" — that failsafe was deleted as a process norm belonging to PRP, so
the skill was teaching agents to rely on a mechanism that no longer
exists. It now says plainly that reporting is the agent's responsibility
and nothing will chase it.

Adds the vocabulary section so the persona/agent/kild/handle distinction
is stated where agents actually read it, and points at
worktree-disposal.md on prune, since prune only reclaims merged and clean
trees and abandoned work otherwise accumulates silently.
The hook drained with `kild room drain`, which is now `kild inbox`. Every
failure path here exits 0 by design — a hook must never block a turn — so
the rename would not have raised anything. It would simply have stopped
delivering mail, silently, which is precisely the failure its own header
warns about in the generation before it.

Also moves the wiring env to KILD_KILD_ID / KILD_HANDLE, matching what the
engine sets on the agents it spawns, so an attached agent describes itself
the same way an owned one does.

Adds the note this file was missing: because everything here fails
silently, the CLI verbs are the one thing to re-check whenever the command
surface moves. Verified end to end against a live engine — attach, send,
drain returns the Stop-hook block JSON naming the new verbs, and a second
drain prints nothing and exits 0.
The guide told helm to pin across the rename and un-pin after, which was
true when the rename was the only breaking change. It is not anymore: the
router commit drops `system` from messages and `state`/`stopped` from
kilds, and requires `to` on every send; the reshape changes routes again.

Porting to each intermediate state would mean doing the work three times,
so helm stays pinned until the reshape lands and ports once against the
final surface. Documents the combined delta of the rename plus the router
commit, including the two things a client is most likely to have built on
— system notices in the thread, and keying off `state`.

Also records the split the router commit is careful about: the engine
never infers a recipient, but the CLI may resolve `--to` client-side when
a kild has exactly one agent. Convenience in the client, never a rule in
the engine.
The extension was the operator tier for pi: it injected a <kild-operator>
guide, spawned sessions with operator: true, spoke the needs-decision
vocabulary and special-cased @Human. All of that was intelligence the
engine no longer has and the extension has no business shipping.

It is now the pi implementation of the attached-harness contract, peer to
Claude Code's hook plus CLI. Eleven tools mirror the CLI verbs one to one,
described in mechanism terms only.

The bridge is now literally the drain. A {message} frame addressed to a
handle this session attached is only the wake signal; the mail itself
always comes from the inbox drain, so the engine's destructive-read and
wake-cap semantics apply to pi exactly as to any other harness rather than
pi having a privileged push. Reconnect drains each attached handle once,
since the inbox is engine state and a socket gap loses nothing.

Two things the rebuild surfaced. There is no REST route to spawn an agent
into a kild — it is WS-only and fire-and-forget, so kild_spawn cannot
report a truthful synchronous failure and instead polls and reports what
it observed. And kild_new does not attach the caller, so entering the
drain contract is an explicit act now that there is no privileged @Human
push.

727 → 844 lines. The ~400 estimate predated the decision to mirror the CLI
verbs one to one; the tool definitions alone are ~340.
Addressing bugs bit in three separate places because a shared broadcast
log forced the engine to guess recipients. The sender now names them, and
the whole class goes with the guess.

`to` is required and never inferred. Gone with the defaulting: the lead
(agents[0]-is-lead, KILD_LEAD, the lead-only guard on stop), the
@human-wakes-lead rule, and the 1:1 bare-send special case — a solo kild
now gets the same answer as any other, because a rule that only applies at
one size is a rule the engine has to remember.

HUMAN is deleted outright. A human-driven harness attaches a handle like
anything else; there is no reserved participant and no privileged push.

kild-router.ts did not survive as a file. Once the choreography went, what
remained was a two-branch loop over state the manager already owns — the
injected Delivery seam existed to unit-test routing decisions, and there
are no decisions left to isolate.

Also removed: system messages (roster changes come from the {kilds}
broadcast, not fabricated log entries), the lifecycle state machine and
its six guards (liveness is registry presence; halt collapsed into stop),
and opener notifications (an agent that wants to tell the opener something
sends it a message).

finalNonSystemPost deliberately survives, moved into memory.ts — its
replacement lands with the fact-based ledger, and removing it first would
delete the ledger's outcome line before anything could write one.

222 pass, 0 fail. 13 tests removed with their subjects and listed in the
review notes; the rest rewritten rather than dropped. New tests pin the
rules that replaced them, including cli.send.test.ts on the split that
matters: the engine never infers a recipient, but the CLI may resolve --to
client-side when a kild has exactly one agent.
@Wirasm
Wirasm force-pushed the claude/buzz-block-kild-comparison-jdcub4 branch from a7b4e3a to 1777a0d Compare July 27, 2026 20:41
claude added 2 commits July 27, 2026 20:46
The extension was rebuilt against the API as it stood an hour earlier, and
the router commit moved four things under it. All seven claimed changes
were checked against engine source first; every one held.

`kild_send` advertised "omit to to address the kild lead" and sent the
field only when non-empty. There is no lead and the engine now rejects an
absent `to`, so the tool was documenting a rejection. `to` is required,
stripped of `@`, and always sent.

`kild_new` posted `kickoff` as a bare string, which is now a 400. It takes
an explicit recipient. It also pre-flights that the recipient is one of
the agents being spawned — without it the engine creates the kild, rejects
the kickoff, and tears the kild back down, which is a confusing way to
learn you typed a handle wrong.

`kild_show` read `.state`; liveness is now which collection answers, so it
queries live then archive and says which. Archived kilds became readable
rather than an error on the way past. `system` is gone from the message
type and the log rendering.

Adds `attachAs` to `kild_new`: a pi session that creates a kild otherwise
receives nothing until it attaches separately, since there is no
privileged push any more. Two explicit calls in one tool — convenience in
the client, not a default in the engine.
Two halves of the same obligation: when intelligence leaves, the mechanism
it was hanging from has to stay.

The engine no longer knows what memory synthesis is. On stop it emits a
kildClosed event carrying only facts it holds — kildId, name, cwd,
worktree, base, transcriptPath, ledgerPath — broadcasts them on the event
stream, and runs whatever hooks.onClose declares. A hook is an agent spec
with a prompt, an argv, or both; there is no registry and no hook name the
engine recognises, so it cannot tell a memory hook from a backup hook.
Facts substitute as {{fact}} rather than ${fact} so shell expansion stays
the author's. The hook is not awaited and every failure is swallowed — a
hook must never hold a stop open or prevent one.

The charter moves out: synthesisPrompt, configuredMemorySynthesis and the
memory.synthesis branch are gone. docs/onclose-hook.md carries the exact
old prompt so migrating is copy-paste rather than git archaeology, and
records that live memory.synthesis configs are no longer read and must
re-point.

The ledger stops guessing. finalNonSystemPost was a tail-grab standing in
for facts the engine already owns — a smoke test caught it writing
"outcome: second message" from a throwaway test post. It is deleted, there
is no outcome line, and a test now forbids one. In its place: whether the
branch landed, commits vs base, files changed, uncommitted count, and
per-agent model and spend, read from kildGitStatus and reviewCommits
rather than reimplemented. A git failure reports "landed: unknown" instead
of inventing a result.

239 pass. Two tests removed with the charter they covered; the rest
rewritten, and 21 added — including collectLedgerFacts against real temp
git repos for the unlanded, merged and non-repo cases.
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