pi-daddy 0.18.0 → 0.19.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 (130) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +46 -2
  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/dist/approval-prompt.d.ts +2 -1
  10. package/dist/approval-prompt.d.ts.map +1 -1
  11. package/dist/approval-prompt.js +9 -0
  12. package/dist/approval-prompt.js.map +1 -1
  13. package/dist/approval.d.ts +4 -2
  14. package/dist/approval.d.ts.map +1 -1
  15. package/dist/approval.js +4 -0
  16. package/dist/approval.js.map +1 -1
  17. package/dist/capabilities.d.ts +105 -0
  18. package/dist/capabilities.d.ts.map +1 -1
  19. package/dist/capabilities.js +161 -3
  20. package/dist/capabilities.js.map +1 -1
  21. package/dist/catalog.d.ts +15 -1
  22. package/dist/catalog.d.ts.map +1 -1
  23. package/dist/catalog.js +54 -3
  24. package/dist/catalog.js.map +1 -1
  25. package/dist/check-runner.d.ts.map +1 -1
  26. package/dist/check-runner.js +5 -7
  27. package/dist/check-runner.js.map +1 -1
  28. package/dist/cli.d.ts.map +1 -1
  29. package/dist/cli.js +21 -1
  30. package/dist/cli.js.map +1 -1
  31. package/dist/definitions.d.ts.map +1 -1
  32. package/dist/definitions.js +7 -1
  33. package/dist/definitions.js.map +1 -1
  34. package/dist/delegate.d.ts.map +1 -1
  35. package/dist/delegate.js +32 -4
  36. package/dist/delegate.js.map +1 -1
  37. package/dist/delegation-approval.d.ts.map +1 -1
  38. package/dist/delegation-approval.js +37 -12
  39. package/dist/delegation-approval.js.map +1 -1
  40. package/dist/executor.d.ts +2 -1
  41. package/dist/executor.d.ts.map +1 -1
  42. package/dist/executor.js +1 -0
  43. package/dist/executor.js.map +1 -1
  44. package/dist/grant-env.d.ts +2 -0
  45. package/dist/grant-env.d.ts.map +1 -1
  46. package/dist/grant-env.js +26 -3
  47. package/dist/grant-env.js.map +1 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/init.d.ts +13 -1
  53. package/dist/init.d.ts.map +1 -1
  54. package/dist/init.js +35 -2
  55. package/dist/init.js.map +1 -1
  56. package/dist/lease-helper.d.ts +58 -0
  57. package/dist/lease-helper.d.ts.map +1 -0
  58. package/dist/lease-helper.js +94 -0
  59. package/dist/lease-helper.js.map +1 -0
  60. package/dist/lease-record.d.ts +15 -3
  61. package/dist/lease-record.d.ts.map +1 -1
  62. package/dist/lease-record.js.map +1 -1
  63. package/dist/ledger-events.d.ts +35 -14
  64. package/dist/ledger-events.d.ts.map +1 -1
  65. package/dist/ledger-events.js +41 -0
  66. package/dist/ledger-events.js.map +1 -1
  67. package/dist/ledger.d.ts +10 -5
  68. package/dist/ledger.d.ts.map +1 -1
  69. package/dist/ledger.js +13 -3
  70. package/dist/ledger.js.map +1 -1
  71. package/dist/propagation.d.ts +16 -0
  72. package/dist/propagation.d.ts.map +1 -1
  73. package/dist/propagation.js +22 -2
  74. package/dist/propagation.js.map +1 -1
  75. package/dist/refusals.d.ts +1 -1
  76. package/dist/refusals.d.ts.map +1 -1
  77. package/dist/refusals.js +2 -0
  78. package/dist/refusals.js.map +1 -1
  79. package/dist/resolve.d.ts +10 -0
  80. package/dist/resolve.d.ts.map +1 -1
  81. package/dist/resolve.js +33 -3
  82. package/dist/resolve.js.map +1 -1
  83. package/dist/routing-authority.d.ts +71 -0
  84. package/dist/routing-authority.d.ts.map +1 -0
  85. package/dist/routing-authority.js +100 -0
  86. package/dist/routing-authority.js.map +1 -0
  87. package/dist/skill-packages.d.ts +11 -5
  88. package/dist/skill-packages.d.ts.map +1 -1
  89. package/dist/skill-packages.js +20 -11
  90. package/dist/skill-packages.js.map +1 -1
  91. package/dist/workspace-lease.d.ts +15 -3
  92. package/dist/workspace-lease.d.ts.map +1 -1
  93. package/dist/workspace-lease.js +81 -24
  94. package/dist/workspace-lease.js.map +1 -1
  95. package/dist/workspace.d.ts +25 -0
  96. package/dist/workspace.d.ts.map +1 -1
  97. package/dist/workspace.js +142 -5
  98. package/dist/workspace.js.map +1 -1
  99. package/extensions/delegation.ts +7 -1
  100. package/extensions/grants-command.ts +11 -1
  101. package/extensions/grants.ts +6 -1
  102. package/extensions/init-command.ts +33 -2
  103. package/extensions/session-report.ts +13 -20
  104. package/extensions/session.ts +9 -1
  105. package/extensions/workspace-runtime.ts +34 -4
  106. package/package.json +8 -3
  107. package/src/approval-prompt.ts +2 -1
  108. package/src/approval.ts +4 -2
  109. package/src/capabilities.ts +173 -4
  110. package/src/catalog.ts +62 -4
  111. package/src/check-runner.ts +8 -8
  112. package/src/cli.ts +24 -1
  113. package/src/definitions.ts +7 -1
  114. package/src/delegate.ts +42 -4
  115. package/src/delegation-approval.ts +37 -12
  116. package/src/executor.ts +2 -1
  117. package/src/grant-env.ts +39 -7
  118. package/src/index.ts +1 -0
  119. package/src/init.ts +40 -2
  120. package/src/lease-helper.ts +97 -0
  121. package/src/lease-record.ts +15 -3
  122. package/src/ledger-events.ts +66 -22
  123. package/src/ledger.ts +30 -6
  124. package/src/propagation.ts +23 -2
  125. package/src/refusals.ts +2 -0
  126. package/src/resolve.ts +35 -3
  127. package/src/routing-authority.ts +121 -0
  128. package/src/skill-packages.ts +20 -13
  129. package/src/workspace-lease.ts +84 -24
  130. package/src/workspace.ts +179 -6
