pi-daddy 0.18.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +136 -23
  3. package/contracts/ledger/v2/README.md +59 -0
  4. package/contracts/ledger/v2/fixtures/capability-decision.json +95 -0
  5. package/contracts/ledger/v2/fixtures/check-receipt.json +40 -0
  6. package/contracts/ledger/v2/fixtures/child-lifecycle.json +42 -0
  7. package/contracts/ledger/v2/fixtures/workspace-lease.json +41 -0
  8. package/contracts/ledger/v2/ledger-event.schema.json +633 -0
  9. package/contracts/ledger/v3/README.md +36 -0
  10. package/contracts/ledger/v3/fixtures/capability-decision.json +98 -0
  11. package/contracts/ledger/v3/fixtures/check-receipt.json +42 -0
  12. package/contracts/ledger/v3/fixtures/child-lifecycle.json +45 -0
  13. package/contracts/ledger/v3/fixtures/workflow-fact.json +41 -0
  14. package/contracts/ledger/v3/fixtures/workspace-lease.json +43 -0
  15. package/contracts/ledger/v3/ledger-event.schema.json +930 -0
  16. package/dist/approval-prompt.d.ts +2 -1
  17. package/dist/approval-prompt.d.ts.map +1 -1
  18. package/dist/approval-prompt.js +9 -0
  19. package/dist/approval-prompt.js.map +1 -1
  20. package/dist/approval.d.ts +4 -2
  21. package/dist/approval.d.ts.map +1 -1
  22. package/dist/approval.js +4 -0
  23. package/dist/approval.js.map +1 -1
  24. package/dist/capabilities.d.ts +90 -0
  25. package/dist/capabilities.d.ts.map +1 -1
  26. package/dist/capabilities.js +114 -3
  27. package/dist/capabilities.js.map +1 -1
  28. package/dist/catalog.d.ts +15 -1
  29. package/dist/catalog.d.ts.map +1 -1
  30. package/dist/catalog.js +54 -3
  31. package/dist/catalog.js.map +1 -1
  32. package/dist/check-runner.d.ts.map +1 -1
  33. package/dist/check-runner.js +19 -7
  34. package/dist/check-runner.js.map +1 -1
  35. package/dist/cli.d.ts.map +1 -1
  36. package/dist/cli.js +21 -1
  37. package/dist/cli.js.map +1 -1
  38. package/dist/correlation.d.ts.map +1 -1
  39. package/dist/correlation.js +10 -0
  40. package/dist/correlation.js.map +1 -1
  41. package/dist/dashboard-cli.d.ts +18 -0
  42. package/dist/dashboard-cli.d.ts.map +1 -0
  43. package/dist/dashboard-cli.js +154 -0
  44. package/dist/dashboard-cli.js.map +1 -0
  45. package/dist/dashboard-handshake.d.ts +37 -0
  46. package/dist/dashboard-handshake.d.ts.map +1 -0
  47. package/dist/dashboard-handshake.js +127 -0
  48. package/dist/dashboard-handshake.js.map +1 -0
  49. package/dist/dashboard-herdr.d.ts +54 -0
  50. package/dist/dashboard-herdr.d.ts.map +1 -0
  51. package/dist/dashboard-herdr.js +286 -0
  52. package/dist/dashboard-herdr.js.map +1 -0
  53. package/dist/dashboard-projection.d.ts +73 -0
  54. package/dist/dashboard-projection.d.ts.map +1 -0
  55. package/dist/dashboard-projection.js +294 -0
  56. package/dist/dashboard-projection.js.map +1 -0
  57. package/dist/dashboard-render.d.ts +10 -0
  58. package/dist/dashboard-render.d.ts.map +1 -0
  59. package/dist/dashboard-render.js +208 -0
  60. package/dist/dashboard-render.js.map +1 -0
  61. package/dist/definitions.d.ts.map +1 -1
  62. package/dist/definitions.js +7 -1
  63. package/dist/definitions.js.map +1 -1
  64. package/dist/delegate-types.d.ts +6 -2
  65. package/dist/delegate-types.d.ts.map +1 -1
  66. package/dist/delegate-types.js.map +1 -1
  67. package/dist/delegate.d.ts.map +1 -1
  68. package/dist/delegate.js +34 -4
  69. package/dist/delegate.js.map +1 -1
  70. package/dist/delegation-approval.d.ts.map +1 -1
  71. package/dist/delegation-approval.js +37 -12
  72. package/dist/delegation-approval.js.map +1 -1
  73. package/dist/execution-id.d.ts +6 -0
  74. package/dist/execution-id.d.ts.map +1 -0
  75. package/dist/execution-id.js +13 -0
  76. package/dist/execution-id.js.map +1 -0
  77. package/dist/executor.d.ts +2 -1
  78. package/dist/executor.d.ts.map +1 -1
  79. package/dist/executor.js +1 -0
  80. package/dist/executor.js.map +1 -1
  81. package/dist/grant-env.d.ts +2 -0
  82. package/dist/grant-env.d.ts.map +1 -1
  83. package/dist/grant-env.js +26 -3
  84. package/dist/grant-env.js.map +1 -1
  85. package/dist/index.d.ts +4 -1
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +4 -1
  88. package/dist/index.js.map +1 -1
  89. package/dist/init.d.ts +13 -1
  90. package/dist/init.d.ts.map +1 -1
  91. package/dist/init.js +35 -2
  92. package/dist/init.js.map +1 -1
  93. package/dist/lease-helper.d.ts +58 -0
  94. package/dist/lease-helper.d.ts.map +1 -0
  95. package/dist/lease-helper.js +94 -0
  96. package/dist/lease-helper.js.map +1 -0
  97. package/dist/lease-record.d.ts +15 -3
  98. package/dist/lease-record.d.ts.map +1 -1
  99. package/dist/lease-record.js.map +1 -1
  100. package/dist/ledger-events.d.ts +59 -15
  101. package/dist/ledger-events.d.ts.map +1 -1
  102. package/dist/ledger-events.js +67 -5
  103. package/dist/ledger-events.js.map +1 -1
  104. package/dist/ledger-identifiers.d.ts +7 -0
  105. package/dist/ledger-identifiers.d.ts.map +1 -0
  106. package/dist/ledger-identifiers.js +20 -0
  107. package/dist/ledger-identifiers.js.map +1 -0
  108. package/dist/ledger-report.d.ts +5 -2
  109. package/dist/ledger-report.d.ts.map +1 -1
  110. package/dist/ledger-report.js +38 -14
  111. package/dist/ledger-report.js.map +1 -1
  112. package/dist/ledger-v3-validation.d.ts +11 -0
  113. package/dist/ledger-v3-validation.d.ts.map +1 -0
  114. package/dist/ledger-v3-validation.js +265 -0
  115. package/dist/ledger-v3-validation.js.map +1 -0
  116. package/dist/ledger.d.ts +27 -9
  117. package/dist/ledger.d.ts.map +1 -1
  118. package/dist/ledger.js +38 -6
  119. package/dist/ledger.js.map +1 -1
  120. package/dist/propagation.d.ts +19 -1
  121. package/dist/propagation.d.ts.map +1 -1
  122. package/dist/propagation.js +23 -1
  123. package/dist/propagation.js.map +1 -1
  124. package/dist/refusals.d.ts +1 -1
  125. package/dist/refusals.d.ts.map +1 -1
  126. package/dist/refusals.js +1 -0
  127. package/dist/refusals.js.map +1 -1
  128. package/dist/resolve.d.ts +10 -0
  129. package/dist/resolve.d.ts.map +1 -1
  130. package/dist/resolve.js +27 -3
  131. package/dist/resolve.js.map +1 -1
  132. package/dist/routing-authority.d.ts +71 -0
  133. package/dist/routing-authority.d.ts.map +1 -0
  134. package/dist/routing-authority.js +100 -0
  135. package/dist/routing-authority.js.map +1 -0
  136. package/dist/run-child.d.ts +3 -1
  137. package/dist/run-child.d.ts.map +1 -1
  138. package/dist/run-child.js +30 -2
  139. package/dist/run-child.js.map +1 -1
  140. package/dist/run-herdr.d.ts +2 -0
  141. package/dist/run-herdr.d.ts.map +1 -1
  142. package/dist/run-herdr.js +8 -0
  143. package/dist/run-herdr.js.map +1 -1
  144. package/dist/skill-packages.d.ts +11 -5
  145. package/dist/skill-packages.d.ts.map +1 -1
  146. package/dist/skill-packages.js +20 -11
  147. package/dist/skill-packages.js.map +1 -1
  148. package/dist/workflow-fact-id.d.ts +4 -0
  149. package/dist/workflow-fact-id.d.ts.map +1 -0
  150. package/dist/workflow-fact-id.js +14 -0
  151. package/dist/workflow-fact-id.js.map +1 -0
  152. package/dist/workflow-facts.d.ts +34 -0
  153. package/dist/workflow-facts.d.ts.map +1 -0
  154. package/dist/workflow-facts.js +43 -0
  155. package/dist/workflow-facts.js.map +1 -0
  156. package/dist/workspace-lease.d.ts +15 -3
  157. package/dist/workspace-lease.d.ts.map +1 -1
  158. package/dist/workspace-lease.js +81 -24
  159. package/dist/workspace-lease.js.map +1 -1
  160. package/dist/workspace.d.ts +25 -0
  161. package/dist/workspace.d.ts.map +1 -1
  162. package/dist/workspace.js +142 -5
  163. package/dist/workspace.js.map +1 -1
  164. package/extensions/chain-ledger.ts +4 -0
  165. package/extensions/chain-plan.ts +98 -0
  166. package/extensions/delegate-chain.ts +63 -122
  167. package/extensions/delegation-ledger.ts +60 -0
  168. package/extensions/delegation.ts +20 -14
  169. package/extensions/execute-child.ts +85 -21
  170. package/extensions/execution-occurrence.ts +20 -0
  171. package/extensions/grants-command.ts +33 -4
  172. package/extensions/grants.ts +50 -1
  173. package/extensions/init-command.ts +33 -2
  174. package/extensions/run-delegation.ts +36 -56
  175. package/extensions/session-report.ts +13 -20
  176. package/extensions/session.ts +16 -3
  177. package/extensions/workspace-runtime.ts +44 -4
  178. package/herdr-plugin/herdr-plugin.toml +18 -0
  179. package/package.json +22 -5
  180. package/src/approval-prompt.ts +2 -1
  181. package/src/approval.ts +4 -2
  182. package/src/capabilities.ts +120 -4
  183. package/src/catalog.ts +62 -4
  184. package/src/check-runner.ts +24 -8
  185. package/src/cli.ts +24 -1
  186. package/src/correlation.ts +11 -0
  187. package/src/dashboard-cli.ts +171 -0
  188. package/src/dashboard-handshake.ts +185 -0
  189. package/src/dashboard-herdr.ts +355 -0
  190. package/src/dashboard-projection.ts +390 -0
  191. package/src/dashboard-render.ts +254 -0
  192. package/src/definitions.ts +7 -1
  193. package/src/delegate-types.ts +6 -2
  194. package/src/delegate.ts +43 -4
  195. package/src/delegation-approval.ts +37 -12
  196. package/src/execution-id.ts +18 -0
  197. package/src/executor.ts +2 -1
  198. package/src/grant-env.ts +39 -7
  199. package/src/index.ts +26 -0
  200. package/src/init.ts +40 -2
  201. package/src/lease-helper.ts +97 -0
  202. package/src/lease-record.ts +15 -3
  203. package/src/ledger-events.ts +116 -28
  204. package/src/ledger-identifiers.ts +21 -0
  205. package/src/ledger-report.ts +40 -19
  206. package/src/ledger-v3-validation.ts +266 -0
  207. package/src/ledger.ts +75 -12
  208. package/src/propagation.ts +24 -1
  209. package/src/refusals.ts +1 -0
  210. package/src/resolve.ts +29 -3
  211. package/src/routing-authority.ts +121 -0
  212. package/src/run-child.ts +35 -3
  213. package/src/run-herdr.ts +9 -0
  214. package/src/skill-packages.ts +20 -13
  215. package/src/workflow-fact-id.ts +16 -0
  216. package/src/workflow-facts.ts +65 -0
  217. package/src/workspace-lease.ts +84 -24
  218. package/src/workspace.ts +179 -6
