# Changelog — pi-daddy

Newest first. **Breaking changes are marked and say what to do about them.**

This file exists because the README had grown ninety lines of stacked version banners before a reader
reached what the package *does* — churn documentation in front of product documentation. The banners are
the record of how the package got here and are worth keeping; they are not worth reading first.

> **0.13.0 is the first PUBLISHED release.** Every version below it was developed in this repository and
> never shipped to npm, so if you are reading this as a new user, **none of the breaking changes described
> below can have affected you** — there was no earlier version to install. They are kept because they are
> the record of how the package arrived at what it does, and because the reasoning behind each one is
> usually the clearest statement of why the current behaviour is what it is.

## 0.40.1 — a `context:` capability could not be written to the ledger, so delegation was denied

**A session whose grant held any `context:` id could not delegate at all.** Reported from a live session:

```
delegation refused: grants: ledger write failed, denying — TypeError: invalid ledger v3 event:
capability decision requested must contain capability identifiers
```

`ledger-identifiers.ts` kept its OWN copy of the capability namespaces, and ADR-0078 added `context:` to the
kernel's list without it. Every capability-decision event validates six capability arrays with that predicate,
and a ledger write that throws denies the delegation — so the whole namespace took delegation down wherever it
appeared, and the message named neither the namespace nor the list.

0.40.0 widened it rather than causing it: held-back definitions began contributing `context:` ids to the
generated grant, so far more sessions carry one. Anyone whose skills declare a handoff mode was affected from
0.33.0 onward.

**The ledger now derives its namespaces from the kernel's list** instead of restating them, and a test asserts
every namespace the kernel defines is a valid ledger identifier. Adding a namespace without the ledger following
now fails.

This is the same shape this package has spent a session finding: one rule, two spellings, the guard on the
quieter one. A validator that restates what it validates is that shape with a ledger behind it.

## 0.40.0 — the file-writing tools are gated, and a held-back definition keeps its context modes

**`tool:write`, `tool:edit` and `tool:edit-diff` join `tool:bash` and `context:fork` in `DEFAULT_GATED`**, by
operator decision after 0.39.0's review measured why. A governed child holding `tool:write` used pi's own
`write` to rewrite the operator's stored grant, widening it from `tool:read` to `tool:bash`, and to write the
record that decides which workspaces are routable. `write` resolves a path against the cwd and writes it with no
check that the result is under the cwd; its schema says "relative or absolute". So it reaches every
operator-state file on the same account, which is where `bash` reaches, by a shorter route.

`edit` and `edit-diff` are included because they share that unconfined resolution. Gating `write` alone would
have been a control with a hole its author already knew about: `edit` needs the file to exist, which is no
protection for a grant store that does.

**What it costs, accepted deliberately.** Most useful delegations write something, so they now ask once until an
approval is banked. R-25 is about exactly that fatigue. `PI_DADDY_GATED` remains the escape hatch, and an
explicitly empty value gates nothing.

**The unit suite barely noticed, which was the more useful finding.** Nearly every delegation test sets
`PI_DADDY_GATED` explicitly, so adding three ids to the default failed exactly one assertion — the one that
restates the list. Three integration tests against a real pi process caught the rest. A default nothing drives
is a default nobody is testing, so there is now a test that plans a real delegation with the variable unset.

**A definition held back for needing a withheld tool now contributes its `context:` modes to the generated
grant.** Reported by the operator and measured: `init` built the live grant from authorised definitions alone,
so one withheld for declaring `write` contributed nothing at all — including a `context:files` that nothing
withholds. With `principal-pi-skills` that stayed invisible, because its two read-only definitions supply
`context:summary` and summary subsumes files. Measured on that package: remove those two and the generated
grant has no `context:` entry at all, while three definitions still declare one. The operator would uncomment
`tool:write`, the definition would become spawnable, and its handoff would be refused by a capability that
appeared neither in the grant nor in the withheld block beside it. Granting it runs nothing and changes nothing,
which is `grant-env.ts`'s own rule; `context:fork` stays out because it is gated.

## 0.39.0 — the registry's id set is the operator's too

0.38.0 bound what a workspace id MEANS. It said nothing about which ids exist, and the registry is an ordinary
file whose path a governed child inherits.

**Measured, and worse than the gap register claimed.** That entry said a child could add an id but that `init`
scaffolding ids commented out kept it unusable. With a `workspace:*` root, no operator action is needed at all:
a child holding `tool:write` appends an entry, the pin stops it resolving in that session and stops nothing
afterwards, and the NEXT root session mints a pin from the tampered registry while the wildcard admits any id.
End to end — the catalog listed it, the pin covered it, and it routed into the child's own directory.

**The accepted id set now lives beside the grant store**, in pi's agent directory. An id nobody accepted is not
pinned, and therefore not routable.

**What that does not buy, because the first draft of these notes claimed it did.** "Outside the workspace, where
a governed child cannot reach it" is false. A security review reproduced it end to end with pi's own `write`
tool from a real depth-1 child holding `tool:write` and no workspace capability: `write` takes an absolute path
and performs no confinement, the agent directory is inherited, and every path under it is deterministic. The
same technique widened the operator's STORED GRANT from `tool:read` to `tool:bash`, so the precedent this cited
does not hold either. What this release actually buys is the narrower closure it was built for — an attacker
who rewrites only the registry — and a raised cost for the wider one. The risk register now carries the class:
`tool:write` is much closer to `tool:bash` than this package has been treating it. `/grants
workspaces` accepts the registry's current ids. A malformed acceptance record accepts NOTHING rather than
falling back to the registry, so corrupting one byte is not a bypass.

**Trust on first use, and said out loud.** A machine that has accepted nothing accepts what the registry holds
the first time and writes it down, because refusing everything on upgrade would enforce a decision the operator
already made by writing that file. What matters is that additions afterwards are visible, and session start
announces the first acceptance rather than installing itself quietly.

**What this does not cover.** Acceptance is per registry, so an id accepted once is accepted for every project
sharing that registry. And it takes effect for the NEXT session: re-settling a live session's pin is exactly
the re-mint that ADR-0042 spent four review rounds closing.

## 0.38.0 — a rewritten registry can no longer change what an authorised id means (ADR-0042)

**The escalation this closes, measured in `g37-registry-tamper`.** `workspace:<id>` attenuated the NAME, not the
mutable id-to-path meaning. A child holding `workspace:staging` and `tool:write` — no `bash` — could rewrite the
operator's registry so `staging` pointed at the `prod` worktree, route a grandchild there, and take an exclusive
write lease on it. Every capability check passed, because the id it was granted was the id it used. File ownership
could not help: a governed child runs as the parent's uid.

**The inherited destination pin.** A root resolves each registered id to its canonical destination and records a
digest. Descendants inherit only the entries their own grant names, cannot mint an entry, and routing requires an
exact match. A rewritten registry therefore changes where an id points and not what an inherited id may mean.

**Every failure refuses, and a DESCENDANT never mints.** Missing, empty, malformed and mismatched all refuse.
That sentence was written before it was true: a security review reproduced the escalation end to end, across a
real process boundary. A parent whose own registry was unreadable at start has no pin, so the variable was
OMITTED from its children's environment, and a child read the absence as "I am a root" — minting a pin from the
registry it had just rewritten and routing to the prod worktree while holding only `workspace:staging`. Any
child with `tool:write` can arrange that precondition by truncating the registry. Depth already rides in the
environment and already attenuates, so anything below the root that arrives without a usable pin now routes
nowhere, and a malformed value refuses instead of earning a promotion.

**A session settles its pin once, and settled-ness survives a reload.** `/grants init` re-established a root's
pin from whatever the registry said at that moment, and a child had had the whole session to rewrite it. Putting
the flag on the session closed that and left the extension RELOAD open, because a reload builds a new session
object: a root legitimately inherits nothing and sits at depth 0, so it minted a second time. A re-run of the
security review measured a reloaded root routing into the prod worktree. Settled-ness now lives on the reload
lifecycle, which is keyed by owner and is already what recovers a root across a reload.

The fix for that then produced two more of itself, both reproduced: the new lifecycle field was assigned at each
`return` and missed two of five exits — including the `catch` around an unreadable registry, which is exactly the
state a child creates by truncating the file — and the one place that replaces a lifecycle's root baseline was
not taught about the pin, so an explicit root replacement was honoured for the grant, the depth and the
approvals and silently ignored for the pin, in the widening direction. `establishRootPin` now computes a value
and assigns once, so "every path settles" is structural rather than a checklist; the checklist is what missed
them.

A sixth followed from the fix for the fifth: the deletion above fires whenever the current environment does not
match the last child publication, and its own comment called that "an explicit change to this owner's root". True
of the second half of that condition and false of the first, which fires when NOTHING has published yet — where
nothing has been replaced at all. A root that settled and reloaded before publishing minted again. It is now
deleted only when the root actually changed.

That is six instances in one feature of the same shape — the rule goes on one path, and another path does not get
it. Every one was found by a reviewer rather than by the author, and four of the six were found in the fix for the
one before. It is the same shape as the `workspace:*` wildcard rule that lived only in `childEnv` while
`delegate.ts` handed the wildcard down.

**One builder for both spawn paths.** `delegate.ts` builds a child's environment itself rather than through
`childEnv`, and this file already records what that fork cost once before: the "never inherit `workspace:*`" rule
lived only in `childEnv`, so the delegate path handed the wildcard down. The pin reached the same fork and is now
written by one function both callers use.

**A session's own pin lives in memory, and is read from the reload snapshot, not from its environment.**
`publishChildEnv` writes the CHILD's narrowed pin into `process.env`, so a session that re-read the variable
would check itself against its child's authority. The first version of this change fixed that at the ROUTING
site and missed the ESTABLISH site, which is the one that overwrites the session's pin — so every `/grants init`
narrowed it, and a `workspace:*` root, whose published child pin is empty because the wildcard is never
inherited, lost ALL routing until restart while being told "no destination pin was inherited" by a session that
had established one. The inherited pin now comes from the lifecycle root snapshot, which is what every other
authority-bearing input already uses.

**Operator surface.** `/grants` lists the pinned ids under `routable`, and session start names any registered
workspace that could not be pinned and why. The mechanism shipped with none of this: an operator refused for
want of a pin could not discover that pins existed.

**API.** `resolveWorkspace` takes a third parameter, a parsed pin, defaulting to the environment — so an
external caller that worked in 0.37.0 now refuses unless one is supplied. `destinationDigest`,
`parseWorkspacePin`, `establishWorkspacePin`, `formatWorkspacePin` and `ENV_WORKSPACE_PIN` are now exported for
that reason. `validateRegisteredWorkspace` remains pin-free by design and now says so: it answers "is this path
the worktree it claims to be", not "may this session route here".