package/CHANGELOG.md CHANGED
@@ -12,6 +12,160 @@ 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.19.0 — workspace routing is a capability (2026-08-23)
16
+
17
+ **The four hang fixes below were staged for a 0.18.2 and are released here instead — a decision reversed
18
+ deliberately, so the reversal is recorded rather than the reason vanishing.** The paragraph that stood here
19
+ argued for cutting them as a patch so a fix for released code would not be gated on a breaking change. The
20
+ counter-argument won: **R-131 — workspace routing does not attenuate — is a live escalation in published
21
+ 0.18.0 and 0.18.1**, a child routed to `staging` can route its grandchild to `prod` with a real lease and a
22
+ ledger line that reads as authorised, and the fix for it *is* the breaking change. A 0.18.2 would have fixed
23
+ one defect and left that one open, on a line that then needed supporting alongside this one. Shipping once
24
+ fixes all three.
25
+
26
+ - **FIX — a retained lease no longer reports a retention that did not happen (R-152).** `markRetained`
27
+ returned `void` and `releaseDelegationWorkspace` hardcoded the ledger word, so a `workspace_lease` event
28
+ said `retained` — *"the pane may still be live"* — for a helper that had already died (the fact is `lost`),
29
+ for a lease already cleanly released, and for a retention whose record could not be written. It now answers
30
+ in the release vocabulary and the caller ledgers what it says. **Breaking for direct library callers only:**
31
+ `WorkspaceLease.markRetained` returns `Promise<LeaseReleaseOutcome>` instead of `Promise<void>`, and
32
+ `LeaseReleaseOutcome` gained `retained` (so `| "retained"` unions are now redundant, not wrong).
33
+ - **FIX — an impossible `herdr tab close` bound is rejected at both ends, and as a `RangeError` (R-152).**
34
+ Above `2^31 - 1` the timeout truncates — `Number.MAX_SAFE_INTEGER` becomes 1ms and SIGKILLs every close
35
+ attempt before herdr can act (measured: callback at 3ms for a 3s sleep) — the mirror of the `0` case, which
36
+ means no bound at all. Fractional counts were accepted too. **The exception type changed on purpose:** this
37
+ was a `GovernanceRefusal` carrying `WORKSPACE_LEASE_STALE`, which elsewhere means the lease went stale, so a
38
+ controller switching on codes would retry a permanent caller bug. Both bounds are now validated for read
39
+ leases as well, where the check previously sat below an early return and validated nothing.
40
+ - **FIX — a retained writer lease no longer stops its own process from exiting (R-146).** `markRetained`
41
+ leaves the kernel lock and the pane alone by design; it also left the parent's three pipes to the lock
42
+ helper referenced, and the helper was never `unref`ed, so node's event loop stayed alive and `pi` could
43
+ never exit. Measured: `exit=124` (timed out) against `exit=0` for the same sequence ending in `release()`.
44
+ Reached whenever the herdr executor's `tab close` fails. **Which hosts it wedged:** those that let the loop
45
+ drain — pi's `-p`/print mode, and library consumers such as an ADR-0034 external controller. Interactive and
46
+ rpc mode call `process.exit()`, so there the process still left and the on-exit pane sweep still ran.
47
+ Present in 0.18.0 and 0.18.1. Retention still holds the lock: on the parent's exit the helper makes its
48
+ bounded close attempts and then releases, so the worktree is recoverable (R-102) rather than stranded.
49
+ - **FIX — one `herdr tab close` attempt is now bounded in wall clock, not only in count (R-146).** The
50
+ helper's `execFile` had no `timeout`, so a herdr that accepted the close and never answered never called
51
+ back: the retry budget was unreachable, no marker was written, and the lock was held **forever** — R-102's
52
+ explicitly rejected outcome. Measured with a `herdr` that sleeps: before, `LOCK=HELD` with no marker
53
+ indefinitely; after, released with the marker written. Tunable via `herdrCloseTimeoutMs` (default 15s).
54
+
55
+
56
+ - Ship a canonical JSON Schema draft 2020-12 contract for `ledgerVersion: 2` plus deterministic fixtures for
57
+ all four event types, generated through the production builders. Stable package export paths let external
58
+ harnesses pin the real contract instead of maintaining a parallel format.
59
+ - Add a check-receipt event builder and use it on the production named-check path, so every checked-in fixture
60
+ is produced by the same builder that emits the corresponding ledger line.
61
+ - Document strict version dispatch: legacy 0.17 grant records have no explicit version; unsupported explicit
62
+ versions fail closed and are never reinterpreted as legacy.
63
+
64
+ - **BREAKING — routing a child to a registered workspace now requires `workspace:<id>` in the caller's
65
+ grant.** Every grant that routes must add it; a delegation naming a workspace the session does not hold
66
+ is refused `WORKSPACE_NOT_AUTHORIZED`, with the id recorded in `denied` so it counts as an escalation.
67
+
68
+ Until now this was the one governance dimension that did **not** attenuate: the registry inherited into
69
+ every governed child and nothing checked the caller's authority, so a child routed to `staging` could
70
+ route its grandchild to `prod` — with a real lease, a validated CWD, and a ledger line naming `prod`.
71
+ Measured in `docs/probes/g36-workspace-attenuation`, decided in ADR-0035, tracked as R-131.
72
+
73
+ Failing open for compatibility was considered and rejected: it would have made an attenuation fix opt-in.
74
+ The migration is one line per grant, and `pi-daddy init` now lists the registered ids commented in
75
+ `.pi/grants.env` so the edit is visible from the file you already open.
76
+
77
+ - `PI_GRANTS_GATED=workspace:prod` asks a human before a child is routed there, through ADR-0024's existing
78
+ mechanism — the id is the caller's authority for that one delegation and never joins the child's grant.
79
+ `workspace:*` in the gate covers every id.
80
+ - `workspace:*` covers the namespace but is **held, never inherited** — R-26's rule, because a descendant
81
+ holding it could route anywhere the registry lists. `agent:*` is unchanged and still inherits. Asking to
82
+ hand `workspace:*` to a child is refused (`NARROWING_VIOLATED`) rather than silently stripped, so the
83
+ ledger never records authority the child did not receive.
84
+ - `tool:*` still satisfies a workspace capability: governance is opt-in and an ungoverned session must keep
85
+ routing anywhere.
86
+ - A `workspace:` id never reaches pi's `--tools`. It is enforced by pi-daddy before the spawn, which is a
87
+ different and weaker class than the `--tools` allowlist; `docs/SPEC.md` now states both classes explicitly.
88
+ - **BREAKING — a registry id must now match `[A-Za-z0-9][A-Za-z0-9._/-]*`.** The registry became an input to
89
+ 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
90
+ refused `GRANT_ID_MALFORMED` at load, naming the file and the id. **Slashes and dots are fine** — a worktree
91
+ named after its branch (`feature/x`) works, and an earlier build of this release wrongly refused it by
92
+ reusing the tool-name grammar. Refused: whitespace (it splits `allowed-tools`), commas and newlines (they
93
+ split a grant — 0.18.1's defect), `*` (it collided with `workspace:*`, so registering a worktree as `*` and
94
+ granting `workspace:*` minted routing over the whole registry), shell metacharacters (they reach the
95
+ `ROUTABLE WORKSPACES` block of a generated `.pi/grants.env`, which tells you to paste them into your
96
+ grant), and non-ASCII (the generated file is reviewed in an editor, where control characters and
97
+ homoglyphs let one id render as another). One bad entry refuses the whole file, so rename before upgrading.
98
+ - **The registry must be a regular file under 1 MiB** — a FIFO there blocked session start indefinitely, and
99
+ the read is bounded by one handle `fstat`-ed as a descriptor. **Ownership and mode are NOT checked.** An
100
+ earlier draft of this release added a uid/world-writable guard and this bullet promised it; `e1937cf`
101
+ removed the code when the change was narrowed to ADR-0035 and left the promise here for a day. Nothing in
102
+ 0.19.0 checks who may write the registry, and a mode check would not reach the attack that matters — a
103
+ governed child runs as the same uid as its parent. Tracked as R-137.
104
+ - No `workspace:` id is live by default in a generated grant, including one a package's `allowed-tools`
105
+ declares. Which worktree a child starts in is the operator's decision (ADR-0028).
106
+
107
+ ### Fixed before release — found reviewing this change
108
+
109
+ The first group never shipped — they were defects in 0.19.0's own development, caught by two review passes
110
+ and a mutation battery, and are listed because ADR-0035 claimed three of them as done (R-133, and that ADR's
111
+ amendment). **The entries under "Present in earlier releases" below DID ship**, and an earlier draft of this section put
112
+ them under this heading, telling operators the `tool:*` attenuation escape could not affect them. (That draft
113
+ then said "two" while three bullets sat under the heading, one of which — the v2 enum — is the single item
114
+ here that provably did *not* ship. It has moved back.)
115
+
116
+ - **Routing terminated below the root instead of attenuating.** `unknownCapabilities` did not know the
117
+ namespace, and a catalog is always present in a real session, so every requested `workspace:<id>` was
118
+ refused as an unknown capability. No child could be granted one, which made the "two authorities" model
119
+ unreachable. This was the headline defect.
120
+ - The `PI_GRANTS_GATED=workspace:<id>` gate above was claimed and inert.
121
+ - `pi-daddy init` had never heard of the workspace registry.
122
+ - `allowed-tools: workspace:prod` in a `SKILL.md` became `tool:workspace:prod`, which names nothing.
123
+ - `isSafeCapability` rejected the namespace, so the boundary that generates grants could not emit the
124
+ capability this release makes mandatory.
125
+ - `subsumedBy` reported `workspace:*`-covered ids as subsumed, contradicting its own rule.
126
+ - The v2 ledger contract's `refusalCode` enum is now **generated** from `REFUSAL_CODES` by
127
+ `scripts/generate-ledger-v2-contract.ts` instead of hand-maintained beside it. `WORKSPACE_NOT_AUTHORIZED`
128
+ joins the enum in this release; `contracts/ledger/v2/README.md` records why that is a legitimate v2 edit
129
+ rather than a v3 — v2 has never been published, so nothing can have pinned it. It is **not** the last such
130
+ edit: every other closed enum in that schema is still hand-maintained beside its source array — `test/ledger-contract.test.ts`
131
+ asserts each equality, so the same ambush waits for whoever adds an executor kind or a lease outcome. (First
132
+ written as "five"; the figure is larger and is not worth restating, since the test file is the list.)
133
+
134
+ ### Present in earlier releases — read these before upgrading
135
+
136
+ **Upgrade if you run governed delegation trees, and read R-135 first.**
137
+
138
+ - **R-135, and it is not part of this feature.** `tool:*` was reaching delegated children. R-26's rule —
139
+ a wildcard is held, never inherited — was enforced only in `childEnv`, the interceptor path, while
140
+ `delegate.ts` (the path that spawns, since 0.7.0 — ADR-0016) applied no filter, and `tool:*` is not universal enough
141
+ for `assertNarrowing` to stop. A parent holding `tool:*` and delegating `tools: ["tool:*"]` gave its child
142
+ `tool:*`, so attenuation ended at the root. **Present in every published version.** One shared
143
+ `inheritableGrant` now serves both paths. If you run governed delegation trees, this is the entry to read.
144
+
145
+ - **R-134.** Session start no longer warns that a gated `agent:` id "does NOT gate spawning that definition".
146
+ 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
147
+ outlived the defect across eight published versions (0.13.0 through 0.18.1), and the integration suite
148
+ required it to, while advising operators to
149
+ remove a control that works.
150
+
151
+ ## 0.18.1 — SECURITY: a capability id containing a comma minted authority
152
+
153
+ **Upgrade if you use `tool:*` or `agent:*` in any grant.** Present in 0.18.0 and every earlier published
154
+ version that has the wildcard prefix rules.
155
+
156
+ `PI_GRANTS_GRANT` is comma-separated. A capability id containing a comma was admitted by a wildcard's
157
+ **prefix** rule — `agent:x,tool:bash` starts with `agent:`, so a root holding `agent:*` covered it — then
158
+ written verbatim into the child's grant and split by the child into two capabilities. The child received a
159
+ real `tool:bash` from a tree whose root never held it, `denied` was empty, so nothing recorded an
160
+ escalation and the ledger line read as an ordinary authorised delegation.
161
+
162
+ `tool:*` is affected identically, since it covers every namespace.
163
+
164
+ Fixed with two guards: a malformed id can no longer be **granted** (it lands in `denied` and is recorded as
165
+ the escalation attempt it is), and neither grant writer will **emit** one. No legitimate id is affected —
166
+ `ext:@scope/pkg/tool`, `skill:my-skill`, `agent:my_agent` and both wildcards are unchanged. Tracked as
167
+ R-132.
168
+
15
169
  ## 0.18.0 — generic runtime enforcement for external controllers