package/CHANGELOG.md CHANGED
@@ -12,6 +12,207 @@ the record of how the package got here and are worth keeping; they are not worth
12
12
  > the record of how the package arrived at what it does, and because the reasoning behind each one is
13
13
  > usually the clearest statement of why the current behaviour is what it is.
14
14
 
15
+ ## 0.20.0 — live governance dashboard and ledger v3 (2026-08-31)
16
+
17
+ - Ship a Herdr 0.8+ plugin inside the trusted pi-daddy package. A Herdr-hosted pi asks once before linking it;
18
+ **Install and open** is the only choice that installs anything. **Not now** and **Never ask** are persisted.
19
+ - Add `/grants dashboard`: verify this exact pi PID is in its declared Herdr pane, require a configured ledger,
20
+ diagnose missing/disabled/incompatible plugin state, then open or reuse a right split with `--no-focus`.
21
+ - Add a pure `ledger -> projection -> terminal renderer` path. Active ancestry stays visible, old completed
22
+ subtrees collapse, durations and Herdr pane identity render live, and corrupt lines are reported without
23
+ showing raw content or modifying the file.
24
+ - **BREAKING — production events now use the closed ledger v3 contract.** Every governed occurrence has a
25
+ globally unique `executionId` and explicit `parentExecutionId`; readable `childId` remains and may repeat.
26
+ Lifecycle/lease joins by `childId` are invalid. Frozen v2 and legacy lines remain readable but v2 lifecycle
27
+ is shown historical/unjoined rather than guessed.
28
+ - Lifecycle adds `running`, `deadlineAt`, and optional `herdrPaneId`/`herdrAgentName`, so an abandoned start
29
+ becomes incomplete and a live Herdr child can be focused by identity.
30
+ - Add identifier-only workflow facts with explicit `planned`, `observed`, or `controller_validated`
31
+ provenance. Enforced children remain a separate event class. Principal runs receive labels through existing
32
+ correlation fields; no workflow prompt prose is parsed.
33
+ - Add packaged v3 schema and generated fixtures while preserving the v2 artifact paths unchanged.
34
+ - **First-review hardening:** keep routed descendants on one absolute ledger; consolidate mixed chain gates
35
+ into one decision per execution; share strict runtime v3 validation across both readers; runtime-check
36
+ workflow vocabularies; isolate display callbacks from child execution; verify plugin provenance before
37
+ enabled state; and persist only literal installation choices.
38
+ - **Second-review hardening:** remove raw corrupt bytes from the canonical `/grants ledger` report; recheck
39
+ workspace/tab on reuse and returned open identity; validate every nested pane-state entry; reject prose in
40
+ v3 display/capability fields and strip all Unicode control/format characters; share one absolute child
41
+ deadline and running-before-terminal append order; and make public builders assert the exact closed wire.
42
+ - **Third-review hardening:** pane reuse keys exactly on workspace/tab/ledger rather than invocation `cwd`, and
43
+ the workflow-fact builder joins every other public v3 builder at the final closed-wire assertion.
44
+ - **Whole-change-review hardening:** reject stored pane entries whose workspace/tab/ledger disagrees with their
45
+ key, and validate explicit v2 against the frozen schema before presenting it as historical/unjoinable.
46
+ - **Critical-retry hardening:** align all seven v3 schema timestamp sites with runtime's seconds `00`–`59`
47
+ profile; reject a running lifecycle event that changes its occurrence deadline; reserve SIGTERM grace inside
48
+ the remaining process budget and enforce the recorded hard deadline with an independent SIGKILL timer.
49
+ - **Deadline race repairs:** soft and hard deadline callbacks allow pending child exit delivery one event-loop
50
+ turn before acting, then refuse to rewrite a completed PID as timed out merely because descendant pipes delay
51
+ `close`. A genuinely live PID remains subject to SIGTERM and absolute-deadline SIGKILL. Soft and hard routes
52
+ each have their own child-synchronized regression and mutation. The soft timer begins after a real ready
53
+ marker; the hard route uses a detached Linux process-state observer to establish OS exit before overdue timer
54
+ delivery. Tests use `Atomics.wait` rather than scheduler assumptions or busy-spinning, while a direct
55
+ request-mutation guard forces production to snapshot the hard deadline before spawn. The Linux observer is
56
+ independently bounded and accepts only zombie state or `ENOENT` as exit. The hard proof requires that exit
57
+ before a future recorded epoch, then delays controller delivery across it; observer status is published by
58
+ newline-terminated complete-status polling. An unexported AsyncLocalStorage test clock establishes the proof
59
+ epoch only after readiness without weakening public `runChild` snapshot semantics; synchronization searches
60
+ across arbitrary output chunk boundaries before retaining a bounded overlap suffix. Dedicated mutations
61
+ force internal-control consultation, cross-chunk matching and complete terminal-status grammar. Deterministic
62
+ status tests cover partial/complete writes, timeout, observer error, transient/permanent reads and expiry;
63
+ same-process control tests use a two-party barrier for real controlled/uncontrolled overlap and also cover
64
+ nesting, rejection and post-settlement cleanup. Polling has an independent attempt ceiling; AsyncLocalStorage
65
+ ownership lives only under `test/`; build cleans stale `dist/` first and installed smoke refuses the former
66
+ artifact, leaving no control module in the published package.
67
+ - **BREAKING — `LedgerReport.corrupt[]` now exposes `{line, reason}` rather than `{line, text}`.** Raw corrupt
68
+ bytes can contain task/output material and are no longer retained by the public integrity result. Consumers
69
+ that need forensic bytes must read the ledger itself at the reported line; pi-daddy never repairs it.
70
+ - **BREAKING — correlation fields rendered as labels/IDs now require the ASCII identifier grammar
71
+ `[A-Za-z0-9@*][A-Za-z0-9@*._:/-]{0,127}`.** Replace spaces with `-` or `_` before delegating. This prevents a
72
+ model-facing `policy_label`/phase/run ID from becoming a free-text task/output channel into the append-only
73
+ ledger and terminal. Other bounded correlation strings remain unchanged. A top-level
74
+ `assurance_scope: null` is normalized away and is not a valid v3 wire value; omit it instead.
75
+ - **BREAKING — named check IDs must use the same ASCII identifier alphabet and be at most 460 characters.**
76
+ Rename prose/whitespace keys before calling `runNamedCheck`; invalid IDs now refuse before the executable
77
+ or lease starts rather than failing only when a v3 receipt is appended.
78
+
79
+
80
+ ## 0.19.0 — workspace routing is a capability (2026-08-23)
81
+
82
+ **The four hang fixes below were staged for a 0.18.2 and are released here instead — a decision reversed
83
+ deliberately, so the reversal is recorded rather than the reason vanishing.** The paragraph that stood here
84
+ argued for cutting them as a patch so a fix for released code would not be gated on a breaking change. The
85
+ counter-argument won: **R-131 — workspace routing does not attenuate — is a live escalation in published
86
+ 0.18.0 and 0.18.1**, a child routed to `staging` can route its grandchild to `prod` with a real lease and a
87
+ ledger line that reads as authorised, and the fix for it *is* the breaking change. A 0.18.2 would have fixed
88
+ one defect and left that one open, on a line that then needed supporting alongside this one. Shipping once
89
+ fixes all three.
90
+
91
+ - **FIX — a retained lease no longer reports a retention that did not happen (R-152).** `markRetained`
92
+ returned `void` and `releaseDelegationWorkspace` hardcoded the ledger word, so a `workspace_lease` event
93
+ said `retained` — *"the pane may still be live"* — for a helper that had already died (the fact is `lost`),
94
+ for a lease already cleanly released, and for a retention whose record could not be written. It now answers
95
+ in the release vocabulary and the caller ledgers what it says. **Breaking for direct library callers only:**
96
+ `WorkspaceLease.markRetained` returns `Promise<LeaseReleaseOutcome>` instead of `Promise<void>`, and
97
+ `LeaseReleaseOutcome` gained `retained` (so `| "retained"` unions are now redundant, not wrong).
98
+ - **FIX — an impossible `herdr tab close` bound is rejected at both ends, and as a `RangeError` (R-152).**
99
+ Above `2^31 - 1` the timeout truncates — `Number.MAX_SAFE_INTEGER` becomes 1ms and SIGKILLs every close
100
+ attempt before herdr can act (measured: callback at 3ms for a 3s sleep) — the mirror of the `0` case, which
101
+ means no bound at all. Fractional counts were accepted too. **The exception type changed on purpose:** this
102
+ was a `GovernanceRefusal` carrying `WORKSPACE_LEASE_STALE`, which elsewhere means the lease went stale, so a
103
+ controller switching on codes would retry a permanent caller bug. Both bounds are now validated for read
104
+ leases as well, where the check previously sat below an early return and validated nothing.
105
+ - **FIX — a retained writer lease no longer stops its own process from exiting (R-146).** `markRetained`
106
+ leaves the kernel lock and the pane alone by design; it also left the parent's three pipes to the lock
107
+ helper referenced, and the helper was never `unref`ed, so node's event loop stayed alive and `pi` could
108
+ never exit. Measured: `exit=124` (timed out) against `exit=0` for the same sequence ending in `release()`.
109
+ Reached whenever the herdr executor's `tab close` fails. **Which hosts it wedged:** those that let the loop
110
+ drain — pi's `-p`/print mode, and library consumers such as an ADR-0034 external controller. Interactive and
111
+ rpc mode call `process.exit()`, so there the process still left and the on-exit pane sweep still ran.
112
+ Present in 0.18.0 and 0.18.1. Retention still holds the lock: on the parent's exit the helper makes its
113
+ bounded close attempts and then releases, so the worktree is recoverable (R-102) rather than stranded.
114
+ - **FIX — one `herdr tab close` attempt is now bounded in wall clock, not only in count (R-146).** The
115
+ helper's `execFile` had no `timeout`, so a herdr that accepted the close and never answered never called
116
+ back: the retry budget was unreachable, no marker was written, and the lock was held **forever** — R-102's
117
+ explicitly rejected outcome. Measured with a `herdr` that sleeps: before, `LOCK=HELD` with no marker
118
+ indefinitely; after, released with the marker written. Tunable via `herdrCloseTimeoutMs` (default 15s).
119
+
120
+
121
+ - Ship a canonical JSON Schema draft 2020-12 contract for `ledgerVersion: 2` plus deterministic fixtures for
122
+ all four event types, generated through the production builders. Stable package export paths let external
123
+ harnesses pin the real contract instead of maintaining a parallel format.
124
+ - Add a check-receipt event builder and use it on the production named-check path, so every checked-in fixture
125
+ is produced by the same builder that emits the corresponding ledger line.
126
+ - Document strict version dispatch: legacy 0.17 grant records have no explicit version; unsupported explicit
127
+ versions fail closed and are never reinterpreted as legacy.
128
+
129
+ - **BREAKING — routing a child to a registered workspace now requires `workspace:<id>` in the caller's
130
+ grant.** Every grant that routes must add it; a delegation naming a workspace the session does not hold
131
+ is refused `WORKSPACE_NOT_AUTHORIZED`, with the id recorded in `denied` so it counts as an escalation.
132
+
133
+ Until now this was the one governance dimension that did **not** attenuate: the registry inherited into
134
+ every governed child and nothing checked the caller's authority, so a child routed to `staging` could
135
+ route its grandchild to `prod` — with a real lease, a validated CWD, and a ledger line naming `prod`.
136
+ Measured in `docs/probes/g36-workspace-attenuation`, decided in ADR-0035, tracked as R-131.
137
+
138
+ Failing open for compatibility was considered and rejected: it would have made an attenuation fix opt-in.
139
+ The migration is one line per grant, and `pi-daddy init` now lists the registered ids commented in
140
+ `.pi/grants.env` so the edit is visible from the file you already open.
141
+
142
+ - `PI_GRANTS_GATED=workspace:prod` asks a human before a child is routed there, through ADR-0024's existing
143
+ mechanism — the id is the caller's authority for that one delegation and never joins the child's grant.
144
+ `workspace:*` in the gate covers every id.
145
+ - `workspace:*` covers the namespace but is **held, never inherited** — R-26's rule, because a descendant
146
+ holding it could route anywhere the registry lists. `agent:*` is unchanged and still inherits. Asking to
147
+ hand `workspace:*` to a child is refused (`NARROWING_VIOLATED`) rather than silently stripped, so the
148
+ ledger never records authority the child did not receive.
149
+ - `tool:*` still satisfies a workspace capability: governance is opt-in and an ungoverned session must keep
150
+ routing anywhere.
151
+ - A `workspace:` id never reaches pi's `--tools`. It is enforced by pi-daddy before the spawn, which is a
152
+ different and weaker class than the `--tools` allowlist; `docs/SPEC.md` now states both classes explicitly.
153
+ - **BREAKING — a registry id must now match `[A-Za-z0-9][A-Za-z0-9._/-]*`.** The registry became an input to
154
+ 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
155
+ refused `GRANT_ID_MALFORMED` at load, naming the file and the id. **Slashes and dots are fine** — a worktree
156
+ named after its branch (`feature/x`) works, and an earlier build of this release wrongly refused it by
157
+ reusing the tool-name grammar. Refused: whitespace (it splits `allowed-tools`), commas and newlines (they
158
+ split a grant — 0.18.1's defect), `*` (it collided with `workspace:*`, so registering a worktree as `*` and
159
+ granting `workspace:*` minted routing over the whole registry), shell metacharacters (they reach the
160
+ `ROUTABLE WORKSPACES` block of a generated `.pi/grants.env`, which tells you to paste them into your
161
+ grant), and non-ASCII (the generated file is reviewed in an editor, where control characters and
162
+ homoglyphs let one id render as another). One bad entry refuses the whole file, so rename before upgrading.
163
+ - **The registry must be a regular file under 1 MiB** — a FIFO there blocked session start indefinitely, and
164
+ the read is bounded by one handle `fstat`-ed as a descriptor. **Ownership and mode are NOT checked.** An
165
+ earlier draft of this release added a uid/world-writable guard and this bullet promised it; `e1937cf`
166
+ removed the code when the change was narrowed to ADR-0035 and left the promise here for a day. Nothing in
167
+ 0.19.0 checks who may write the registry, and a mode check would not reach the attack that matters — a
168
+ governed child runs as the same uid as its parent. Tracked as R-137.
169
+ - No `workspace:` id is live by default in a generated grant, including one a package's `allowed-tools`
170
+ declares. Which worktree a child starts in is the operator's decision (ADR-0028).
171
+
172
+ ### Fixed before release — found reviewing this change
173
+
174
+ The first group never shipped — they were defects in 0.19.0's own development, caught by two review passes
175
+ and a mutation battery, and are listed because ADR-0035 claimed three of them as done (R-133, and that ADR's
176
+ amendment). **The entries under "Present in earlier releases" below DID ship**, and an earlier draft of this section put
177
+ them under this heading, telling operators the `tool:*` attenuation escape could not affect them. (That draft
178
+ then said "two" while three bullets sat under the heading, one of which — the v2 enum — is the single item
179
+ here that provably did *not* ship. It has moved back.)
180
+
181
+ - **Routing terminated below the root instead of attenuating.** `unknownCapabilities` did not know the
182
+ namespace, and a catalog is always present in a real session, so every requested `workspace:<id>` was
183
+ refused as an unknown capability. No child could be granted one, which made the "two authorities" model
184
+ unreachable. This was the headline defect.
185
+ - The `PI_GRANTS_GATED=workspace:<id>` gate above was claimed and inert.
186
+ - `pi-daddy init` had never heard of the workspace registry.
187
+ - `allowed-tools: workspace:prod` in a `SKILL.md` became `tool:workspace:prod`, which names nothing.
188
+ - `isSafeCapability` rejected the namespace, so the boundary that generates grants could not emit the
189
+ capability this release makes mandatory.
190
+ - `subsumedBy` reported `workspace:*`-covered ids as subsumed, contradicting its own rule.
191
+ - The v2 ledger contract's `refusalCode` enum is now **generated** from `REFUSAL_CODES` by
192
+ `scripts/generate-ledger-v2-contract.ts` instead of hand-maintained beside it. `WORKSPACE_NOT_AUTHORIZED`
193
+ joins the enum in this release; `contracts/ledger/v2/README.md` records why that is a legitimate v2 edit
194
+ rather than a v3 — v2 has never been published, so nothing can have pinned it. It is **not** the last such
195
+ edit: every other closed enum in that schema is still hand-maintained beside its source array — `test/ledger-contract.test.ts`
196
+ asserts each equality, so the same ambush waits for whoever adds an executor kind or a lease outcome. (First
197
+ written as "five"; the figure is larger and is not worth restating, since the test file is the list.)
198
+
199
+ ### Present in earlier releases — read these before upgrading
200
+
201
+ **Upgrade if you run governed delegation trees, and read R-135 first.**
202
+
203
+ - **R-135, and it is not part of this feature.** `tool:*` was reaching delegated children. R-26's rule —
204
+ a wildcard is held, never inherited — was enforced only in `childEnv`, the interceptor path, while
205
+ `delegate.ts` (the path that spawns, since 0.7.0 — ADR-0016) applied no filter, and `tool:*` is not universal enough
206
+ for `assertNarrowing` to stop. A parent holding `tool:*` and delegating `tools: ["tool:*"]` gave its child
207
+ `tool:*`, so attenuation ended at the root. **Present in every published version.** One shared
208
+ `inheritableGrant` now serves both paths. If you run governed delegation trees, this is the entry to read.
209
+
210
+ - **R-134.** Session start no longer warns that a gated `agent:` id "does NOT gate spawning that definition".
211
+ 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
212
+ outlived the defect across eight published versions (0.13.0 through 0.18.1), and the integration suite
213
+ required it to, while advising operators to
214
+ remove a control that works.
215
+
15
216
  ## 0.18.1 — SECURITY: a capability id containing a comma minted authority