**What this does not cover, stated because eight refuted attacks reads like a proof and is not one.** The pin binds
a governed descendant, because it rides in the environment of a process the parent starts; a child holding `bash`
starts an ungoverned process and none of this applies. It binds the id-to-destination mapping and says nothing
about which ids exist — the registry stays child-writable, and what keeps a new entry unusable is that `init`
scaffolds `workspace:` ids commented out, which is a policy in a scaffolder rather than an enforced invariant. It
is a digest of the path, never of the contents. And the Herdr pane path depends on one measured fact with no test
behind it, that `tab create --env VAR=` delivers a variable set-but-empty. The gap register carries the full list.

## 0.37.0 — the handoff probe, and the budget was cutting the wrong end

**A `pruned` handoff carried the turns furthest from the task.** Turn sections are pushed oldest-first and the
32 KiB budget was spent in array order, so when the cap bound the turns dropped were the most recent ones — the
ones adjacent to the task and the likeliest to matter. Measured over 78 real pi sessions, the cap bound in 13% of
them at the old default and 60% at twenty turns, and the effect was visible end to end: the share of task-referenced
entities reaching the child **peaked at twenty turns and then fell** at the fifty-turn ceiling. Asking for more
context made the child worse off, and the parameter read as if it did the opposite.

**`ContextSection` gained `keepRank`.** The budget is spent by rank, highest first; presentation stays
chronological, because a child reading its parent's turns out of order is a different defect. Delivered recall is
monotone again.

**`CONTEXT_RANK` names the bands.** What the parent chose — a summary, a file it named — outranks what a rule
chose, and within the rule's output a turn kept for NAMING a file outranks one kept for being recent. The first
version ranked turns only and let everything else default to zero, which meant a `pruned` handoff dropped the file
the parent had explicitly named before it dropped any turn, leaving no header behind to say a file had been named.

**`DEFAULT_CONTEXT_TURNS` is 20, raised from 6.** Delivered recall at 6, 20 and 50 turns is 0.532, 0.737 and 0.747.
The jump from 6 to 20 is large and well outside the corpus's own noise of about 0.02; the last 0.010 is not, so 20
is the conservative end of a flat region rather than an optimum. The cost, stated because recall rises with this
number by construction: the mean payload goes from 12.6 KiB to 25.8 KiB. Raising it before the fill order was
corrected would have made things worse, not better.

**The record now counts what crossed.** `sections` and `keptTurns` were counted before the budget ran, so a handoff
could record twenty-one kept turns having sent thirteen — tolerable while the cap bound in 13% of handoffs, not
once the new default made it 60%.

**Raising the default would have killed the pruning advisor, and nearly did.** `advisePruning` gave up entirely
above `MAX_JUDGED_TURNS` (twelve), so with a default of twenty every default request would have exceeded the bound
and the decision point shipped in 0.35.0 would never have fired again — silently, with nothing failing. It now
judges the most recent twelve candidates and KEEPS the older ones unjudged, which is still only narrowing because a
turn it was never shown is never dropped. A test now asserts the advisor fires at whatever the default happens to
be, so the next change to that number cannot repeat this.

**The probe ships as a rerunnable measurement**, `test-integration/pruned-handoff-probe.it.ts`, and it calls the
real `fenceContext` rather than simulating the cap — a first draft simulated it and would not have caught the
regression it exists to describe. Reverting the fill order fails it with a message naming the cause. Without
`PI_DADDY_PROBE_SESSIONS` it reports that it measured nothing rather than passing quietly.

**Two corrections review forced on the measurement itself**, recorded because a probe whose errors are not written
down is worth less than none. Its first ground truth came from `JSON.stringify(message)`, and 48.9% of the scored
terms never appeared in anything anyone wrote — envelope keys present in every turn, scoring near 1.00. And the
claim that "a rule keeping the wrong turns would score badly" was false: on that metric, twenty turns chosen at
random nearly matched the rule and the twenty longest beat it. The ground truth is now the task's own prose and
control rules are measured every run.

**What the probe does not establish, and one thing it settled.** It does not show that term recall is task success:
no child was run and no model was called. It does not support making `pruned` the default mode, and that question
is now answered no — about a quarter of what a task names is missing at the default, and the cost of being wrong is
the operator's own session leaving the machine. `none` stays the default.

## 0.36.0 — one bounded reader, and a registry that fails soft without failing silent

**No behaviour changes for a working setup.** Everything here is about what happens when an operator-authored
file is malformed, oversized or hostile, and about making three guards fail a test instead of a reviewer.

**One reader for both session-start reads.** `loadWorkspaceRegistry` had worked out the correct shape the
expensive way — a non-blocking open, because a FIFO blocks inside `open(2)` before any read starts and no signal
can rescue it; every check against the held descriptor, because `stat`-by-name followed by read-by-name is a
TOCTOU; a deadline between chunks; a size bound checked twice. Its own comment ended "One reader is why that
cannot happen again", and then the second session-start read went on using a bare `readFile`. Both now call
`readBoundedFile`.

**`SKILL.md` reads are bounded, and a dropped definition is named at session start.** They had no size bound:
measured at `7096f78`, an 8 MiB definition was read whole into memory in 8ms, once per discovered skill inside
`session_start`. The bound is 1 MiB, matching the registry's, and roughly thirty times the largest real
definition found on this machine. **This is a behaviour change**: a definition over 1 MiB used to load and now
does not, so session start names it, the file and the bound. There were three readers of `SKILL.md`, not two —
the third feeds `pi-daddy init`, and bounding only the other two made `init` grant `agent:<name>` for a
definition the runtime refused, whose only symptom was `unknown agent "x"` at delegation time. All three are
bounded now and a test asserts the two agree.

**A malformed registry says so.** It still fails SOFT — one bad entry must not stop a session starting, because
nothing in the catalog is an authority — but the reason is no longer discarded. It rides on
`Catalog.registryRefusal`, session start names it, `/grants` prints it under `routable`, and both `init` paths
report it. Before this, one malformed entry removed every workspace from `/grants`, the catalog and `init` with
no message anywhere, so an operator could not tell it apart from having registered none. That is rule 8's silent
safe-mode. Session start matters most: it is where every comparable notice already lives, and the only surface
an operator sees without asking.

**Guards that could be deleted with the suite still green.** Measured before fixing: replacing the registry
deadline with `if (false)` left all 876 tests passing. The clock is injectable so a test can force it without a
slow disk. Three independent reviews then found three more unforced properties and all are forced now: the
deadline's POSITION between chunks rather than merely its existence, `grew-while-reading` (deleting it turned an
overrun into a silent truncation reported as a successful read), and the wiring of every new report — the
definitions callback, the session-start notices, and both `init` call sites were each revertible with the whole
suite green.

**API.** `makeCatalog` takes an optional second argument and `Catalog` has an optional `registryRefusal`;
`loadDefinitions` and `registeredWorkspaceIds` each take an optional reporting callback; `readBoundedBytes` is
the byte-level reader. All additive, and the only behaviour change for an existing caller is the 1 MiB bound
described above.

**Known limitation.** A definitions-discovery failure suppresses the registry refusal: `buildCatalog` calls
`loadDefinitions`, which can throw on a malformed settings file, so on that path no catalog exists to carry the
reason and only the definitions fault is reported. Fixing it means changing the catalog's error model.

## 0.35.0 — the first two decision points, and only the environment can enable an advisor

**An advisor now fills two blanks.** When a `delegate` call names no `thinking` level, an enabled advisor is asked
to choose one from the levels the CHILD's model reports it supports. An explicit level is never overruled, and with
no advisor, no answer or a timeout the blank stays blank and the child is spawned exactly as before. The task text
is sent to the advisor so it has something to judge; it is still never recorded.

**A `pruned` context handoff can now ask which turns to carry.** The mechanical rule runs first and decides what is
eligible; the advisor is then shown at most twelve of those turns, truncated, and may only NARROW the set — an id
it invents or one the rule dropped is ignored, and an incomplete answer is discarded whole. The selection is asked
only after a plan says the handoff survived the ceiling, the grant and the gate, so a delegation the grant refuses
never ships session turns. With no advisor or no usable answer, the mechanical selection stands unchanged.

**This sends more than the effort point does.** A `pruned` handoff sends the operator's own session turns, not just
the task, to a third party. That is the sharpest egress in the package and it is why an advisor is off by default,
enabled only from the environment, and reported by `/grants`.

**Security fix over 0.34.0.**

0.34.0 read the advisor's enable switch from `.pi/pi-daddy/settings.json`. That file is writable by any child
holding `tool:write`, and this package keeps the grant outside the workspace for exactly that reason: a ceiling a
governed child can rewrite is not a ceiling. The same argument applies here one step sideways — a child could have
flipped the switch and made the operator's next session send its own description to a third party.

**What to do.** Enabling an advisor is now `PI_DADDY_ADVISOR=jev` alongside `PI_DADDY_ADVISOR_KEY`, with
`PI_DADDY_ADVISOR_MODEL` to override the model; all three are stripped from a child spawned as a subprocess. A
Herdr pane inherits the daemon's environment, so a daemon started from a shell exporting them still hands them to
pane children — stated rather than implied, and not yet closed.

A project's `advisor` block in `settings.json` may turn an advisor off for that project and shorten its timeout. It
can no longer turn one on, choose its model, or lengthen its bound: a model is a destination and a longer bound is
not a narrowing, and that file is writable by any child holding `tool:write`. Anything that is not exactly `true`
on `enabled` disables. So a settings file that relied on `enabled: true` will find the advisor off until the
environment variable is set, and one that set `model` will be refused with a message naming the variable to use.

## 0.34.0 — an advisors layer, off by default (ADR-0077)