16
170
 
17
171
  - Optional correlation metadata joins capability decisions to run/task/workspace/context IDs, candidate
package/README.md CHANGED
@@ -330,6 +330,29 @@ All fields are optional; existing callers behave unchanged.
330
330
  Git worktree root, and sets initial CWD. A caller cannot label a write-capable grant read-only. Kernel
331
331
  util-linux `flock` allows one **pi-daddy-governed** writer per canonical root; `setpriv --pdeathsig` plus
332
332
  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.
333
+
334
+ **BREAKING in 0.19.0 — routing now requires a capability.** A delegation naming `workspace_id: W` needs
335
+ `workspace:W` in the caller's grant, or it is refused `WORKSPACE_NOT_AUTHORIZED`. Every grant that routes
336
+ must add it: `PI_GRANTS_GRANT="tool:read,tool:delegate,workspace:W"`. `pi-daddy init` lists the registered
337
+ ids commented in `.pi/grants.env`. A child can only route on to ids it was granted itself, so this is also
338
+ the list of what any descendant could reach; `workspace:*` exists but is held and never inherited, which
339
+ makes it the wrong answer for anything but a single-worktree setup. `PI_GRANTS_GATED=workspace:W` asks a
340
+ human first. Enforced by pi-daddy before the spawn, not by pi's `--tools` — see `docs/SPEC.md` on the
341
+ enforcement classes.
342
+
343
+ **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
344
+ capability id, so it has to survive the grant grammar. **Slashes and dots are fine**, so a worktree named
345
+ after its branch (`feature/x`) works. Refused, with the file and the id named: whitespace (it splits a
346
+ definition's `allowed-tools`), commas and newlines (they split a grant), `*` (it collided with
347
+ `workspace:*`), shell metacharacters (they reach a generated file you are told to paste from), non-ASCII,
348
+ and `@ + % = ^ ! ? ~ { } [ ]` or a leading `_`, `-` or `.`. **One bad entry refuses the whole registry**, so
349
+ rename before upgrading. The regex is the specification; that list is a summary.
350
+
351
+ **Also new in 0.19.0:** the registry must be a **regular file under 1 MiB**. A FIFO or device there would
352
+ block session start rather than fail, and the read is bounded so a file that grows after its size is checked
353
+ is refused rather than allocated. What is *not* checked: ownership, permissions, and whether a descendant
354
+ holding a write tool repointed an entry — routing attenuates by **id**, not by **destination**
355
+ (`docs/probes/g37-registry-tamper`, tracked as R-137).
333
356
  - Refusals retain current prose and add stable codes such as `CAPABILITY_ESCALATION`,