16
217
 
17
218
  **Upgrade if you use `tool:*` or `agent:*` in any grant.** Present in 0.18.0 and every earlier published
package/README.md CHANGED
@@ -220,6 +220,39 @@ no `--env` (the grant rides on the pane, which the agent's shell inherits), `age
220
220
  shell so a multi-line argument must be staged to a file, and `agent wait --until idle` matches the state the
221
221
  agent was *already* in, so settling requires a state counter to advance.
222
222
 
223
+ ### Persistent Herdr dashboard
224
+
225
+ When this pi process is itself hosted inside Herdr 0.8+, `/grants dashboard` opens a managed right split:
226
+
227
+ ```text
228
+ PI-DADDY
229
+ ◆ principal-feature · critical · declared
230
+ ● plan running 0:42 · pane w7:p13
231
+ └─ ⛔ deploy refused 0:00
232
+
233
+ depth 2 · 1 active
234
+ ```
235
+
236
+ The plugin ships inside pi-daddy and is linked globally only after an explicit **Install and open** choice.
237
+ Literal **Not now** and **Never ask** choices are persisted; dismissing or losing the dialog stores nothing.
238
+ `/grants dashboard` never installs silently and prints the exact manual command when the plugin is absent. It
239
+ checks the bundled plugin root and protocol before suggesting that a disabled plugin be enabled. Panes and
240
+ ledgers stay workspace/tab-specific: reuse rechecks the pane's current workspace/tab, a wrong-host open is
241
+ closed and rejected, and malformed nested pane state refuses rather than risking a duplicate. A stored entry's
242
+ workspace/tab/ledger must also agree with its hash key before reuse. Invocation `cwd` sets the first pane
243
+ process directory but is not pane identity, so the same workspace/tab/ledger still reuses one pane across caller directories. Opening uses a right split targeted at this pi pane with `--no-focus`.
244
+
245
+ The view is a read-only ledger projection. Yellow is authorised/starting/running, green completed, red failed
246
+ or refused, and grey incomplete/historical. Old completed subtrees collapse; active ancestry stays visible.
247
+ It never displays task text, prompts, tool arguments, child output, or raw corrupt lines. Displayed ledger
248
+ values must satisfy the v3 identifier grammars (unsafe frozen-v2 values are redacted), and Unicode C1/bidi or
249
+ other control/format characters are removed before terminal output.
250
+
251
+ Provenance markers are explicit: **P** planned phase, **O** observed inline activity, **V**
252
+ controller-validated transition, **E** pi-daddy-enforced child, **D** caller-declared correlation. A principal
253
+ run can label itself through `run_id`, `phase`, effective assurance and `policy_label`; pi-daddy does not parse
254
+ principal workflow prose or invent pending/completed phases.
255
+
223
256
  ## Approving a gated capability
224
257
 
225
258
  `gated` capabilities are ones a session holds but may not pass on without a human saying so.
@@ -330,11 +363,36 @@ All fields are optional; existing callers behave unchanged.
330
363
  Git worktree root, and sets initial CWD. A caller cannot label a write-capable grant read-only. Kernel
331
364
  util-linux `flock` allows one **pi-daddy-governed** writer per canonical root; `setpriv --pdeathsig` plus
332
365
  helper attachment stops the writer process or herdr tab on parent death before release. This does not confine paths or exclude unrelated writers; `bash` remains an escape.
366
+
367
+ **BREAKING in 0.19.0 — routing now requires a capability.** A delegation naming `workspace_id: W` needs
368
+ `workspace:W` in the caller's grant, or it is refused `WORKSPACE_NOT_AUTHORIZED`. Every grant that routes
369
+ must add it: `PI_GRANTS_GRANT="tool:read,tool:delegate,workspace:W"`. `pi-daddy init` lists the registered
370
+ ids commented in `.pi/grants.env`. A child can only route on to ids it was granted itself, so this is also
371
+ the list of what any descendant could reach; `workspace:*` exists but is held and never inherited, which
372
+ makes it the wrong answer for anything but a single-worktree setup. `PI_GRANTS_GATED=workspace:W` asks a
373
+ human first. Enforced by pi-daddy before the spawn, not by pi's `--tools` — see `docs/SPEC.md` on the
374
+ enforcement classes.
375
+
376
+ **BREAKING in 0.19.0 — a registry id must match `[A-Za-z0-9][A-Za-z0-9._/-]*`.** An id is now the tail of a
377
+ capability id, so it has to survive the grant grammar. **Slashes and dots are fine**, so a worktree named
378
+ after its branch (`feature/x`) works. Refused, with the file and the id named: whitespace (it splits a
379
+ definition's `allowed-tools`), commas and newlines (they split a grant), `*` (it collided with
380
+ `workspace:*`), shell metacharacters (they reach a generated file you are told to paste from), non-ASCII,
381
+ and `@ + % = ^ ! ? ~ { } [ ]` or a leading `_`, `-` or `.`. **One bad entry refuses the whole registry**, so
382
+ rename before upgrading. The regex is the specification; that list is a summary.
383
+
384
+ **Also new in 0.19.0:** the registry must be a **regular file under 1 MiB**. A FIFO or device there would
385
+ block session start rather than fail, and the read is bounded so a file that grows after its size is checked
386
+ is refused rather than allocated. What is *not* checked: ownership, permissions, and whether a descendant
387
+ holding a write tool repointed an entry — routing attenuates by **id**, not by **destination**
388
+ (`docs/probes/g37-registry-tamper`, tracked as R-137).
333
389
  - Refusals retain current prose and add stable codes such as `CAPABILITY_ESCALATION`,
334
390
  `GATED_UNAPPROVED`, `APPROVAL_SCOPE_MISMATCH`, and `WORKSPACE_WRITE_CONFLICT`.
335
- - Ledger v2 adds joinable capability, lease, lifecycle and check-receipt events while reading legacy lines.
391
+ - Ledger v3 adds unique execution/parent identity, joinable capability/lease/lifecycle/check events, and
392
+ provenance-labelled workflow facts while retaining frozen v2 and legacy readers.
336
393
  - `pi-daddy/check-runner` selects an operator-named absolute executable+argv definition, never a shell
337
- command string. It strips sensitive inherited environment, enforces timeout/output caps, executes a
394
+ command string. Check IDs use the v3 ASCII identifier alphabet and are refused before execution if they do
395
+ not fit their generated receipt identity. It strips sensitive inherited environment, enforces timeout/output caps, executes a
338
396
  private copy of the exact executable bytes it hashed, and pre/post-verifies Git head/candidate-tree
339
397
  identity under an exclusive coordination lease. The executable remains arbitrary code; no filesystem or
340
398
  network sandbox is claimed.
@@ -342,6 +400,22 @@ All fields are optional; existing callers behave unchanged.
342
400
  Public subpaths: `pi-daddy/correlation`, `pi-daddy/refusals`, `pi-daddy/workspace`,
343
401
  `pi-daddy/check-runner`.
344
402
 
403
+ ### Canonical ledger v3 contract
404
+
405
+ Machine consumers should import or resolve
406
+ `pi-daddy/contracts/ledger/v3/ledger-event.schema.json`, not infer a format from prose. Generated fixtures for
407
+ all five events are adjacent. v3 adds globally unique `executionId`, explicit `parentExecutionId`, bounded
408
+ start deadlines, optional Herdr pane identity, and provenance-labelled workflow facts. `childId` remains the
409
+ readable logical tree position and is never an occurrence join.
410
+
411
+ Dispatch on version before event: no version/discriminator is a legacy 0.17 grant record; explicit v2 uses the
412
+ frozen published v2 contract; explicit v3 uses the new closed contract; every other explicit version fails
413
+ closed and is never read as legacy. `verifyLedger` and the dashboard share exact runtime v3 validation, so a
414
+ lookalike string version, missing join identity or malformed nested correlation cannot be `OK` in one and
415
+ corrupt in the other. Explicit v2 is checked against its exact frozen schema before the dashboard labels it
416
+ historical; malformed v2 never becomes a grey row or orphan count. A field/event/enum/requiredness or semantic change requires another ledger version. See
417
+ `contracts/ledger/v3/README.md`; the v2 path remains available unchanged.
418
+
345
419
  ## Running it
346
420
 
347
421
  ```bash
@@ -350,6 +424,8 @@ PI_GRANTS_GRANT="agent:review,tool:read,tool:grep,tool:find,tool:ls,tool:delegat
350
424
  PI_GRANTS_LEDGER=.pi/grants.jsonl \
351
425
  PI_GRANTS_MAX_DEPTH=2 \
352
426
  pi
427
+ # The relative ledger is resolved once at session start and inherited as one absolute path,
428
+ # so routed descendants changing cwd still append to this tree.
353
429
  ```
354
430
 
355
431
  **Plain `pi`, no `-e`, when you installed this from npm** — the package declares `pi.extensions` and pi
@@ -373,12 +449,15 @@ approval says so:
373
449
  as NONE, never as everything.
374
450
  ```
375
451
 
376
- `/grants ledger` reads the ledger back and reports its integrity record count, escalation attempts, any
377
- unparseable lines with line numbers, and **which instructions actually ran**: records grouped by definition
452
+ `/grants dashboard` verifies that this exact pi PID is hosted in its declared Herdr pane, verifies the ledger
453
+ and plugin, then opens or reuses a right split without changing focus. `/grants ledger` reads the audit file
454
+ back and reports its integrity — record count, escalation attempts, any unparseable lines with line numbers
455
+ and content-free reasons (never copied ledger bytes), and **which instructions actually ran**: records grouped by definition
378
456
  digest, each compared against the file on disk (`current` / `CHANGED since`), which is what makes ADR-0018's
379
457
  `definitionDigest` answerable rather than decorative. It exists because nothing in this package had ever read a ledger
380
458
  back, so a torn line was indistinguishable from a spawn that never happened. A corrupt line is **evidence**
381
- and is left alone rather than repaired. Nothing runs this check automatically.
459
+ and is left alone rather than repaired. Session start checks integrity automatically; `/grants ledger` gives
460
+ the full report.
382
461
 
383
462
  ## Worked example: governing `principal-pi-skills`
384
463
 
@@ -511,7 +590,8 @@ everything below it.
511
590
  | `PI_GRANTS_WORKSPACE_LEASE_DIR` | under `$PI_CODING_AGENT_DIR/pi-daddy/` | Kernel writer locks and ownership metadata. |
512
591
  | `PI_GRANTS_CHILD_TIMEOUT` | `600` (seconds) | Wall-clock limit for a child. Inherited by descendants — an operator preference, deliberately *not* attenuating state. |
513
592
  | `PI_GRANTS_FANOUT` | `8` | Per-call width and downward subtree budget; not a session-total counter. Malformed or `0` falls back to the default. |
514
- | `PI_GRANTS_PARENT_ID` | `d0` | This session's ledger id; set by the parent. Makes sibling records joinable into a tree. |
593
+ | `PI_GRANTS_PARENT_ID` | `d0` | Readable logical tree position; set by the parent and allowed to repeat across calls. |
594
+ | `PI_GRANTS_EXECUTION_ID` | unset at a root | Unique governed execution occurrence; set by the parent. Lifecycle/lease joins use this, never `PI_GRANTS_PARENT_ID`. |
515
595
  | `PI_GRANTS_HERDR` | unset ⇒ **probe** | Three-state. Unset probes for a reachable herdr and uses panes if one answers; `1` demands panes and refuses every delegation if herdr is unreachable; `0` demands captured subprocesses. Never detected from `herdr` merely being on `PATH`. |
516
596
  | `PI_GRANTS_HERDR_WORKSPACE` | the parent's `HERDR_WORKSPACE_ID` | herdr workspace for spawned panes. Defaults to the workspace this session is in, so a child is a tab away rather than a workspace away. |
517
597
  | `PI_GRANTS_HERDR_KEEP_PANE` | unset | `1` keeps each child's pane for inspection, and no sweep closes it. Off by default: a fan-out would flood the workspace. |
@@ -532,21 +612,32 @@ is broken after 10s.
532
612
 
533
613
  ## The ledger
534
614
 
535
- Append-only JSONL. Version 2 records capability decisions plus workspace-lease, child-lifecycle and
536
- check-receipt events. Every per-child capability decision is present, **including refusals**; call-level
537
- schema/cardinality rejections that create no child ID are not capability events. The reader still accepts
538
- legacy grant-only lines.
615
+ Append-only JSONL. Version 3 records capability decisions, workspace leases, child lifecycle, check receipts
616
+ and workflow facts. Every per-child capability decision is present, **including refusals**. The reader still
617
+ accepts frozen v2 and legacy grant-only lines.
618
+
619
+ `childId` is hierarchical and readable (`d0.1`, `d0.1.2`) but repeated/parallel calls may reuse it. v3 adds a
620
+ random `executionId` and explicit `parentExecutionId`; those are the only lifecycle/lease occurrence join.
621
+ A v2 lifecycle is therefore shown historical/unjoined rather than guessed. A v3 starting deadline and the
622
+ executor timer share one absolute budget — waiting for the strict starting append consumes it, a later running
623
+ event cannot replace it, and process SIGTERM grace stays inside it under an independent hard-kill timer. That
624
+ timer bounds the governed PID while live. Soft and hard deadline callbacks first allow one event-loop turn for
625
+ pending child exit delivery; retained descendant pipes cannot rewrite a PID that already exited successfully as
626
+ timed out. A pending running append always lands before a terminal event. `denied` non-empty remains the designated escalation signal — **an agent asking for what it does not hold is an escalation attempt, and it
627
+ is invisible without a record.**
539
628
 
540
- Ids are hierarchical and derived: a child of `d0` is `d0.1`, its own second child `d0.1.2`. Ancestry reads
541
- from the id alone with no join, and it is reproducible, so two runs of the same fan-out produce a diffable
542
- ledger. `denied` non-empty is the one designated escalation signal **an agent asking for what it does not
543
- hold is an escalation attempt, and it is invisible without a record.**
629
+ Workflow facts are identifier-only and mark `planned`, `observed`, or `controller_validated`; they can never
630
+ claim pi-daddy enforcement. Capability/lifecycle records are the enforced class. Correlation remains a
631
+ caller-declared label; fields eligible for display must use the v3 ASCII identifier grammar rather than prose.
544
632
 
545
633
  **Privacy is a property of this file, and the boundary is exact: capability ids, counts and identifiers only
546
634
  — never prompts, task text, tool arguments or results.** Trusted `definitionDigest` and `taskDigest` values
547
635
  identify the exact operator body and task. A predictable task can be guessed from SHA-256, so its digest is
548
636
  sensitive/linkable metadata, not anonymization. Caller-supplied digest-looking values remain under
549
- `correlation` and never authorize.
637
+ `correlation` and never authorize. Public v3 builders validate their serialized wire form against the same
638
+ closed runtime contract the readers use; `deadlineAt` therefore cannot be a `Date.parse` lookalike. Schema and
639
+ runtime share the seconds `00`–`59` timestamp profile because JavaScript deadline arithmetic cannot represent
640
+ leap seconds, and a normalized-away top-level null assurance scope is not schema-valid.
550
641
 
551
642
  ## Propagation is race-free by construction
552
643
 
@@ -624,7 +715,7 @@ The resolver, ledger, spawn planner and the whole approval model are pure functi
624
715
  without pi:
625
716
 
626
717
  ```ts
627
- import { resolve, assertNarrowing, planSpawn, buildRecord, appendRecord } from "pi-daddy";
718
+ import { resolve, assertNarrowing, planSpawn, buildRecord, appendRecord, digestTask, newExecutionId } from "pi-daddy";
628
719
 
629
720
  const result = resolve({
630
721
  requested: ["tool:read", "tool:grep"],
@@ -638,12 +729,15 @@ const plan = planSpawn({ effective: result.effective, prompt: task });
638
729
  // -> ["--print","--no-session","--no-extensions","--no-skills","--no-context-files",
639
730
  // "--no-prompt-templates","--tools","grep,read"," summarise src/"]
640
731
 
641
- await appendRecord({ path: ".pi/grants.jsonl" }, buildRecord({ /* … */ result, blocked: false, now: new Date() }));
732
+ await appendRecord({ path: ".pi/grants.jsonl" }, buildRecord({
733
+ /* capability fields … */ result, blocked: false, executor: "process",
734
+ executionId: newExecutionId(), parentExecutionId: null, taskDigest: digestTask(task), now: new Date(),
735
+ }));
642
736
  ```
643
737
 
644
738
  Subpaths are exported individually (`pi-daddy/resolve`, `/ledger`, `/spawn`, `/delegate`, `/catalog`,
645
739
  `/propagation`, `/definitions`, `/fanout`, `/pi-tools`, `/approval`, `/approval-store`, `/approval-prompt`,
646
- `/run-child`, `/run-herdr`).
740
+ `/run-child`, `/run-herdr`, `/dashboard-projection`, `/dashboard-render`).
647
741
 
648
742
  ## Design decisions worth knowing
649
743
 
@@ -673,7 +767,9 @@ npx pi-daddy init # as a command: scaffold .pi/skills/ and .pi/grants.
673
767
  # skill packages — see the worked example above
674
768
  ```
675
769
 
676
- The package is all three. pi loads `extensions/grants.ts` through its own transpiling loader, which reads
770
+ The package is also the source of the optional Herdr plugin: the extension offers to link the trusted
771
+ `herdr-plugin/` directory explicitly, and `pi-daddy-dashboard` is the terminal renderer binary. pi loads
772
+ `extensions/grants.ts` through its own transpiling loader, which reads
677
773
  TypeScript from `node_modules` quite happily; **Node does not** — it refuses to strip types under
678
774
  `node_modules` — so the library entry points are compiled to `dist/`. Until 0.6.0 `exports` pointed at
679
775
  `./src/*.ts`, and every consumer import failed with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` while
@@ -683,11 +779,11 @@ every in-repo test passed. `npm run test:smoke` packs a tarball, installs it int
683
779
  ## Testing
684
780
 
685
781
  ```bash
686
- npm test # 587 unit tests. Fast, pure, no pi, no network.
782
+ npm test # 719 unit tests. Fast, pure, no pi, no network.
687
783
  npm run typecheck # src + extensions + tests + integration tests
688
- npm run test:integration # 44 tests against a REAL pi process. ~55s, no model tokens.
689
- npm run test:smoke # pack, install into a scratch project, import and use it — and run the
690
- # installed `pi-daddy init` bin, which is how R-73 was found
784
+ npm run test:integration # 45 tests against a REAL pi process. ~55s, no model tokens.
785
+ npm run test:smoke # pack/install; exercise library exports, both bins, the v2/v3 contracts,
786
+ # bundled Herdr plugin, dashboard, and `pi-daddy init`
691
787
  PI_GRANTS_IT_MODEL=1 npm run test:integration # + 10 end-to-end tests with a real model. Costs money.
692
788
  ```
693
789
 
@@ -713,6 +809,17 @@ its own author the day after it was added: rather than raise the cap, `delegatio
713
809
 
714
810
  ## Status
715
811
 
812
+ **0.20.0 — live Herdr dashboard and ledger v3.** Adds the explicit installation handshake,
813
+ `/grants dashboard`, duplicate-safe right split, pure live projection, unique execution/parent identity,
814
+ workflow provenance facts and principal correlation labels. Enforcement is unchanged.
815
+
816
+ **0.19.0 — workspace routing is a capability.** Routing now attenuates through `workspace:<id>`; see the
817
+ changelog for the breaking migration.
818
+
819
+ **0.18.1 — security fix for malformed capability IDs.** Capability IDs containing comma, CR, LF, NUL or
820
+ surrounding whitespace are refused before resolution and again before grant serialization, preventing a
821
+ wildcard-covered string from splitting into authority the parent never held.
822
+
716
823
  **0.18.0 — generic runtime enforcement, still honest about scope.** The governed spawn path now includes
717
824
  optional correlation, exact task-bound approvals, registered-worktree CWD validation, OS-backed governed
718
825
  writer leases, structured refusals, lifecycle/lease ledger events, and a no-shell named-check subpath. All
@@ -737,6 +844,12 @@ Known gaps, stated because a gap nobody wrote down is the one that surprises som
737
844
  - **A running delegation is visible.** One status block per call — per child: its definition, its herdr agent, its
738
845
  pane id, its state, elapsed time, and the last three lines it printed. Bounded in height and width, so a fan-out
739
846
  cannot flood your screen. It is a **display, never the result**.
847
+ - **The dashboard whole-file polls.** The MVP is sized for 10 MiB; 50 MiB or 100 ms p95 projection is the
848
+ switch point for incremental replay.
849
+ - **v2 is historical in the dashboard.** It has no unique occurrence ID, so lifecycle is reported unjoined
850
+ rather than matched by reusable `childId`.
851
+ - **principal-pi-skills does not yet publish a generated graph declaration.** Explicit correlation and
852
+ provenance facts render; prompt prose is never parsed into a graph.
740
853
  - **A definition's *instructions* are governed only by identity.** `agent:<name>` says which file may be
741
854
  spawned and the digest says which version ran, but nothing reads a body and judges what it says — the
742
855
  operator authorises a file, and its contents are their responsibility.
@@ -0,0 +1,59 @@
1
+ # pi-daddy ledger contract — version 2
2
+
3
+ Canonical machine contract for one `ledgerVersion: 2` JSONL line:
4
+
5
+ - `ledger-event.schema.json` — JSON Schema draft 2020-12 union, closed by event discriminator.
6
+ - `fixtures/*.json` — deterministic examples generated in the repository by
7
+ `scripts/generate-ledger-v2-contract.ts` through the production builders.
8
+
9
+ After the package version carrying this unreleased artifact is published, installed consumers can resolve
10
+ `pi-daddy/contracts/ledger/v2/ledger-event.schema.json` and the four fixtures as
11
+ `pi-daddy/contracts/ledger/v2/fixtures/{capability-decision,workspace-lease,child-lifecycle,check-receipt}.json`.
12
+ JSON module imports require the runtime's JSON import attribute; filesystem-based validators can resolve the
13
+ same exported package paths without importing the JSON as code.
14
+
15
+ ## Dispatch and compatibility
16
+
17
+ 1. No `ledgerVersion` and no `event` is a legacy 0.17 `GrantRecord`. It is intentionally not described by
18
+ the v2 schema; pi-daddy's reader continues accepting it.
19
+ 2. `ledgerVersion: 2` requires one of the four exact event discriminators and validation against this schema.
20
+ 3. Any other explicit `ledgerVersion`, a missing discriminator, or an unknown discriminator must fail closed.
21
+ Never reinterpret it as a legacy grant line. pi-daddy's `verifyLedger` enforces that dispatch boundary and
22
+ required join fields; consumers needing full nested validation use this schema.
23
+
24
+ The v2 schema is closed (`additionalProperties: false`) so a pinned consumer cannot silently assign meaning
25
+ to a field it does not know. Adding/removing a field, event, enum member, requiredness rule, or changing a
26
+ field's meaning requires a new ledger version and a new versioned path. Correcting prose or adding a fixture
27
+ that does not change accepted records does not.
28
+
29
+ **The rule above binds from first publication, and v2 was first published in 0.19.0.** It was added to the
30
+ repository during 0.19.0's development and no released version has ever carried it, so nothing can have
31
+ pinned it — which is why `WORKSPACE_NOT_AUTHORIZED` (ADR-0035) could join `refusalCode`'s enum in that same
32
+ release rather than forcing a v3 for one refusal code. Recorded here rather than left to inference, because
33
+ the rule as written forbids exactly that edit and a reader comparing the two would be right to.
34
+
35
+ This is the only such amendment for `refusalCode`: from 0.19.0 that enum is **generated** from
36
+ `REFUSAL_CODES` by `scripts/generate-ledger-v2-contract.ts`, and `test/ledger-contract.test.ts` holds the two
37
+ equal. So a new refusal code is now a visible, mechanical change to this artifact — which is what makes the
38
+ rule above enforceable instead of aspirational. Before that it was hand-maintained beside the array it had to
39
+ match, and the first code added after publication turned the suite red with no diff from the generator.
40
+
41
+ ## Event field inventory
42
+
43
+ All v2 events require `ledgerVersion`, `event`, and RFC 3339 `ts`; all carry a joinable `childId` and may
44
+ carry bounded, non-authoritative `correlation` metadata.
45
+
46
+ - `capability_decision`: parent/child/depth, executor, requested/parent/effective/denied/clipped/gated sets,
47
+ blocked/reason, trusted task and optional definition digests, approval source/scope/expiry/use facts,
48
+ optional prior-task author, human/gate outcome, and structured refusal.
49
+ - `workspace_lease`: workspace/root/access, acquisition or release outcome, recovery fact, release reason,
50
+ and structured refusal.
51
+ - `child_lifecycle`: starting/completed/failed state, executor, nullable exit code and signal, timeout/abort/
52
+ truncation flags, and reason.
53
+ - `check_receipt`: receipt/workspace/check IDs and candidate tree identity. The full named-check receipt is a
54
+ separate return artifact; the ledger event is its join record.
55
+
56
+ Trusted fields (`taskDigest`, `definitionDigest`) are outside `correlation`. Digest-looking correlation
57
+ values remain opaque and non-authoritative. `assurance_scope`, `schema_version`, access classification,
58
+ workspace attenuation, approval freshness, tree identity, lease-directory identity, and runtime-cycle
59
+ limitations remain exactly as documented in `docs/SPEC.md`; this artifact does not repair or hide them.
@@ -0,0 +1,95 @@
1
+ {
2
+ "ledgerVersion": 2,
3
+ "event": "capability_decision",
4
+ "ts": "2026-08-20T12:00:01.000Z",
5
+ "parentId": "d0",
6
+ "childId": "d0.1",
7
+ "depth": 1,
8
+ "agentType": "build",
9
+ "executor": "process",
10
+ "taskFrom": "d0.0",
11
+ "taskDigest": "9999999999999999999999999999999999999999999999999999999999999999",
12
+ "correlation": {
13
+ "schema_version": "1.0",
14
+ "run_id": "run-contract-001",
15
+ "task_id": "task-contract-001",
16
+ "workspace_id": "workspace-contract",
17
+ "context_id": "context-contract",
18
+ "phase": "verify",
19
+ "assurance": "critical",
20
+ "assurance_effective": "critical",
21
+ "policy_label": "policy-contract",
22
+ "assurance_source": "policy",
23
+ "assurance_scope": {
24
+ "kind": "changed-files",
25
+ "paths": [
26
+ "src/**"
27
+ ],
28
+ "include_untracked": true
29
+ },
30
+ "activated_at": "2026-08-20T12:00:00.000Z",
31
+ "plan_digest": "1111111111111111111111111111111111111111111111111111111111111111",
32
+ "definition_digest": "2222222222222222222222222222222222222222222222222222222222222222",
33
+ "task_digest": "3333333333333333333333333333333333333333333333333333333333333333",
34
+ "base_sha": "4444444444444444444444444444444444444444",
35
+ "head_sha": "5555555555555555555555555555555555555555",
36
+ "tree_sha": "6666666666666666666666666666666666666666",
37
+ "event_seq": 21,
38
+ "last_change_seq": 18,
39
+ "last_authority_seq": 20,
40
+ "check_receipt_id": "7777777777777777777777777777777777777777777777777777777777777777"
41
+ },
42
+ "refusal": {
43
+ "code": "WORKSPACE_WRITE_CONFLICT",
44
+ "message": "workspace-contract already has a governed writer",
45
+ "details": {
46
+ "workspace_id": "workspace-contract",
47
+ "retryable": true,
48
+ "holder_depth": 1
49
+ }
50
+ },
51
+ "requested": [
52
+ "tool:bash",
53
+ "tool:read"
54
+ ],
55
+ "parentGrant": [
56
+ "agent:build",
57
+ "tool:bash",
58
+ "tool:read"
59
+ ],
60
+ "effective": [
61
+ "tool:bash",
62
+ "tool:read"
63
+ ],
64
+ "denied": [],
65
+ "clipped": [],
66
+ "gatedBlocked": [],
67
+ "blocked": true,
68
+ "reason": "workspace-contract already has a governed writer",
69
+ "approved": [
70
+ "tool:bash",
71
+ "tool:read"
72
+ ],
73
+ "approvalSources": {
74
+ "tool:bash": "persisted",
75
+ "tool:read": "prompt"
76
+ },
77
+ "approvalScopes": {
78
+ "tool:bash": "always",
79
+ "tool:read": "once"
80
+ },
81
+ "approvalExpiresAt": {
82
+ "tool:bash": "2026-09-19T12:00:00.000Z"
83
+ },
84
+ "approvalUses": {
85
+ "tool:read": {
86
+ "max": 1,
87
+ "remaining": 0
88
+ }
89
+ },
90
+ "definitionDigest": {
91
+ "name": "build",
92
+ "source": "/operator/skills/build/SKILL.md",
93
+ "sha256": "8888888888888888888888888888888888888888888888888888888888888888"
94
+ }
95
+ }