`src/advisors/` holds a `Decider` that answers typed questions — `noul` (a boolean), `choice` (one of the options
the caller already had) and `score` (a level from the caller's own list) — each with a probability. It may select,
rank, annotate or propose, and it can never widen a grant, satisfy a gate or replace a human's answer. That is
enforced rather than promised: no type in the layer names a capability or a refusal code, and no kernel or
governance module imports it, both checked by tests.

Off by default. An advisor runs only when `.pi/pi-daddy/settings.json` has an `advisor` block saying so and
`PI_DADDY_ADVISOR_KEY` is set; malformed configuration disables it and names the field rather than failing either
silently or open. Every use writes an `advice` record to the ledger, including the uses that produced nothing, and
that record never contains the state the caller composed. Degradation is always "no advice": disabled, no key, a
two-second timeout, a transport error or an unrecognised response all return the same nothing, so a caller written
against the null decider behaves identically with an advisor present.

The first adapter is TypeSafe's Jev through OpenRouter's Decisions endpoint (`typesafe/jev-1.13`). The request shape
is the documented one. The response shape is **not confirmed against a live call** — OpenRouter describes the
`answers` object without showing it — so the parser accepts what the documentation describes and treats anything
else, including a choice that was never offered, as no advice. No decision point uses an advisor yet.

There is no dashboard toggle: the dashboard is a read-only renderer that never affects enforcement, so enabling an
advisor is an operator edit to the reviewable settings file.

## 0.33.0 — a child can be given context, and the giving attenuates (ADR-0078)

`delegate`, `delegate_all` and each fan-out child accept a `context` parameter naming how much of the parent's own
session crosses: `none` (the default, and what every child got before), `files`, `pruned`, `summary` or `fork`. Each
mode is a `context:<mode>` capability in a new namespace, so it is intersected with the parent's grant and the
definition's `allowed-tools` ceiling exactly like a tool, appears in `/grants` and in the ledger's effective set, and
cannot be widened by a child. The modes are ordered and each subsumes the weaker ones.

`context:fork` is **gated by default** beside `tool:bash`: it is the one mode that can carry content an untrusted
repository put in front of the parent into a fresh child. What crosses arrives in a `<<<PARENT-CONTEXT …>>>` fence
marked as data, distinct from the chain handoff's fence, capped at 32 KiB with anything dropped said inside the
fence. Paths named for `files` are confined to the session's working directory. The capability-decision record gains
`handoff`, naming the mode the child received, how many sections and bytes crossed, and for `pruned` which rule ran
and how many turns it kept.

Nothing changes for a caller that passes no `context`: the default is `none`, which adds no capability and crosses
nothing. `pruned`'s selection rule is deterministic but its recall is unmeasured, so it is not a default.

A `delegate_chain` step takes the same parameter. A chain is planned as one unit, so a step's handoff is capped by
that step's own definition and any gate it raises is answered before the first step runs.

**Breaking for consumers of the package API:** `splitSystemPrompt` now returns `systemPrompts: string[]` instead of
`systemPrompt?: string`, because a child can carry more than one appended system prompt and only the first was being
staged for the Herdr executor.

## 0.32.1 — a doubled namespace in `allowed-tools` is explained, not just reported

An `allowed-tools` entry written with a capitalised namespace, such as `Tool:Read`, misses the lower-case prefix test,
is prefixed a second time on the bare-entry path and becomes `tool:tool:read`; `Workspace:prod` becomes
`tool:workspace:prod`. That spelling is the one the Agent Skills standard and this package's own README example
invite. `pi-daddy init` then refuses the definition and named only the mangled identifier, and a spawn refusal said
the capability was absent from the catalog. Both messages now name the mistake and the entry that was meant. No
behaviour beyond the wording changes: the refusal, its code and the effective grant are exactly as before.

## 0.32.0 — a child is stopped for inactivity, not by a wall clock (ADR-0076 PR 3e)

A governed child is now stopped when `PI_DADDY_CHILD_IDLE_TIMEOUT` seconds pass with no activity (default fifteen
minutes; zero or malformed selects the default). Activity is a stdout or stderr byte, a change to the child's pi
session file, or, on the process executor on Linux, CPU time in the child's process tree or a change in its
descendants. Every child gets a session file for this: the retention target when one is configured, otherwise a
private temporary file removed after the run on every path (kept only while an operator keeps a Herdr pane). Note the
consequence: every child's transcript is on disk for the duration of its run, whether or not retention is on.
`PI_DADDY_CHILD_TIMEOUT` is unchanged in meaning but is now the runaway ceiling with a six-hour default, so a build
that runs the test suite is no longer killed at sixty minutes while a hung child is stopped in fifteen. Both executors
apply the bound; on Herdr the signals are the session file and, when a progress display is attached, the pane text. Lifecycle events carry
`idleTimeoutMs` beside `deadlineAt`, and a stop records `reason: "idle-timeout"` or `"wall-clock"`; the refusal text
names the variable that sets the bound that fired. The `governance-event.schema.json` gains the optional field.

## 0.31.0 — the big cleanup: the delegate path and its record, nothing else (BREAKING)

Deleted, by operator decision, everything that was not the three tools, approvals, the ledger, workspaces, executors,
the activity timeline and the read-only dashboard: the skill-harness learning product (`/grants host`, `/grants
learning`, daily dashboard host, debrief, blind interventions, adoption policy, experiments, resource budget, intent and
dispatch control, ordinary-children cancellation), the work DAG (`/grants work`, `pi-daddy work *`) and the work
ledger v4, the daily view and panel, the check runner and workflow facts (ledger event kinds `check_receipt` and
`workflow_fact`, refusal codes `CHECK_*`), the `guide` and `current` commands, `PRODUCT-GUIDE.md` and
`REQUIREMENTS.md`. `pi-daddy-dashboard` takes only `--ledger`, `--once`, `--details`, `--no-color`.

**What to do about it.** The export map is now the root and `contracts/*` only: replace any import from
`pi-daddy/kernel`, `/ledger`, `/approvals`, `/executors`, `/dashboard`, `/work` or `/learning` with `pi-daddy`. Every
contract except `contracts/ledger-record/v1` is gone; regenerate consumers from that one. skill-harness is no longer
referenced. Project files `work.jsonl`, `work-current.json`, `work-setups/`, `work-outcomes/`, `work-policy-registry.json`,
`work-policies/`, `work-registry-bindings/`, `learning-workspace.json` and `work-last-run.json` under `.pi/pi-daddy/`
are no longer read or written; delete them when you like.

The repository's `docs/` folder, `CLAUDE.md` and the pre-commit branch guard are gone too; `AGENTS.md` carries the
rules, the decisions still in force, the measured facts and the roadmap, and `README.md` is the product description.

## 0.30.0 — one record envelope, one namespace, one state directory (ADR-0076 PRs 2 through 3d-i and the cleanup)

**PR 3d-i (BREAKING wire):** every grants-ledger and activity-timeline line is a record envelope
(`contracts/ledger-record/v1/record.schema.json`); governance events are the `body`, unchanged
(`governance-event.schema.json`, formerly ledger v3). A damaged file is read up to the damage and refuses appends with
`LEDGER_DAMAGED` until `pi-daddy ledger repair <path> --yes`. A pre-format `.pi/grants.jsonl` is imported once at
session start; ledger v2 is archived and no longer read. Consumers that pinned ledger v3 re-pin to
`ledger-record/v1`; the work ledger keeps its v4 line until PR 3d-ii.

**Earlier on this line (PR 2):** source layered under `src/{kernel,governance,executors,products}`

**Cleanup (same unreleased line, BREAKING):** five features are deleted: the bwrap digest effect profile,
factory orders, measured orders and sessions, the producer IPC bridge, and `delegate_all`'s primary/shadow mode
with `/grants variants`. Their exports, contracts and CI steps are gone; `fixed-experiment-v2` charters are
refused. The learning flow, dashboard, work plans and activity timeline are unchanged. The package README is a
page; the older documents are under `docs/archive/`.

**PR 3c (same unreleased line, BREAKING for operators):** every project file lives under `.pi/pi-daddy/`
(`grants.jsonl`, `work.jsonl`, `work-current.json`, `work-setups/`, `work-outcomes/`, `work-policy-registry.json`,
`work-policies/`, `work-registry-bindings/`, `learning-workspace.json`, `work-last-run.json`, `activity.jsonl`,
`content/`), and every user-level store under `<agent dir>/pi-daddy/` (`grants/`, `approvals/`,
`workspace-leases/`). **Nothing is migrated** (operator decision following ADR-0020): a session that finds a store
at its old location and not at the new one says so at start; run `/grants init` again and answer the gate again.
The old project ledger is left in place for the single-ledger release to import. `pi-daddy init` no longer writes
`.pi/grants.env`; it writes `.pi/pi-daddy/settings.json`, the same reviewable record as JSON, plus a
`.gitignore` that keeps everything else in that directory out of commits. **The default child timeout is sixty
minutes** (was twenty; ADR-0038 dated note); an inactivity-based deadline is the next PR after the ledger format.

**PR 3b (same unreleased line, BREAKING for operators and importers):** every `PI_GRANTS_*` environment
variable is now `PI_DADDY_*` (same suffix; `PI_GRANTS_LEDGER` becomes `PI_DADDY_LEDGER`, which the dashboard
already used for the same path). Legacy names are still read for one minor release with a warning; children are
spawned with the new names only. The export map is nine keys: `pi-daddy`, `pi-daddy/kernel`, `pi-daddy/ledger`,
`pi-daddy/approvals`, `pi-daddy/executors`, `pi-daddy/dashboard`, `pi-daddy/work`, `pi-daddy/learning` and
`pi-daddy/contracts/*`. Every previous per-file subpath (`pi-daddy/spawn`, `pi-daddy/workspace`, …) is gone;
import the layer barrel that holds it.
`pi-daddy/contracts/*` deliberately exports every file under `contracts/`, READMEs included, where eighteen
curated patterns were exported before. `PI_DADDY_GOVERNANCE` can no longer be set through the planner's
`childEnv` hook: it is a governance key and the guard now refuses by the closed list, not by prefix.

**PR 3a (same unreleased line):** the source is formatted with Prettier at width 120 and CI checks it; the
module-size guard counts statements and caps lines at 200 characters; two tests that asserted the wording of
repository documents were deleted. No runtime behaviour changed; seventeen long message and embedded-script
strings were split with identical bytes.

Internal restructure with no wire change. Export keys in `package.json` are unchanged and every subpath
still resolves; the compiled files moved from `dist/<name>.js` to `dist/<layer>/<name>.js`, so anything that
deep-imported a `dist/` file by path must use the export map instead. `pi-daddy/workspace` still exports the
lease functions (now from `governance/workspace-public.ts`). **One type change:** the planner's
`DelegationContext.activity` field is replaced by `childEnv?: (child) => Record<string,string>`; the kernel
refuses any `PI_GRANTS_*` key from it. `test/layering.test.ts` enforces that no import points upward.

## 0.28.1 — activity timeline safety and clearer outcomes

- Render private prompt/final detail content as safe literal text: terminal controls are escaped, deliberate
  line breaks are retained, and long detail lines wrap to the panel width. Metadata receives the same
  control escaping; color-disabled output cannot gain ANSI/OSC sequences from retained content.
- Clarify the default activity tree with semantic labels/colors, stable short selectors, compact quiet history,
  separate parent-turn/child outcomes, and visible observed child-failure totals. Completion is not acceptance.
- Pi's public extension/theme surface still cannot apply a per-extension background to ordinary assistant
  transcript messages, so prompt/response backgrounds remain unchanged and no transcript is mutated.
- The retained screenshot's `Herdr did not activate the bundled Pi lifecycle reporter before prompt dispatch`
  launcher error remains unresolved: no retained demo session/log was inspected in that pass to prove a packet-local cause,
  so no launcher behavior changed in this patch.

## 0.28.0 — unified activity timeline and default-on operation

- Combine root turns, delegated agents and observed/declared skill activity in one filtered timeline with history.
- Load exact submitted prompts and final responses on demand from bounded private local content references.
- Track leaf children through a no-tool observer without expanding grants or tool allowlists.
- Enable root governance and local observation by default, with explicit opt-outs and compatible Herdr auto-presentation/Pi fallback.
- Available, read and declared-active skills remain distinct; shell-only reads and unavailable editor provenance are not invented.
- Upgrade behavior: local prompt/final retention is now default-on. Use the documented metadata-only or observation-off option when desired. This does not enable uploads, model learning calls or automatic policy adoption.

## 0.27.4 — independent Herdr delegation names (2026-09-17)

- Generate a fresh execution suffix instead of a process-local counter, preventing parallel Pi parents
  and extension reloads from reusing globally reserved Herdr agent names.
- Keep readable tab labels and valid agent names within Herdr's 32-character limit.
- Regression coverage exercises parallel processes and fresh module instances; grant and cleanup behavior is unchanged.

## 0.27.3 — visible completed delegation history (2026-09-15)

- Show how many completed roots and children the compact ledger dashboard hides.
- Use h + Enter to expand/collapse finished history independently of Details.
- Active and attention ancestry remains visible; connected daily/debrief controls are unchanged.
- Expanded history can exceed the viewport; this patch does not add pagination.

## 0.27.2 — standalone CLI runtime dependency (2026-09-15)

- Declare the Pi SDK and TypeBox as runtime dependencies, so standalone pi-daddy commands work after Pi's managed
  installer omits peer dependencies. The 0.27.1 local-update check exposed the missing dependency.
- Installed-package smoke now uses the same peer-omitting npm mode as Pi, without injecting the SDK.
- Skill discovery and grant behavior are unchanged; the no-copy setup fix remains in place.

## 0.27.1 — reference enabled installed runtime skills (ADR-0074)

- Setup uses configured Pi runtime skills directly instead of copying them into `.pi/skills`, avoiding
  duplicate warnings and stale local copies after package upgrades.
- Definitions and the capability catalog honor Pi package/resource filters, scope precedence and
  `PI_CODING_AGENT_DIR` without installs, extension execution or model calls.
- Existing local overrides and grant files stay intact; unregistered npm packages retain legacy scaffolding.
  Review old local copies before removing them to follow installed package updates.

## 0.27.0 — ordinary work and learning product (release candidate; ADR-0073)

Prepared from `b9cbef9087b3596a0d930c0ec9b843f34c0f27e8`: Sol-approved producer runtime plus the
passing supported-command assertion fix. Feature PR #52 merged with green CI at
`b974963a7d0ba5a74fdafe331356c348a1fba565`. Release PR CI/merge, installed qualification and publication
remain pending. Matching skill-harness release planned: **0.16.0**. Release preparation changes metadata
and test synchronization only; dependencies and runtime are unchanged.

- Outcome-first Herdr daily panel, observed active model/effort, collapsed history and exact-frame numbered
  actions. The former diagnostic renderer remains `renderDailyDetails`; frozen fixtures stay unchanged.
- `/grants work` authoring/selecting of 1–8-task DAGs, scope/alternative/priority changes and explicit
  model/effort. Bounded runs reuse original delegation/approval/executor/cancellation owners, retain full
  bounded results, wait on pause and refuse stale pinned scope at admission. No automatic retries.
- Separate private instruction/presentation retention, including single-work outcome labels; governance
  ledgers still do not store instruction text. CLI `work list`/`work show` and public setup/run exports.
- `/grants learning` uses the loaded harness's scoped workspace/wizard. Original trust attention is reused;
  quality/reveal, adopt/reject/defer, independent eligibility, activation and later observation stay separate.
- Additive model/effort-only next-order registry, original receipt linkage, rollback and readback recovery;
  active/old runs stay pinned. Existing fixed-policy, ledger and retention contracts remain unchanged.
- PR52 review repairs: refuse host stop while pause/pending/unknown control remains; preserve holds on
  denied or unacknowledged resumes. Show the retained restore digest and every task model/effort before
  rollback consent; cover exact activation/readback receipt identities and friendly-label CI regressions.
- Packaged concise guide/register for pi-daddy 0.27.0 with skill-harness 0.16.0. Release PR CI/merge,
  installed two-extension linkage qualification and publication remain separate coordinator gates; no
  actual calibration, human acceptance or production improvement is claimed.

## 0.26.1 — measured-order fractional usage repair (2026-09-14)

- Preserve fractional measured-order usage when serializing results, so bounded recovery and dependent review retain exact reported usage.
- Reviewed source fix evidence includes 1,258 prior full tests, 28 final focused tests and typecheck; a bounded three-call Terra-negative/recovery and dependent-Sol-review demo settled all calls.
- The demo is execution evidence only: it does not establish human-quality acceptance.

## 0.26.0 — usability follow-up (2026-09-13)

- Fix Pi reload ownership: an owner-keyed global `WeakMap` survives extension path-cache reload, and delegation registers only after that owner's `session_start`; root settings and a separately inherited same-process SDK child remain distinct.
- Connected dashboard refresh preserves the unfinished command line, shows host-frame errors and conservative acknowledgement/result feedback, and continues to offer only exact displayed action keys.
- New `init`/work control directories are private at creation; unsafe or prior daily-host state is preserved and requires a fresh host ID rather than reuse.
- `delegate_chain` now accepts each step's validated `thinking` level and forwards it with that step's model request.

## 0.25.2 — live dashboard cancellation controls (2026-09-12)

- Refresh cancellation controls when ordinary children attach or settle after an idle dashboard frame.
- Preserve each displayed command's exact meaning; revision-specific keys prevent stale commands from targeting another child.
- Hide cancellation already requested while the original caller settles, and bound retained command history across supported child lifetimes.
- Independently reviewed by Sol; regression coverage includes idle-to-active display, sibling churn, stale frames, and pending cancellation.
- Fresh human dashboard cancellation acceptance remains a separate post-install test.

## 0.25.1 — reliable Herdr child completion (2026-09-12)

- Fix a governed child finishing while its parent keeps waiting with Pi 0.85.1 and Herdr 0.8.2.
  Children explicitly load the pinned, lifecycle-only Herdr integration; general extension discovery
  stays disabled and the granted tool surface is unchanged.
- Require native idle activation before prompting and a newer terminal sequence before settlement.
  Missing activation or stale terminal state refuses within a bounded wait and cleans up the child.
- Preserve existing public exports and ship the upstream Apache-2.0 notice and license.
- Verified route: Pi 0.85.1 / Herdr 0.8.2. Ordinary subprocess compatibility is unchanged.
  This does not repair standalone Herdr screen detection or the separate first-use host setup errors.

## 0.25.0 — practical daily steering and measured orders (2026-09-12)

- Add protocol-safe dashboard relink, exact active-attempt cancellation and durable measured-session spawn acknowledgement. Unknown/protocol-incompatible plugins still fail closed.
- Add labelled daily priority, recorded scope-successor and same-scope alternative actions through existing intent authority/CAS. Scope changes atomically rebind persisted and current-session declared work before ordinary dispatch resumes; active ordinary work suppresses steering actions.
- Add explicit two-minute closing presence and safe case-card preparation. Missing trust qualification spends zero of the durable five-card ceiling; preparation is not delivery until visibly acknowledged.
- Add a separate opt-in measured Sol/Terra SDK route with in-memory/no-tool bounds, observed identity, Pi Usage, clean-stop checks and explicit unknown state for launched nonsettling attempts. Pi catalog cost is not subscription billing and ordinary print-child Usage remains unavailable.
- Add package-exported bounded measured DAG orders with exact and deterministic free-form output qualification, complete sealed predecessor bytes/hash, original-owner unknown accounting, exact acknowledgement before declared recovery and inode-pinned append journals.
- Export the production daily host through the package root and `pi-daddy/daily-dashboard-host`; retain the exact skill-harness 0.15.0 bridge pin and learning-lifecycle navigation.
- Prior real NWS Sol/Terra outputs remain direct SDK comparison evidence. Offline contract validation is a no-model replay, not a new packaged measured-order occurrence. Human trust calibration, quality choice, configured adoption and later outcomes remain separate.

## 0.24.0 — declared daily work and concurrent variants (2026-09-11)

- Add `pi-daddy work add --id <id> --outcome <text>`: retain a digest-only selected obligation, reject changed redelivery and unsupported destinations, and join ordinary governed attempts without storing outcome prose. Runtime completion remains distinct from acceptance.
- Add the production `/grants host <fresh-id>|stop` lifecycle backed by the loaded skill-harness source bridge. The original process owns child handles, private socket transport and native validators; no authority is recovered from PID, pane labels or disk state.
- Publish frozen `pause-new-dispatch`, `resume-dispatch` and `refresh-current-work` dashboard actions. Pause changes only new ordinary admission, running children keep their original caller/result, refresh advances the existing source checkpoint explicitly, and stop never cancels a child.
- Add explicit bounded thinking selection to child argv and Work-v4 labels. Requested effort records selection only and is not proof of provider-internal reasoning.
- Add opt-in `delegate_all` primary return with eventual original-owner shadow accounting. Existing calls still wait for all; shadow failure or cancellation cannot replace or delay the selected primary, and invalid primary selection refuses before any child starts.
- Derive only exact retained scope, obligation and coverage when declared facts are absent. Deadlines, violations, prior acceptance, reopen history and unavailable artifact coverage are never inferred.
- Candidate-installed bridge/host startup, live busy-child pause/resume, same-host refresh/reconnect and primary-before-shadow settlement were observed. These are path/lifecycle checks, not acceptance or efficacy measurements.

## 0.23.0 — bounded factory control and retained evidence (2026-09-11)

- Add an opt-in work-ledger v4 contract, strict builders/readers, deterministic fixtures and explicit
  authority-scoped projection. Ledger v3 remains the default governance format; existing v2/v3 paths and
  behavior are unchanged.
- Add opt-in execution retention for exact available process, Herdr and native-session evidence without
  converting runtime completion into acceptance or claiming unavailable bytes.
- Add one fixed `linux-bwrap-digest-v1` effect profile with durable aggregate attempt/input/concurrency
  accounting. It executes package-owned digest/hold code only; arbitrary shell/model profiles, shared
  writable destinations and aggregate CPU/memory/money enforcement remain unsupported.
- Add versioned resource dispatch and work-intent controls, bounded experiments and factory orders. Exact
  authority, CAS, quiescence, reservation and acknowledgement boundaries fail closed; unknown ownership is
  never replayed, refunded or inferred as success.
- Add read-only daily projections, retained blind debrief/checkpoint adapters and an explicit dashboard host
  transport over a private bounded socket. Presentation, steering and cancellation require independently
  supplied exact authority; refresh remains read-only and does not authenticate a human or module.
- Retain original ordinary-child cancellation handles and intent admission holds without replacing caller
  lifetimes. Best-effort terminal observation failure stays visible but no longer permanently poisons a
  known-settled boundary; required failure, unknown ownership and late coverage still refuse quiescence.
- Reject benign stale dashboard CAS and immutable-ID mismatches without poisoning the host before any
  claim/effect. Attempted or uncertain effects retain the existing failed/unknown stop.
- Remove each newly owned native-profile probe fixture after all probe children settle, on success and
  failure, without sweeping historical or caller-selected directories.
- CI now provisions bubblewrap on its ephemeral Ubuntu 26.04 preview runners, records bounded runtime
  diagnostics and stages a verified job-owned Node runtime. These observations do not establish a general
  sandbox or a new supported-host guarantee.
- Add public subpath exports and versioned artifacts for the new work-ledger, resource, experiment,
  factory-order, effect-profile, retention, debrief and dashboard-host APIs. The workspace root remains
  private; `pi-daddy` is the only published package. No dependency ranges changed.

## 0.22.0 — fail-closed audit follow-ups (2026-09-04)

- **BREAKING — invalid project stores now fail closed.** Malformed, unsupported, unreadable and wrong-directory
  stores never restore the ungoverned wildcard; `GRANT_STORE_INVALID` records the safety stop. Repair the store
  or deliberately rerun `/grants init` rather than relying on invalid state being treated as absent.
- **BREAKING — explicit delegation models are resolved before any side effect.** Models absent from pi's session
  catalogue refuse as `MODEL_UNRESOLVED`. Register custom models with pi, or deliberately set the operator
  escape hatch `PI_GRANTS_ALLOW_UNRESOLVED_MODELS=1`.
- **BREAKING — correlation inputs are narrower.** `schema_version`, when present, must be exactly `"1.0"`, and
  `assurance_scope` must use the closed entire-run/selectors union instead of arbitrary JSON. Strict ledger-v3
  consumers must re-pin to the regenerated schema and fixtures, which also add `GRANT_STORE_INVALID` and
  `MODEL_UNRESOLVED` to the refusal enum.
- Supplied `tree_sha` and `last_change_seq` now narrow exact approval reuse. `@types/node >=22` is declared as
  a peer because the public declarations use `NodeJS.*`.
- Primary operation errors are preserved when Herdr, named-check, Git-index, or init cleanup also fails. Every
  finalizer still runs and its failures remain visible; an attached Herdr close failure still retains the
  workspace writer lease.
- CI runs the model-free, Herdr-free real-pi integration tier.

## 0.21.1 — twenty-minute default child timeout (2026-09-02)

- Raise the default per-child wall-clock limit from 600 to 1200 seconds. Explicit
  `PI_GRANTS_CHILD_TIMEOUT` values remain unchanged and inherited by descendants.

## 0.21.0 — one project init enables its ledger (2026-09-02)

- `/grants init` now atomically persists one project choice containing both the capability grant and default
  `.pi/grants.jsonl`, then adopts both after that write succeeds. Future plain `pi` starts load both, so
  `/grants ledger` and `/grants dashboard` need no repeated shell export.
- Merely installing pi-daddy still initializes nothing. `PI_GRANTS_GRANT` bypasses the project store for
  children/CI, and `PI_GRANTS_LEDGER` overrides the stored default, including `""` for a one-run opt-out.
- Existing version-1 grant stores retain their no-ledger behavior until the operator explicitly reruns
  `/grants init`; an upgrade never converts prior governance consent into recording consent.
- Newly generated `.pi/grants.env` files export the project ledger. Existing reviewed files remain untouched.
- **First-review repair:** remember whether the ledger came from the environment before publishing a stored
  default, so a later init after a cwd change can replace pi-daddy's own old path without overriding a real
  operator value. A second review made v1 lookalike `projectLedger` fields an explicit inert case after its
  mutation survived the first tests. Review also exposed pre-existing malformed-store fail-open behavior
  (R-175); it remains open and is not presented as fixed by this release.
- **Final-quality repair:** installed smoke now matches the active ledger export as a complete line; the old
  commented `#export` contained the previous substring and falsely passed.

## 0.20.1 — open the dashboard with Herdr's split-pane contract (2026-09-01)

- Fix `/grants dashboard` on Herdr 0.8+: a split plugin pane targets the verified caller pane and must not
  also carry the mutually exclusive workspace selector. Released 0.20.0 sent both, so Herdr refused every
  first open with `invalid_params`. Host verification and the returned workspace/tab check are unchanged.

## 0.20.0 — live governance dashboard and ledger v3 (2026-08-31)

- Ship a Herdr 0.8+ plugin inside the trusted pi-daddy package. A Herdr-hosted pi asks once before linking it;
  **Install and open** is the only choice that installs anything. **Not now** and **Never ask** are persisted.
- Add `/grants dashboard`: verify this exact pi PID is in its declared Herdr pane, require a configured ledger,
  diagnose missing/disabled/incompatible plugin state, then open or reuse a right split with `--no-focus`.
- Add a pure `ledger -> projection -> terminal renderer` path. Active ancestry stays visible, old completed
  subtrees collapse, durations and Herdr pane identity render live, and corrupt lines are reported without
  showing raw content or modifying the file.
- **BREAKING — production events now use the closed ledger v3 contract.** Every governed occurrence has a
  globally unique `executionId` and explicit `parentExecutionId`; readable `childId` remains and may repeat.
  Lifecycle/lease joins by `childId` are invalid. Frozen v2 and legacy lines remain readable but v2 lifecycle
  is shown historical/unjoined rather than guessed.
- Lifecycle adds `running`, `deadlineAt`, and optional `herdrPaneId`/`herdrAgentName`, so an abandoned start
  becomes incomplete and a live Herdr child can be focused by identity.
- Add identifier-only workflow facts with explicit `planned`, `observed`, or `controller_validated`
  provenance. Enforced children remain a separate event class. Principal runs receive labels through existing
  correlation fields; no workflow prompt prose is parsed.
- Add packaged v3 schema and generated fixtures while preserving the v2 artifact paths unchanged.
- **First-review hardening:** keep routed descendants on one absolute ledger; consolidate mixed chain gates
  into one decision per execution; share strict runtime v3 validation across both readers; runtime-check
  workflow vocabularies; isolate display callbacks from child execution; verify plugin provenance before
  enabled state; and persist only literal installation choices.
- **Second-review hardening:** remove raw corrupt bytes from the canonical `/grants ledger` report; recheck
  workspace/tab on reuse and returned open identity; validate every nested pane-state entry; reject prose in
  v3 display/capability fields and strip all Unicode control/format characters; share one absolute child
  deadline and running-before-terminal append order; and make public builders assert the exact closed wire.
- **Third-review hardening:** pane reuse keys exactly on workspace/tab/ledger rather than invocation `cwd`, and
  the workflow-fact builder joins every other public v3 builder at the final closed-wire assertion.
- **Whole-change-review hardening:** reject stored pane entries whose workspace/tab/ledger disagrees with their
  key, and validate explicit v2 against the frozen schema before presenting it as historical/unjoinable.
- **Critical-retry hardening:** align all seven v3 schema timestamp sites with runtime's seconds `00`–`59`
  profile; reject a running lifecycle event that changes its occurrence deadline; reserve SIGTERM grace inside
  the remaining process budget and enforce the recorded hard deadline with an independent SIGKILL timer.
- **Deadline race repairs:** soft and hard deadline callbacks allow pending child exit delivery one event-loop
  turn before acting, then refuse to rewrite a completed PID as timed out merely because descendant pipes delay
  `close`. A genuinely live PID remains subject to SIGTERM and absolute-deadline SIGKILL. Soft and hard routes
  each have their own child-synchronized regression and mutation. The soft timer begins after a real ready
  marker; the hard route uses a detached Linux process-state observer to establish OS exit before overdue timer
  delivery. Tests use `Atomics.wait` rather than scheduler assumptions or busy-spinning, while a direct
  request-mutation guard forces production to snapshot the hard deadline before spawn. The Linux observer is
  independently bounded and accepts only zombie state or `ENOENT` as exit. The hard proof requires that exit
  before a future recorded epoch, then delays controller delivery across it; observer status is published by
  newline-terminated complete-status polling. An unexported AsyncLocalStorage test clock establishes the proof
  epoch only after readiness without weakening public `runChild` snapshot semantics; synchronization searches
  across arbitrary output chunk boundaries before retaining a bounded overlap suffix. Dedicated mutations
  force internal-control consultation, cross-chunk matching and complete terminal-status grammar. Deterministic
  status tests cover partial/complete writes, timeout, observer error, transient/permanent reads and expiry;
  same-process control tests use a two-party barrier for real controlled/uncontrolled overlap and also cover
  nesting, rejection and post-settlement cleanup. Polling has an independent attempt ceiling; AsyncLocalStorage
  ownership lives only under `test/`; build cleans stale `dist/` first and installed smoke refuses the former
  artifact, leaving no control module in the published package.
- **BREAKING — `LedgerReport.corrupt[]` now exposes `{line, reason}` rather than `{line, text}`.** Raw corrupt
  bytes can contain task/output material and are no longer retained by the public integrity result. Consumers
  that need forensic bytes must read the ledger itself at the reported line; pi-daddy never repairs it.
- **BREAKING — correlation fields rendered as labels/IDs now require the ASCII identifier grammar
  `[A-Za-z0-9@*][A-Za-z0-9@*._:/-]{0,127}`.** Replace spaces with `-` or `_` before delegating. This prevents a
  model-facing `policy_label`/phase/run ID from becoming a free-text task/output channel into the append-only
  ledger and terminal. Other bounded correlation strings remain unchanged. A top-level
  `assurance_scope: null` is normalized away and is not a valid v3 wire value; omit it instead.
- **BREAKING — named check IDs must use the same ASCII identifier alphabet and be at most 460 characters.**
  Rename prose/whitespace keys before calling `runNamedCheck`; invalid IDs now refuse before the executable
  or lease starts rather than failing only when a v3 receipt is appended.


## 0.19.0 — workspace routing is a capability (2026-08-23)

**The four hang fixes below were staged for a 0.18.2 and are released here instead — a decision reversed
deliberately, so the reversal is recorded rather than the reason vanishing.** The paragraph that stood here
argued for cutting them as a patch so a fix for released code would not be gated on a breaking change. The
counter-argument won: **R-131 — workspace routing does not attenuate — is a live escalation in published
0.18.0 and 0.18.1**, a child routed to `staging` can route its grandchild to `prod` with a real lease and a
ledger line that reads as authorised, and the fix for it *is* the breaking change. A 0.18.2 would have fixed
one defect and left that one open, on a line that then needed supporting alongside this one. Shipping once
fixes all three.

- **FIX — a retained lease no longer reports a retention that did not happen (R-152).** `markRetained`
  returned `void` and `releaseDelegationWorkspace` hardcoded the ledger word, so a `workspace_lease` event
  said `retained` — *"the pane may still be live"* — for a helper that had already died (the fact is `lost`),
  for a lease already cleanly released, and for a retention whose record could not be written. It now answers
  in the release vocabulary and the caller ledgers what it says. **Breaking for direct library callers only:**
  `WorkspaceLease.markRetained` returns `Promise<LeaseReleaseOutcome>` instead of `Promise<void>`, and
  `LeaseReleaseOutcome` gained `retained` (so `| "retained"` unions are now redundant, not wrong).
- **FIX — an impossible `herdr tab close` bound is rejected at both ends, and as a `RangeError` (R-152).**
  Above `2^31 - 1` the timeout truncates — `Number.MAX_SAFE_INTEGER` becomes 1ms and SIGKILLs every close
  attempt before herdr can act (measured: callback at 3ms for a 3s sleep) — the mirror of the `0` case, which
  means no bound at all. Fractional counts were accepted too. **The exception type changed on purpose:** this
  was a `GovernanceRefusal` carrying `WORKSPACE_LEASE_STALE`, which elsewhere means the lease went stale, so a
  controller switching on codes would retry a permanent caller bug. Both bounds are now validated for read
  leases as well, where the check previously sat below an early return and validated nothing.
- **FIX — a retained writer lease no longer stops its own process from exiting (R-146).** `markRetained`
  leaves the kernel lock and the pane alone by design; it also left the parent's three pipes to the lock
  helper referenced, and the helper was never `unref`ed, so node's event loop stayed alive and `pi` could
  never exit. Measured: `exit=124` (timed out) against `exit=0` for the same sequence ending in `release()`.
  Reached whenever the herdr executor's `tab close` fails. **Which hosts it wedged:** those that let the loop
  drain — pi's `-p`/print mode, and library consumers such as an ADR-0034 external controller. Interactive and
  rpc mode call `process.exit()`, so there the process still left and the on-exit pane sweep still ran.
  Present in 0.18.0 and 0.18.1. Retention still holds the lock: on the parent's exit the helper makes its
  bounded close attempts and then releases, so the worktree is recoverable (R-102) rather than stranded.
- **FIX — one `herdr tab close` attempt is now bounded in wall clock, not only in count (R-146).** The
  helper's `execFile` had no `timeout`, so a herdr that accepted the close and never answered never called
  back: the retry budget was unreachable, no marker was written, and the lock was held **forever** — R-102's
  explicitly rejected outcome. Measured with a `herdr` that sleeps: before, `LOCK=HELD` with no marker
  indefinitely; after, released with the marker written. Tunable via `herdrCloseTimeoutMs` (default 15s).


- Ship a canonical JSON Schema draft 2020-12 contract for `ledgerVersion: 2` plus deterministic fixtures for
  all four event types, generated through the production builders. Stable package export paths let external
  harnesses pin the real contract instead of maintaining a parallel format.
- Add a check-receipt event builder and use it on the production named-check path, so every checked-in fixture
  is produced by the same builder that emits the corresponding ledger line.
- Document strict version dispatch: legacy 0.17 grant records have no explicit version; unsupported explicit
  versions fail closed and are never reinterpreted as legacy.

- **BREAKING — routing a child to a registered workspace now requires `workspace:<id>` in the caller's
  grant.** Every grant that routes must add it; a delegation naming a workspace the session does not hold
  is refused `WORKSPACE_NOT_AUTHORIZED`, with the id recorded in `denied` so it counts as an escalation.

  Until now this was the one governance dimension that did **not** attenuate: the registry inherited into
  every governed child and nothing checked the caller's authority, so a child routed to `staging` could
  route its grandchild to `prod` — with a real lease, a validated CWD, and a ledger line naming `prod`.
  Measured in `docs/probes/g36-workspace-attenuation`, decided in ADR-0035, tracked as R-131.

  Failing open for compatibility was considered and rejected: it would have made an attenuation fix opt-in.
  The migration is one line per grant, and `pi-daddy init` now lists the registered ids commented in
  `.pi/grants.env` so the edit is visible from the file you already open.

- `PI_GRANTS_GATED=workspace:prod` asks a human before a child is routed there, through ADR-0024's existing
  mechanism — the id is the caller's authority for that one delegation and never joins the child's grant.
  `workspace:*` in the gate covers every id.
- `workspace:*` covers the namespace but is **held, never inherited** — R-26's rule, because a descendant
  holding it could route anywhere the registry lists. `agent:*` is unchanged and still inherits. Asking to
  hand `workspace:*` to a child is refused (`NARROWING_VIOLATED`) rather than silently stripped, so the
  ledger never records authority the child did not receive.
- `tool:*` still satisfies a workspace capability: governance is opt-in and an ungoverned session must keep
  routing anywhere.
- A `workspace:` id never reaches pi's `--tools`. It is enforced by pi-daddy before the spawn, which is a
  different and weaker class than the `--tools` allowlist; `docs/SPEC.md` now states both classes explicitly.
- **BREAKING — a registry id must now match `[A-Za-z0-9][A-Za-z0-9._/-]*`.** The registry became an input to
  the grant grammar when an id became the tail of a capability id, so ids that 0.18.0/0.18.1 accepted are now
  refused `GRANT_ID_MALFORMED` at load, naming the file and the id. **Slashes and dots are fine** — a worktree
  named after its branch (`feature/x`) works, and an earlier build of this release wrongly refused it by
  reusing the tool-name grammar. Refused: whitespace (it splits `allowed-tools`), commas and newlines (they
  split a grant — 0.18.1's defect), `*` (it collided with `workspace:*`, so registering a worktree as `*` and
  granting `workspace:*` minted routing over the whole registry), shell metacharacters (they reach the
  `ROUTABLE WORKSPACES` block of a generated `.pi/grants.env`, which tells you to paste them into your
  grant), and non-ASCII (the generated file is reviewed in an editor, where control characters and
  homoglyphs let one id render as another). One bad entry refuses the whole file, so rename before upgrading.
- **The registry must be a regular file under 1 MiB** — a FIFO there blocked session start indefinitely, and
  the read is bounded by one handle `fstat`-ed as a descriptor. **Ownership and mode are NOT checked.** An
  earlier draft of this release added a uid/world-writable guard and this bullet promised it; `e1937cf`
  removed the code when the change was narrowed to ADR-0035 and left the promise here for a day. Nothing in
  0.19.0 checks who may write the registry, and a mode check would not reach the attack that matters — a
  governed child runs as the same uid as its parent. Tracked as R-137.
- No `workspace:` id is live by default in a generated grant, including one a package's `allowed-tools`
  declares. Which worktree a child starts in is the operator's decision (ADR-0028).

### Fixed before release — found reviewing this change

The first group never shipped — they were defects in 0.19.0's own development, caught by two review passes
and a mutation battery, and are listed because ADR-0035 claimed three of them as done (R-133, and that ADR's
amendment). **The entries under "Present in earlier releases" below DID ship**, and an earlier draft of this section put
them under this heading, telling operators the `tool:*` attenuation escape could not affect them. (That draft
then said "two" while three bullets sat under the heading, one of which — the v2 enum — is the single item
here that provably did *not* ship. It has moved back.)

- **Routing terminated below the root instead of attenuating.** `unknownCapabilities` did not know the
  namespace, and a catalog is always present in a real session, so every requested `workspace:<id>` was
  refused as an unknown capability. No child could be granted one, which made the "two authorities" model
  unreachable. This was the headline defect.
- The `PI_GRANTS_GATED=workspace:<id>` gate above was claimed and inert.
- `pi-daddy init` had never heard of the workspace registry.
- `allowed-tools: workspace:prod` in a `SKILL.md` became `tool:workspace:prod`, which names nothing.
- `isSafeCapability` rejected the namespace, so the boundary that generates grants could not emit the
  capability this release makes mandatory.
- `subsumedBy` reported `workspace:*`-covered ids as subsumed, contradicting its own rule.
- The v2 ledger contract's `refusalCode` enum is now **generated** from `REFUSAL_CODES` by
  `scripts/generate-ledger-v2-contract.ts` instead of hand-maintained beside it. `WORKSPACE_NOT_AUTHORIZED`
  joins the enum in this release; `contracts/ledger/v2/README.md` records why that is a legitimate v2 edit
  rather than a v3 — v2 has never been published, so nothing can have pinned it. It is **not** the last such
  edit: every other closed enum in that schema is still hand-maintained beside its source array — `test/ledger-contract.test.ts`
  asserts each equality, so the same ambush waits for whoever adds an executor kind or a lease outcome. (First
  written as "five"; the figure is larger and is not worth restating, since the test file is the list.)

### Present in earlier releases — read these before upgrading

**Upgrade if you run governed delegation trees, and read R-135 first.**

- **R-135, and it is not part of this feature.** `tool:*` was reaching delegated children. R-26's rule —
  a wildcard is held, never inherited — was enforced only in `childEnv`, the interceptor path, while
  `delegate.ts` (the path that spawns, since 0.7.0 — ADR-0016) applied no filter, and `tool:*` is not universal enough
  for `assertNarrowing` to stop. A parent holding `tool:*` and delegating `tools: ["tool:*"]` gave its child
  `tool:*`, so attenuation ended at the root. **Present in every published version.** One shared
  `inheritableGrant` now serves both paths. If you run governed delegation trees, this is the entry to read.

- **R-134.** Session start no longer warns that a gated `agent:` id "does NOT gate spawning that definition".
  That was R-47's partial fix in 0.11.1 and false from 0.12.0, when ADR-0024's gate landed. The warning
  outlived the defect across eight published versions (0.13.0 through 0.18.1), and the integration suite
  required it to, while advising operators to
  remove a control that works.

## 0.18.1 — SECURITY: a capability id containing a comma minted authority

**Upgrade if you use `tool:*` or `agent:*` in any grant.** Present in 0.18.0 and every earlier published
version that has the wildcard prefix rules.

`PI_GRANTS_GRANT` is comma-separated. A capability id containing a comma was admitted by a wildcard's
**prefix** rule — `agent:x,tool:bash` starts with `agent:`, so a root holding `agent:*` covered it — then
written verbatim into the child's grant and split by the child into two capabilities. The child received a
real `tool:bash` from a tree whose root never held it, `denied` was empty, so nothing recorded an
escalation and the ledger line read as an ordinary authorised delegation.

`tool:*` is affected identically, since it covers every namespace.

Fixed with two guards: a malformed id can no longer be **granted** (it lands in `denied` and is recorded as
the escalation attempt it is), and neither grant writer will **emit** one. No legitimate id is affected —
`ext:@scope/pkg/tool`, `skill:my-skill`, `agent:my_agent` and both wildcards are unchanged. Tracked as
R-132.

## 0.18.0 — generic runtime enforcement for external controllers

- Optional correlation metadata joins capability decisions to run/task/workspace/context IDs, candidate
  `tree_sha`, sequence floors and opaque policy metadata. Trusted task/definition/request/effective digests
  are computed separately; supplied IDs and digests never grant authority.
- Correlated gated approvals bind the exact definition, task, requested/effective capabilities,
  workspace/context and parent. They do not inherit. Legacy calls without correlation keep their existing
  approval behavior. Persisted approvals retain the 30-day expiry; `once` remains one-use. Concurrent exact
  scopes never share a dialog or rebind one human answer to another task.
- Operator-registered Git worktrees can be selected by ID. The child starts at the validated canonical root;
  a kernel `flock` permits at most one pi-daddy-governed writer per canonical root. Caller `read` cannot
  lower write-capable grants; parent death stops the attached process/herdr tab before recovery. This is
  coordination, **not path confinement** and not exclusion of unrelated writers or detached bash children.
- Stable refusal codes accompany existing human diagnostics.
- `pi-daddy/check-runner` runs operator-named executable+argv definitions without shell interpolation,
  with an environment allowlist, timeout/output bounds, privately staged exact executable bytes, and
  pre/post-verified Git HEAD/candidate-tree receipts under an exclusive coordination lease. Arbitrary
  executables remain arbitrary code; no filesystem/network sandbox is claimed.
- **BREAKING — ledger format v2:** append-only capability, workspace-lease, child-lifecycle and
  check-receipt events. The new reader accepts legacy grant lines. Three things a consumer must know, two of
  which are compile-or-run breaks rather than advisories:
  - External readers that assume every line is a grant record must **switch on `event`** first.
  - `LedgerReport` (published via `pi-daddy/ledger`) gained **required** fields — `events`,
    `workspaceLeases`, `lifecycle`. Any TypeScript consumer that constructs one, or handles it
    exhaustively, breaks at compile time.
  - `report.records` **changed meaning**: it counted every parsed line and now counts capability decisions
    only, with lease/lifecycle/receipt lines going to `events`. A consumer reading it as "ledger lines"
    silently gets a different number.
  - A line carrying `ledgerVersion` is now **rejected as corrupt** unless it satisfies the full v2 field
    set, which matters to anyone who hand-writes or re-emits ledger lines.
- **BREAKING — `correlation` is a whitelist.** Only the pinned schema 1.0 field set is accepted; strings are
  capped at 512 characters, `assurance_scope` at 4 KB, and an **undeclared key is refused by name** rather
  than passed through. A caller sending extra fields must stop. This is a privacy control: `correlation` is
  model-facing and copied verbatim onto every append-only event, so an unbounded object was a channel for
  writing arbitrary text into a file that carries no prompts, arguments or results.
- **BREAKING for direct API callers — `WorkspaceLease.release()` returns
  `"released" | "released-unrecorded" | "lost"` and no longer throws**, and `recovered` is
  `boolean | "unknown"`. `WorkspaceLeaseOutcome` gains `uncontended`, `released-unrecorded`, `lost` and
  `retained`. Code that relied on `release()` rejecting must read the value instead.
- **Fixes from a six-reviewer pass over this candidate before release.** The capability invariant held on
  every path any reviewer could construct; these are the runtime half. `flock`'s command inherits the lock
  file descriptor (measured — `docs/probes/g35-flock-fd-inheritance`), so teardown killing only the wrapper
  stranded the lock and reported it to everyone else as a conflict. `recovered` could assert a crash after a
  clean handover and hide a real one. The herdr close loop retried forever while holding the lock. A bound
  approval could be spent outside the workspace it named, because two of its six identity components were
  read from caller-supplied correlation. A refused delegation could leave a 30-day approval on disk. A child
  could mint the upstream controller's `BLOCKED_CRITICAL_ASSURANCE` verdict out of a timeout. Fan-out
  discarded every sibling error but the first. Five planner refusals and every execution-phase failure
  carried no stable code. See `docs/03-risks.md` R-99…R-118 and the ADR-0034 amendment, which also lists
  what the pass did **not** resolve.

New public subpaths: `pi-daddy/correlation`, `pi-daddy/refusals`, `pi-daddy/workspace`, and
`pi-daddy/check-runner`.

## 0.17.1 — no functional change: the release that exists because a rule got enforced

**Nothing in the shipped code changed.** This tarball differs from 0.17.0 in its version and in this file,
and that is stated here rather than dressed up, because a changelog that implies behaviour changed is worse
than a version nobody needed. `dist/`, `src/` and `extensions/` are byte-identical to 0.17.0.

What happened is in the repository, not the package: working rule 10 — **`main` is only ever advanced by
merging a pull request** — plus `hooks/pre-commit`, which refuses a commit on `main` and names a recovery that
is never a force-push, and `test/branch-guard.test.ts`, which pins it against seven mutations of the hook
(tests are not published, so that file is not in this tarball either).

**Why a version at all**, since nothing here changed: rule 10 says version bumps take the same path as
everything else, and this one did — branch, PR, review, squash-merge. The rule's first draft was reviewed by
six independent agents across two rounds that found sixteen defects, including that *"never commit to `main`"*
forbids the merge the rule requires, and that the guard written to fix that had reproduced the same defect in
shell. `docs/03-risks.md` R-85 and the 2026-08-18 session-log entry hold the details.

**Also fixed here:** this file claimed "Newest first" while 0.14.0 sat above 0.17.0, 0.16.0 and 0.15.0. The
order is now what the sentence says it is.

## 0.17.0 — `delegate_chain`

**Sub-agents in sequence, each seeing the previous one's output** (ADR-0033). The third and last spawn tool, and the
one that removes the last reason to keep an ungoverned spawner installed alongside this one.

- **The handoff is fenced, labelled and nonce-delimited.** A chain makes step N's task the output of a governed
  child, which is the highest-authority text a child receives after its own `SKILL.md`. The label is *framing* — a
  determined injection can argue with it — but the **nonce is mechanism**: minted after the producing child
  finished, so it cannot forge a closing delimiter. ADR-0033 records the stronger option (quarantine to a file the
  next step must `read`) as the prepared answer if framing proves insufficient.
- **Gated upfront: one dialog per capability *and* definition, all before the first step runs.** Approvals arrive
  together instead of interrupting a running pipeline. Not one dialog for everything — an approval is keyed
  `capability@subject`, so a single dialog spanning several definitions would ask about one and spend the answer on
  the rest. ADR-0033 originally specified that, three reviewers found it was a privilege path, and the ADR carries
  the amendment.
- **Declined means nothing runs.** Running only the ungated steps would return a partial result that reads like a
  complete one.
- **A failed step aborts the rest**, and everything completed still comes back.
- **`taskFrom` in the ledger** — which child's output composed this step's task. The question a
  framing-not-enforcement handoff makes worth answering.

**What to do about it:** a chain spends one budget unit per step, so a seven-step pipeline needs
`PI_GRANTS_FANOUT=12` (the default is 8). At most 8 steps.

**Also:** `test-integration/herdr.it.ts` now checks herdr's own contracts against a real server in an isolated
workspace — the gap that hid three shipping defects in 0.16.0, where the unit fake was a *claim* about herdr that
nothing verified.

## 0.16.0 — children you can watch, in panes chosen for you

**If herdr is running, your sub-agents now run in herdr panes without you configuring anything — and the
parent shows what each one is doing while it works.** Two ADRs, shipped together.

### `PI_GRANTS_HERDR` is three-state, and unset now means *probe* (ADR-0031)

| Value | Behaviour |
| :--- | :--- |
| unset | Probe once at session start (`herdr tab list`, 2s bound). A server that **answers** ⇒ herdr panes; anything else ⇒ captured subprocess. |
| `1` | Demand herdr. **Every delegation refuses** if it is unreachable — no fallback. |
| `0` | Demand the captured subprocess. No probe. |

**Not a `PATH` check.** A binary with no server behind it would make every delegation fail at `tab create`, on
a path nobody chose — so only a *reachable* server counts. This reverses part of ADR-0016 point 6, which
refused auto-detection on the grounds that a run must not "silently relocate"; the answer to *silently* is that
the executor is now named at session start, in `/grants`, and per child in the ledger.

**What to do about it:** nothing, unless you relied on an unset variable meaning subprocesses. If you did, set
`PI_GRANTS_HERDR=0`.

A stale `PI_GRANTS_HERDR=1` in a shell profile still breaks delegation on a machine without herdr — **as it did in
0.15.0**, where every child failed at `tab create`. There was never a fallback to lose. What changed is that the
failure is reported at session start, names the variable, and says what to set instead, rather than surfacing as a
per-delegation spawn error.

### A running delegation is visible (ADR-0032)

Both tools discarded pi's `onUpdate`, so a delegation showed the bare word `delegate` from the call until the
result — up to ten minutes, and the same one word for all eight children of a `delegate_all`. Now there is one
status block per call, redrawn in place, with a three-line tail per child and its herdr agent and pane id:

```
2 children · herdr panes

review   agent review-d0.1   pane w7:t12   running  0:42
  3 findings so far: unchecked nil at
  session.ts:88, missing expiry compare…
```

Both executors stream. The block is a **display, never the result** — the answer is still what the child
returned.

### Panes live until you get your prompt back

A pane used to be destroyed the instant its child settled, so a twenty-second child's pane was gone before
anyone could switch to it. Panes now belong to the **agent run** and are swept at `agent_settled`, capped at 8
at once, with process `exit` as the backstop. `PI_GRANTS_HERDR_KEEP_PANE=1` still means *not even then*.

A child's pane also defaults to the **parent's own herdr workspace** now, so switching to one is a tab away
rather than a workspace away.

### Fixed before release — eighteen defects from six independent reviewers

Everything above was implemented, then attacked by six reviewers with one written hypothesis each. Two were
shipping blockers, and they change what the herdr path guarantees:

- **`herdr agent stop` does not exist.** Measured against herdr 0.7.5. Three call sites issued it for nothing, and
  `docs/probes/g16-herdr` asserted it worked — from a rerun block that was never run. Closing the tab is the only
  kill herdr offers, so an unsettled child now loses its tab at once; leaving it would have left a governed child
  working with its grant after its result was reported.
- **herdr binds an agent name to its tab.** With panes outliving their calls, the **second `delegate` of every
  turn** failed with `agent_name_taken`. Names are now unique per spawn.
- **The 8-pane cap killed live siblings**, because pi runs tool calls in parallel by default. Only *settled* panes
  are reclaimable now; if they are all live the cap yields rather than enforcing.
- **The pane reader amplified output 89,000×** once a pane scrolled or passed the output cap: `agent read` returns
  a snapshot of a bounded terminal and was being diffed as an append-only stream.
- **The output cap counted bytes but truncated by UTF-16 code units** — 2048 bytes through the 1024 default on
  non-ASCII — and a multi-byte character split across a pipe boundary became U+FFFD **in the child's answer**.
  Both pre-existing, both fixed.
- **A failed pane read came back as the child's successful answer**, and a truncated pane never said so.
- **The session-start executor line never reached pi's TUI**, because consecutive `info` notifies overwrite each
  other — which also means `holding [...]` has been silently overwritten since the spawnable summary was added.
  All info lines are now one message.
- **The approval dialog ran before the refusal**, so a demanded-but-unreachable herdr could bank a 30-day approval
  for a delegation it then refused.

Six tests that could not fail were rewritten, each re-verified by re-applying the mutation that had defeated it,
and coverage was added where it was simply absent — including that nothing verified a real spawn records
`executor: "herdr"`. 461 unit tests, up from 442.

### Also

- **`/grants ledger` tallies executors**, so "which children ran in panes?" no longer needs `jq`.
- **The tripwire names `delegate_all`.** It said only *"Use `delegate` instead"*, and a request for parallel
  work was answered with a single sequential call as a result.
- **The ledger records `executor` per child** — required, not optional. The executor is decided by a probe now,
  so nothing outside the record preserves which one ran, and the two paths do not produce the same argv.
- **R-62 re-rated L×L → M×L.** Its "low severity" rested on the herdr executor being opt-in, which is no
  longer true. The failure is unchanged; how often anyone meets it is not.

## 0.15.0 — `/grants init`, a grant that survives without an env var, and a setup that was wrong

**Setup is two steps instead of five, and one of the five was wrong.**

```bash
pi install npm:pi-daddy
pi install npm:principal-pi-skills
pi
/grants init
```

- **`pi install`, not `npm install`.** This package's own documentation said `npm install`, and that is not
  how pi packages are installed: `pi install` registers the package in pi's settings, which is what makes
  the extension auto-load, and it installs to `$PI_CODING_AGENT_DIR/npm/node_modules` rather than the
  project. **`init` searched only the project and so found nothing for anyone who followed pi's own
  instructions** — telling them to install a package they had just installed. It now searches both roots,
  project first so a pinned copy outranks the machine-wide one.
- **`/grants init`** scaffolds definitions, asks about the capabilities that can change your machine, and
  applies the answer to the session you are already in — **no `source`, no restart**.
- **A grant can now come from a store**, at `$PI_CODING_AGENT_DIR/grants/<project>.json` — **outside** your
  project, because a ceiling a child holding `tool:write` could rewrite is not a ceiling. `PI_GRANTS_GRANT`
  **always wins**: it is how a child is governed and how CI is configured. `.pi/grants.env` is still written
  and still worth committing — it is the reviewable record, no longer what the enforcer reads. (ADR-0030)
- **The dialog does not ask questions whose answers cannot matter.** `tool:bash` subsumes `write` and
  `edit`, so once bash is granted those are conferred; asking anyway let an operator answer *no* to
  `tool:write` and then watch `build` be allowed it. It now reports them as already conferred instead.
- **An unknown `/grants` subcommand is refused rather than ignored.** `/grants init` used to print the
  ordinary status screen with the word silently dropped, so a command that did not exist looked like it had
  worked.
- **`init` counted authorisations and called them declarations** — "7 skill(s), 3 declaring allowed-tools"
  when all seven declared.

## 0.14.0 — `pi-daddy init`, and a startup line that names what it will and will not spawn

> **Reviewed before release by five independent agents, one hypothesis each. All five found something, and
> nine defects were fixed here** — including one that executed arbitrary code from the file this feature
> tells you to commit (R-78), one that overwrote an operator's file and wrote through symlinks (R-79), and
> one where the startup line blamed your `SKILL.md` files for a misconfigured environment variable (R-81).
> **ADR-0029 came out of that review**: the generated grant is read-only by default. The reasoning for each
> is in `docs/03-risks.md` R-78 through R-82.

The pi-daddy half of making this package and a package of skills work together out of the box
(`docs/HANDOFF-principal-pi-skills-integration.md`, items B1/B2/B4; **ADR-0028**). Nothing about grant
resolution, enforcement, approvals or the ledger changed — this is the part before and around them.

- **The generated grant is READ-ONLY by default** (ADR-0029). `init` grants what the copied skills declare
  minus anything that can change your machine — `bash`, `write`, `edit` and the universal capabilities are
  written **commented**, naming the definitions that need them. `init` + `source` gives a working read-only
  setup; widening costs one deliberate uncomment. The reason: `PI_GRANTS_GRANT` is what *bounds* a declared
  ceiling, so generating it from those ceilings would give the bound and the bounded one author, and it
  would not be you.

- **`npx pi-daddy init` scaffolds a governed project.** It reads `<cwd>/node_modules` for packages declaring
  skills in their own `package.json` (`"pi": {"skills": [...]}`, pi's convention), copies each declared
  `SKILL.md` into `.pi/skills/`, and writes an annotated `.pi/grants.env`. That replaces, per skill: make a
  directory, copy the body, hand-write frontmatter, choose a capability set with no guidance, and assemble a
  `PI_GRANTS_GRANT` string by hand — seven times for `principal-pi-skills`.

  **It chooses no ceiling, and that boundary is the whole design.** A skill that declares `allowed-tools` is
  copied byte for byte; one that declares none is copied with a *commented* placeholder and stays
  unspawnable until a human fills it in. The placeholder is deliberately not a working example, so
  uncommenting it unedited fails loudly instead of granting something nobody decided. An existing file is
  **kept**, never overwritten — that edit is the capability decision, and a second `init` run is exactly
  when it would be destroyed. `--force` exists and says what it costs.

- **Session start says how many definitions are spawnable, and names the withheld ones.** The line reported
  the grant and never the definitions, so *"governance is working"* and *"did the install fail?"* looked
  identical:

  ```
  grants: 1 of 7 definitions spawnable — review
    withheld: architect, build, … — need agent:architect, …, which this session does not hold
  ```

  Classified by the same planner a real spawn comes through (no human is asked, stored approvals count), and
  it speaks even when **nothing** is spawnable — which is the state most worth being told about. It is an
  upper bound: it runs before the tool surface is observed, and `/grants` is the settled answer.

- **A worked `principal-pi-skills` example in the README**, replacing the invented one, with every line
  produced by running the commands (`docs/probes/b2-init-principal-pi-skills`).

- **Fixed before release: a skill's `allowed-tools` VALUE could execute code from the generated grant file**
  (R-78). `ceilingForDefinition` passes `ext:`/`skill:`/`agent:` entries through as written — correct for
  enforcement, where the catalog refuses what it does not know — so a package declaring
  `allowed-tools: Read,ext:x";touch /tmp/pwned;PI_GRANTS_GRANT="` produced a `.pi/grants.env` that ran the
  payload when sourced: silently, exit 0, with the variable left looking plausible. It survives
  `--ignore-scripts` and travels in the file you commit. Declared ids are whitelisted now, `tool:*`/`agent:*`
  from a package are refused, and the assembled grant is charset-checked before the file is written at all.

- **Fixed before release: `init` overwrote an operator's file and wrote through symlinks** (R-79). The
  presence probe was `readFile`, which treats *unreadable* as *absent* — so a permissions-restricted
  `SKILL.md` was replaced and its ceiling **widened**, with no `--force`. And `writeFile` follows symlinks, so
  a dangling link at a target path created the file outside the project while reporting an in-project path.
  Both are one `open(path, "wx")`. `--force` no longer regenerates `.pi/grants.env`, and `pi-daddy init
  --Force` is no longer accepted as a silent no-op.

- **Fixed before release: the startup line blamed your files for a session-level refusal** (R-81). A session
  at its depth limit, or with a malformed `PI_GRANTS_MAX_DEPTH`, was told its `SKILL.md` files were written
  wrong — two lines above `/grants` saying "delegation is disabled (maxDepth 0)". A session with no
  `tool:delegate`, which has no delegate tool at all, was told definitions were spawnable.

- **Fixed before release: a skill's directory name could write a capability into the generated grant**
  (R-77). A name is interpolated into a comma-separated `PI_GRANTS_GRANT`, into a file the operator sources,
  and into a path. A package shipping a directory called `a,tool:bash` produced
  `PI_GRANTS_GRANT="agent:a,tool:bash,…"` — `tool:bash` in an operator's grant, declared by no definition.
  Names are now whitelisted at discovery and a refusal is printed with its reason.

- **Fixed before release: `npx pi-daddy init` printed nothing and exited 0 for every installed copy** (R-73).
  npm installs a bin as a symlink, so `process.argv[1]` is the link and the entry-point guard compared it
  against the real file's URL. Caught by the smoke test, which now runs the installed bin — the same class of
  defect as the `exports` map that worked in the tree and threw for every consumer, and the second time that
  script has caught it.

## 0.13.0 — the approvals file gets the lock the ledger already had, and two silences end

Closing the last items that were open rather than out of scope, then **red-teaming the result**: an
operator review and four independent agents, each given one hypothesis to attack. Between them they found
eight further defects **in that work**, all repaired here before release. The two worth knowing about
as a user of this package:

- **The file lock admitted two writers into the critical section.** `rm(lockPath)` deletes whatever is at
  the path, not the lock this process created, so a broken stale lock cascaded: the old holder freed the new
  owner's lock on the way out, and the next arrival — which raced nothing — walked in beside it. Reproduced
  across real OS processes. Every lock now carries a token and proves ownership before deleting.
- **The ledger over-claimed human approval under concurrency.** One *Allow for this session* answered under
  a fan-out of eight wrote eight lines each recording `approvalSource: "prompt"`. Riders now record
  `session`, which is what actually happened.

One breaking change, and it is a type.

- **BREAKING: `revokeApproval` returns `"revoked" | "absent" | "failed" | "busy"`**, not a boolean. It had
  two outcomes for four facts, and `/grants revoke` printed *"no persisted approval named X"* whenever the
  write failed — telling an operator that the approval they are revoking does not exist **while it is still
  in effect**. The most alarming outcome wore the most reassuring message. `busy` is separate because a lock
  timeout happens *before* the load, so nothing was looked at and nothing may be claimed about the entry.
  Callers switching on the boolean must switch on the string; `"revoked"` is the only success.
- **Approval writes hold a lock (R-49).** Every write is load → modify → write and none of them was
  serialised, so session 1 could load, session 2 could revoke, and session 1's next save would restore the
  revoked entry for the rest of its 30 days. The lock is the ledger's, moved to `src/file-lock.ts` and used
  by both — one implementation, two callers, opposite failure policies: the ledger fails a delegation closed
  when it cannot take the lock, the approvals store never fails your work, because it is a convenience cache.
- **`/grants ledger` counts where each approval came from.** ADR-0020 keeps the persistence layer on an
  asserted fatigue argument and named the evidence that would settle it — `persisted` against `prompt` over
  real use. The data was already recorded and nothing read it. **Two numbers, both labelled**: raw records
  are an *upper bound* on prompts avoided, because within one session only the first would have been a
  prompt and the rest come from the in-memory session cache; distinct `capability@subject` pairs are the
  closer estimate. Reporting records alone overstated the layer twentyfold in the obvious case (R-63).
  Records written before per-capability sources existed are reported as *not counted* rather than folded in,
  because that older scalar over-claimed `prompt`.
- **A revoke is documented as taking effect at the next gate check**, not "immediately". A spawn whose gate
  check already passed is not retracted by a revoke arriving microseconds later — inherent to revoking
  anything, and no lock closes it.
- **An unreadable ledger no longer silences session start (R-60).** `verifyLedger` rethrows on anything but
  a missing file, and that call sat inside an empty catch — so a `PI_GRANTS_LEDGER` pointing at a directory
  produced no alarm, no warning, and not even the line saying governance was on.
- **Herdr panes opened by a killed process are closed at exit.** Coverage is exact and documented in
  `src/pane-reaper.ts`: normal exit and `process.exit()` are covered; SIGKILL is not, and neither is a
  SIGTERM nothing else in the process is listening for. **No signal handler is installed**, deliberately —
  one here would suppress Node's default termination and turn pi's "interrupt this turn" into "exit pi".

---

Versions before 0.13.0 (never published) were moved to `../../docs/archive/CHANGELOG-prepublication.md` (repository `docs/archive/`) on 2026-09-21.