334
357
  `GATED_UNAPPROVED`, `APPROVAL_SCOPE_MISMATCH`, and `WORKSPACE_WRITE_CONFLICT`.
335
358
  - Ledger v2 adds joinable capability, lease, lifecycle and check-receipt events while reading legacy lines.
@@ -342,6 +365,23 @@ All fields are optional; existing callers behave unchanged.
342
365
  Public subpaths: `pi-daddy/correlation`, `pi-daddy/refusals`, `pi-daddy/workspace`,
343
366
  `pi-daddy/check-runner`.
344
367
 
368
+ ### Canonical ledger v2 contract
369
+
370
+ Machine consumers should import or resolve
371
+ `pi-daddy/contracts/ledger/v2/ledger-event.schema.json`, not infer a format from examples in prose. This
372
+ unreleased source candidate packages generated fixtures at
373
+ `pi-daddy/contracts/ledger/v2/fixtures/{capability-decision,workspace-lease,child-lifecycle,check-receipt}.json`.
374
+ They are built through the same event builders as production and pin nested correlation, trusted digests,
375
+ approval and refusal facts, and lifecycle nullability.
376
+
377
+ Dispatch on version before event: no `ledgerVersion`/`event` is a legacy 0.17 grant record; explicit version
378
+ 2 must validate as one of the four events; every other explicit version fails closed and must never be read
379
+ as legacy. `verifyLedger` enforces that dispatch boundary and required join fields; full nested validation
380
+ uses the schema. The v2 schema is closed, so a field/event/enum/requiredness or semantic change requires a new
381
+ ledger version and versioned artifact path. The already-published npm 0.18.1 does not contain these files;
382
+ they require the next authorized package release. See `contracts/ledger/v2/README.md` for the field inventory
383
+ and compatibility rules.
384
+
345
385
  ## Running it
346
386
 
347
387
  ```bash
@@ -683,9 +723,9 @@ every in-repo test passed. `npm run test:smoke` packs a tarball, installs it int
683
723
  ## Testing
684
724
 
685
725
  ```bash
686
- npm test # 587 unit tests. Fast, pure, no pi, no network.
726
+ npm test # 633 unit tests. Fast, pure, no pi, no network.
687
727
  npm run typecheck # src + extensions + tests + integration tests
688
- npm run test:integration # 44 tests against a REAL pi process. ~55s, no model tokens.
728
+ npm run test:integration # 45 tests against a REAL pi process. ~55s, no model tokens.
689
729
  npm run test:smoke # pack, install into a scratch project, import and use it — and run the
690
730
  # installed `pi-daddy init` bin, which is how R-73 was found
691
731
  PI_GRANTS_IT_MODEL=1 npm run test:integration # + 10 end-to-end tests with a real model. Costs money.
@@ -713,6 +753,10 @@ its own author the day after it was added: rather than raise the cap, `delegatio
713
753
 
714
754
  ## Status
715
755
 
756
+ **0.18.1 — security fix for malformed capability IDs.** Capability IDs containing comma, CR, LF, NUL or
757
+ surrounding whitespace are refused before resolution and again before grant serialization, preventing a
758
+ wildcard-covered string from splitting into authority the parent never held.
759
+
716
760
  **0.18.0 — generic runtime enforcement, still honest about scope.** The governed spawn path now includes
717
761
  optional correlation, exact task-bound approvals, registered-worktree CWD validation, OS-backed governed
718
762
  writer leases, structured refusals, lifecycle/lease ledger events, and a no-shell named-check subpath. All
@@ -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
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "ledgerVersion": 2,
3
+ "event": "check_receipt",
4
+ "ts": "2026-08-20T12:00:04.000Z",
5
+ "childId": "check:spec-lint:00000000-0000-4000-8000-000000000000",
6
+ "receiptId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
7
+ "workspaceId": "workspace-contract",
8
+ "checkId": "spec-lint",
9
+ "treeSha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
10
+ "correlation": {
11
+ "schema_version": "1.0",
12
+ "run_id": "run-contract-001",
13
+ "task_id": "task-contract-001",
14
+ "workspace_id": "workspace-contract",
15
+ "context_id": "context-contract",
16
+ "phase": "verify",
17
+ "assurance": "critical",
18
+ "assurance_effective": "critical",
19
+ "policy_label": "policy-contract",
20
+ "assurance_source": "policy",
21
+ "assurance_scope": {
22
+ "kind": "changed-files",
23
+ "paths": [
24
+ "src/**"
25
+ ],
26
+ "include_untracked": true
27
+ },
28
+ "activated_at": "2026-08-20T12:00:00.000Z",
29
+ "plan_digest": "1111111111111111111111111111111111111111111111111111111111111111",
30
+ "definition_digest": "2222222222222222222222222222222222222222222222222222222222222222",
31
+ "task_digest": "3333333333333333333333333333333333333333333333333333333333333333",
32
+ "base_sha": "4444444444444444444444444444444444444444",
33
+ "head_sha": "5555555555555555555555555555555555555555",
34
+ "tree_sha": "6666666666666666666666666666666666666666",
35
+ "event_seq": 21,
36
+ "last_change_seq": 18,
37
+ "last_authority_seq": 20,
38
+ "check_receipt_id": "7777777777777777777777777777777777777777777777777777777777777777"
39
+ }
40
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "ledgerVersion": 2,
3
+ "event": "child_lifecycle",
4
+ "ts": "2026-08-20T12:00:03.000Z",
5
+ "childId": "d0.1",
6
+ "state": "failed",
7
+ "executor": "process",
8
+ "exitCode": null,
9
+ "signal": null,
10
+ "aborted": true,
11
+ "reason": "child did not start",
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
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "ledgerVersion": 2,
3
+ "event": "workspace_lease",
4
+ "ts": "2026-08-20T12:00:02.000Z",
5
+ "childId": "d0.1",
6
+ "workspaceId": "workspace-contract",
7
+ "root": "/worktrees/contract",
8
+ "access": "write",
9
+ "outcome": "acquired",
10
+ "recovered": "unknown",
11
+ "correlation": {
12
+ "schema_version": "1.0",
13
+ "run_id": "run-contract-001",
14
+ "task_id": "task-contract-001",
15
+ "workspace_id": "workspace-contract",
16
+ "context_id": "context-contract",
17
+ "phase": "verify",
18
+ "assurance": "critical",
19
+ "assurance_effective": "critical",
20
+ "policy_label": "policy-contract",
21
+ "assurance_source": "policy",
22
+ "assurance_scope": {
23
+ "kind": "changed-files",
24
+ "paths": [
25
+ "src/**"
26
+ ],
27
+ "include_untracked": true
28
+ },
29
+ "activated_at": "2026-08-20T12:00:00.000Z",
30
+ "plan_digest": "1111111111111111111111111111111111111111111111111111111111111111",
31
+ "definition_digest": "2222222222222222222222222222222222222222222222222222222222222222",
32
+ "task_digest": "3333333333333333333333333333333333333333333333333333333333333333",
33
+ "base_sha": "4444444444444444444444444444444444444444",
34
+ "head_sha": "5555555555555555555555555555555555555555",
35
+ "tree_sha": "6666666666666666666666666666666666666666",
36
+ "event_seq": 21,
37
+ "last_change_seq": 18,
38
+ "last_authority_seq": 20,
39
+ "check_receipt_id": "7777777777777777777777777777777777777777777777777777777777777777"
40
+ }
41
+ }