pi-daddy 0.20.1 → 0.21.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.
package/CHANGELOG.md CHANGED
@@ -12,6 +12,24 @@ 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.21.0 — one project init enables its ledger (2026-09-01)
16
+
17
+ - `/grants init` now atomically persists one project choice containing both the capability grant and default
18
+ `.pi/grants.jsonl`, then adopts both after that write succeeds. Future plain `pi` starts load both, so
19
+ `/grants ledger` and `/grants dashboard` need no repeated shell export.
20
+ - Merely installing pi-daddy still initializes nothing. `PI_GRANTS_GRANT` bypasses the project store for
21
+ children/CI, and `PI_GRANTS_LEDGER` overrides the stored default, including `""` for a one-run opt-out.
22
+ - Existing version-1 grant stores retain their no-ledger behavior until the operator explicitly reruns
23
+ `/grants init`; an upgrade never converts prior governance consent into recording consent.
24
+ - Newly generated `.pi/grants.env` files export the project ledger. Existing reviewed files remain untouched.
25
+ - **First-review repair:** remember whether the ledger came from the environment before publishing a stored
26
+ default, so a later init after a cwd change can replace pi-daddy's own old path without overriding a real
27
+ operator value. A second review made v1 lookalike `projectLedger` fields an explicit inert case after its
28
+ mutation survived the first tests. Review also exposed pre-existing malformed-store fail-open behavior
29
+ (R-175); it remains open and is not presented as fixed by this release.
30
+ - **Final-quality repair:** installed smoke now matches the active ledger export as a complete line; the old
31
+ commented `#export` contained the previous substring and falsely passed.
32
+
15
33
  ## 0.20.1 — open the dashboard with Herdr's split-pane contract (2026-09-01)
16
34
 
17
35
  - Fix `/grants dashboard` on Herdr 0.8+: a split plugin pane targets the verified caller pane and must not
package/README.md CHANGED
@@ -6,7 +6,8 @@ tree, so a sub-agent can never hold more of the **tool surface** than its parent
6
6
  configured.
7
7
 
8
8
  Both qualifiers are load-bearing and are not buried: a child granted `bash` can escape governance entirely
9
- (ADR-0012, measured), and the ledger is opt-in. See *What this governs, and what it does not*.
9
+ (ADR-0012, measured), and recording is opt-in explicitly by environment or by running `/grants init` once
10
+ for this project. See *What this governs, and what it does not*.
10
11
 
11
12
  > **0.13.0 was the first published release.** Earlier versions were developed in-repo and never shipped, so
12
13
  > the breaking changes in the changelog describe how this package arrived at its current behaviour rather
@@ -475,9 +476,12 @@ pi # then, inside pi:
475
476
 
476
477
  **`pi install`, not `npm install`** — it registers the package with pi, which is what makes the extension
477
478
  auto-load; presence in `node_modules` alone does nothing. `/grants init` asks only about the capabilities
478
- that can change your machine and applies the answer to the running session, no restart (ADR-0030).
479
- `npx pi-daddy init` does the same scaffolding from a shell, and both search pi's install root as well as
480
- the project's.
479
+ that can change your machine and applies the answer to the running session, no restart (ADR-0030). That one
480
+ project opt-in now also enables `.pi/grants.jsonl` immediately and for every later plain `pi` start
481
+ (ADR-0037), so `/grants ledger` and `/grants dashboard` need no shell export. Merely installing the package
482
+ does not initialize unrelated directories. `npx pi-daddy init` does the same scaffolding from a shell, and
483
+ both search pi's install root as well as the project's. A pre-0.21 stored grant is not retroactive recording
484
+ consent; run `/grants init` once after upgrading to enable its default ledger.
481
485
 
482
486
  ```
483
487
  found principal-pi-skills@2.3.1 — 7 skill(s), 0 declaring allowed-tools
@@ -579,13 +583,13 @@ everything below it.
579
583
 
580
584
  | Variable | Default | Notes |
581
585
  | :--- | :--- | :--- |
582
- | `PI_GRANTS_GRANT` | unset → ungoverned | Presence is what switches governance on. |
586
+ | `PI_GRANTS_GRANT` | unset → use this directory's stored init choice, or ungoverned when none exists | Presence switches governance on and bypasses the whole cwd store; this is how children and CI stay environment-only. |
583
587
  | `PI_GRANTS_MAX_DEPTH` | `2` | Child-depth bound. `0` disables spawning. |
584
588
  | `PI_GRANTS_DEPTH` | `0` | This session's own depth; set by the parent, not by hand. |
585
589
  | `PI_GRANTS_GATED` | **`tool:bash`** in a governed session | Capabilities needing human approval. Set to `""` to gate nothing. Gating is closed under subsumption, so this also covers `write`/`edit`/`read`/`grep`/`find`/`ls` (ADR-0012). |
586
590
  | `PI_GRANTS_APPROVED` | unset | Inherited `capability@subject#sha256` entries; set by the parent, clamped to the child's own grant, and honoured only against the definition body the child itself loaded (ADR-0022). |
587
591
  | `PI_GRANTS_APPROVAL_TIMEOUT` | `120` (seconds) | How long a dialog waits. `0` or an unreadable value means **no timeout**: waiting forever denies nothing, so it is the safe reading of a value we do not understand. |
588
- | `PI_GRANTS_LEDGER` | unset → not recording | **Setting this makes the ledger load-bearing** see below. |
592
+ | `PI_GRANTS_LEDGER` | unset → a v2 `/grants init` choice uses `<cwd>/.pi/grants.jsonl`; otherwise not recording | Presence overrides the project default; `""` disables it for one run. Any effective path is load-bearing. |
589
593
  | `PI_GRANTS_WORKSPACE_REGISTRY` | unset | Operator-owned `{version:1, workspaces:{id:{path}}}` file, required only for workspace-routed spawns. |
590
594
  | `PI_GRANTS_WORKSPACE_LEASE_DIR` | under `$PI_CODING_AGENT_DIR/pi-daddy/` | Kernel writer locks and ownership metadata. |
591
595
  | `PI_GRANTS_CHILD_TIMEOUT` | `600` (seconds) | Wall-clock limit for a child. Inherited by descendants — an operator preference, deliberately *not* attenuating state. |
@@ -595,7 +599,7 @@ everything below it.
595
599
  | `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`. |
596
600
  | `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. |
597
601
  | `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. |
598
- | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | pi's own variable, not ours — but it decides where persisted approvals live, so it is listed here. |
602
+ | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | pi's own variable, not ours — but it decides where stored project grants/ledger consent and persisted approvals live. |
599
603
 
600
604
  **A malformed value disables spawning; it never falls back to a default.** An unreadable
601
605
  `PI_GRANTS_MAX_DEPTH` or `PI_GRANTS_DEPTH` yields `maxDepth: 0` and a startup warning naming the
@@ -603,12 +607,13 @@ variable. Before 0.5.0 these were read with `parseInt`, which accepts numeric pr
603
607
  and otherwise gives `NaN` — and since every comparison against `NaN` is false, a typo did not tighten the
604
608
  depth limit, it **removed** it.
605
609
 
606
- **Configuring a ledger makes it a precondition, not a log.** If `PI_GRANTS_LEDGER` is set and the write
607
- fails, the delegation is **refused**. Asking for an audit trail is an explicit act, and `ledger.ts` has
608
- always documented that an unrecorded grant should fail closed; until 0.5.0 both call sites silently
609
- swallowed the error. Sessions with no ledger configured are unaffected. Concurrent appends are serialised by
610
- a lock file with a short timeout failing closed beats hanging and a lock abandoned by a killed process
611
- is broken after 10s.
610
+ **Configuring a ledger makes it a precondition, not a log.** This applies equally to an explicit
611
+ `PI_GRANTS_LEDGER` and to the project default accepted through `/grants init`: if the write fails, delegation
612
+ is **refused**. Asking for an audit trail is an explicit act, and `ledger.ts` has always documented that an
613
+ unrecorded grant should fail closed; until 0.5.0 both call sites silently swallowed the error. Legacy v1 grant
614
+ stores and sessions with no ledger configured are unaffected. Concurrent appends are serialised by a lock file
615
+ with a short timeout — failing closed beats hanging — and a lock abandoned by a killed process is broken after
616
+ 10s.
612
617
 
613
618
  ## The ledger
614
619
 
@@ -1 +1 @@
1
- {"version":3,"file":"grant-env.d.ts","sourceRoot":"","sources":["../src/grant-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAEvE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,UAAU,EAEpD,CAAC;AAEF,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAQ/D;AAaD,qBAAa,gBAAiB,SAAQ,KAAK;CAAG;AAE9C,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI,CAU/D;AAED,gFAAgF;AAChF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,+FAA+F;IAC/F,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,iCAAiC;IACjC,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,6GAA6G;IAC7G,QAAQ,EAAE,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;IACpC,yFAAyF;IACzF,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,sGAAsG;IACtG,kBAAkB,CAAC,EAAE,UAAU,EAAE,CAAC;IAClC,kGAAkG;IAClG,eAAe,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,UAAU,CAAA;KAAE,EAAE,CAAC;IAC5D,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CA4G3D;AAED,qFAAqF;AACrF,eAAO,MAAM,WAAW,EAAE,UAAgC,CAAC"}
1
+ {"version":3,"file":"grant-env.d.ts","sourceRoot":"","sources":["../src/grant-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAEvE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,UAAU,EAEpD,CAAC;AAEF,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAQ/D;AAaD,qBAAa,gBAAiB,SAAQ,KAAK;CAAG;AAE9C,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI,CAU/D;AAED,gFAAgF;AAChF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,+FAA+F;IAC/F,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,iCAAiC;IACjC,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,6GAA6G;IAC7G,QAAQ,EAAE,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;IACpC,yFAAyF;IACzF,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,sGAAsG;IACtG,kBAAkB,CAAC,EAAE,UAAU,EAAE,CAAC;IAClC,kGAAkG;IAClG,eAAe,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,UAAU,CAAA;KAAE,EAAE,CAAC;IAC5D,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CA6G3D;AAED,qFAAqF;AACrF,eAAO,MAAM,WAAW,EAAE,UAAgC,CAAC"}
package/dist/grant-env.js CHANGED
@@ -146,7 +146,7 @@ export function renderGrantEnv(input) {
146
146
  lines.push(`# CAUTION: ${caution}`);
147
147
  if (input.cautions.length > 0)
148
148
  lines.push("#");
149
- lines.push("# tool:delegate is what registers the delegation tools at all — withhold it and this session is a leaf.", `export PI_GRANTS_GRANT="${input.live.join(",")}"`, "", "# Optional. Setting a ledger makes it LOAD-BEARING: a spawn that cannot be recorded is refused.", '#export PI_GRANTS_LEDGER=".pi/grants.jsonl"', "", "# Optional. tool:bash is ALREADY gated by default, so this line only matters if you widen it:", "# gating is closed under subsumption, so gating tool:write also gates tool:bash (bash can write).", '#export PI_GRANTS_GATED="tool:bash,tool:write"', "");
149
+ lines.push("# tool:delegate is what registers the delegation tools at all — withhold it and this session is a leaf.", `export PI_GRANTS_GRANT="${input.live.join(",")}"`, "", "# Init enables this project ledger. It is LOAD-BEARING: a spawn that cannot be recorded is refused.", '# PI_GRANTS_LEDGER still overrides the stored default used by plain `pi` after `/grants init`.', 'export PI_GRANTS_LEDGER=".pi/grants.jsonl"', "", "# Optional. tool:bash is ALREADY gated by default, so this line only matters if you widen it:", "# gating is closed under subsumption, so gating tool:write also gates tool:bash (bash can write).", '#export PI_GRANTS_GATED="tool:bash,tool:write"', "");
150
150
  return lines.join("\n");
151
151
  }
152
152
  /** The capability that must always be live, or the generated file is inert (S-5). */
@@ -1 +1 @@
1
- {"version":3,"file":"grant-env.js","sourceRoot":"","sources":["../src/grant-env.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"grant-env.js","sourceRoot":"","sources":["../src/grant-env.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  /**
2
- * The grant for a directory, stored **outside** it.
2
+ * The init choice for a directory, stored **outside** it: its grant and, in v2, explicit project-ledger
3
+ * consent.
3
4
  *
4
- * `PI_GRANTS_GRANT` is the propagation channel to children and stays exactly as it was — a parent writes
5
- * it once per session and every child inherits it. What this adds is a second *source* for the root
6
- * session's own grant, so an operator does not have to `source` a file and restart pi to be governed.
5
+ * `PI_GRANTS_GRANT` and `PI_GRANTS_LEDGER` remain the propagation channel to children — a parent writes them
6
+ * and every child inherits them. The store is a root-session source only, so an operator does not have to
7
+ * `source` a file and restart pi after making the project choice.
7
8
  *
8
9
  * **Outside the workspace, and that is the whole design.** A grant is a ceiling; a ceiling a governed child
9
10
  * can rewrite is not a ceiling. `<cwd>/.pi/grants.env` is writable by any child holding `tool:write`, so
@@ -18,6 +19,14 @@
18
19
  * of the decision, diffable in a PR. It is simply no longer the thing the enforcer reads.
19
20
  */
20
21
  import type { Capability } from "./resolve.ts";
22
+ export interface StoredGrant {
23
+ grant: Capability[];
24
+ /** Whether a root session defaults to `<cwd>/.pi/grants.jsonl`. False for every legacy v1 store. */
25
+ projectLedger: boolean;
26
+ }
27
+ export interface SaveGrantOptions {
28
+ projectLedger?: boolean;
29
+ }
21
30
  /**
22
31
  * Where this directory's grant lives.
23
32
  *
@@ -27,15 +36,17 @@ import type { Capability } from "./resolve.ts";
27
36
  */
28
37
  export declare function grantStorePath(cwd: string): string;
29
38
  /**
30
- * Parse a store file's text into a grant, or null.
39
+ * Parse a store file's text into its versioned project choice, or null.
31
40
  *
32
41
  * Split out from the readers so the **one** validation lives in one place: both the sync and async paths
33
42
  * must agree, and two parsers is how they come to disagree.
34
43
  *
35
- * Fails closed on every doubt a malformed file grants nothing rather than something. The `cwd` check is
36
- * R-27's: a file copied to another machine or another checkout describes a directory that is not this one,
37
- * and honouring it would let a grant travel somewhere nobody authorised it for.
44
+ * Rejects every doubt at this parser boundary it never constructs a partial ceiling. The session currently
45
+ * treats absent and invalid stores alike (R-175), so do not call that wider behavior fail-closed. The `cwd`
46
+ * check is R-27's: a copied file describes a directory nobody authorised it for.
38
47
  */
48
+ export declare function parseStoredGrant(text: string, cwd: string): StoredGrant | null;
49
+ /** Compatibility view for callers that need only the capability ceiling. */
39
50
  export declare function parseGrantFile(text: string, cwd: string): Capability[] | null;
40
51
  /**
41
52
  * Read this directory's stored grant, synchronously.
@@ -46,12 +57,16 @@ export declare function parseGrantFile(text: string, cwd: string): Capability[]
46
57
  * decision it exists to inform, so the store would silently fail to grant delegation — the exact class of
47
58
  * defect R-38 and R-39 were.
48
59
  */
60
+ export declare function loadStoredGrantSync(cwd: string): StoredGrant | null;
49
61
  export declare function loadGrantSync(cwd: string): Capability[] | null;
50
62
  /** Async twin, for callers that already have one. Same parser, so they cannot disagree. */
63
+ export declare function loadStoredGrant(cwd: string): Promise<StoredGrant | null>;
51
64
  export declare function loadGrant(cwd: string): Promise<Capability[] | null>;
65
+ /** The one project-local default ADR-0037 binds to an explicit v2 init choice. */
66
+ export declare function projectLedgerPath(cwd: string): string;
52
67
  export type SaveOutcome = "saved" | "failed" | "busy";
53
68
  /**
54
- * Write this directory's grant.
69
+ * Write this directory's grant and optional project-ledger consent as one atomic choice.
55
70
  *
56
71
  * Locked with the same lock the ledger and the approval store use, for the same reason: two sessions in one
57
72
  * directory running `/grants init` concurrently must not interleave. A lock this cannot take yields `busy`
@@ -62,7 +77,7 @@ export type SaveOutcome = "saved" | "failed" | "busy";
62
77
  * filesystem, so a reader never sees a half-written grant. Both copied from `writeFileSafely`, deliberately
63
78
  * — a second, subtly different atomic-write is how the two come to disagree about what "safe" meant.
64
79
  */
65
- export declare function saveGrant(cwd: string, grant: Capability[]): Promise<SaveOutcome>;
80
+ export declare function saveGrant(cwd: string, grant: Capability[], options?: SaveGrantOptions): Promise<SaveOutcome>;
66
81
  /** Remove this directory's stored grant. True when a file was there to remove. */
67
82
  export declare function clearGrant(cwd: string): Promise<boolean>;
68
83
  //# sourceMappingURL=grant-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"grant-store.d.ts","sourceRoot":"","sources":["../src/grant-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AASH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAgB/C;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,GAAG,IAAI,CAW7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,GAAG,IAAI,CAM9D;AAED,2FAA2F;AAC3F,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,CAMzE;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAmBtF;AAED,kFAAkF;AAClF,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAS9D"}
1
+ {"version":3,"file":"grant-store.d.ts","sourceRoot":"","sources":["../src/grant-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAoB/C,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,oGAAoG;IACpG,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAOD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAoB9E;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,GAAG,IAAI,CAE7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAMnE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,GAAG,IAAI,CAE9D;AAED,2FAA2F;AAC3F,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAM9E;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,CAEzE;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,EAAE,EACnB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,WAAW,CAAC,CAsBtB;AAED,kFAAkF;AAClF,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAS9D"}
@@ -1,9 +1,10 @@
1
1
  /**
2
- * The grant for a directory, stored **outside** it.
2
+ * The init choice for a directory, stored **outside** it: its grant and, in v2, explicit project-ledger
3
+ * consent.
3
4
  *
4
- * `PI_GRANTS_GRANT` is the propagation channel to children and stays exactly as it was — a parent writes
5
- * it once per session and every child inherits it. What this adds is a second *source* for the root
6
- * session's own grant, so an operator does not have to `source` a file and restart pi to be governed.
5
+ * `PI_GRANTS_GRANT` and `PI_GRANTS_LEDGER` remain the propagation channel to children — a parent writes them
6
+ * and every child inherits them. The store is a root-session source only, so an operator does not have to
7
+ * `source` a file and restart pi after making the project choice.
7
8
  *
8
9
  * **Outside the workspace, and that is the whole design.** A grant is a ceiling; a ceiling a governed child
9
10
  * can rewrite is not a ceiling. `<cwd>/.pi/grants.env` is writable by any child holding `tool:write`, so
@@ -22,7 +23,7 @@ import { readFileSync } from "node:fs";
22
23
  import { mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
23
24
  import { randomUUID } from "node:crypto";
24
25
  import { homedir } from "node:os";
25
- import { basename, dirname, join } from "node:path";
26
+ import { basename, dirname, join, resolve } from "node:path";
26
27
  import { withFileLock, LockTimeoutError } from "./file-lock.js";
27
28
  /** `$PI_CODING_AGENT_DIR`, or pi's default. Same resolution as the approval store. */
28
29
  function agentDir() {
@@ -41,19 +42,19 @@ export function grantStorePath(cwd) {
41
42
  return join(agentDir(), "grants", `${slug}-${hash}.json`);
42
43
  }
43
44
  /**
44
- * Parse a store file's text into a grant, or null.
45
+ * Parse a store file's text into its versioned project choice, or null.
45
46
  *
46
47
  * Split out from the readers so the **one** validation lives in one place: both the sync and async paths
47
48
  * must agree, and two parsers is how they come to disagree.
48
49
  *
49
- * Fails closed on every doubt a malformed file grants nothing rather than something. The `cwd` check is
50
- * R-27's: a file copied to another machine or another checkout describes a directory that is not this one,
51
- * and honouring it would let a grant travel somewhere nobody authorised it for.
50
+ * Rejects every doubt at this parser boundary it never constructs a partial ceiling. The session currently
51
+ * treats absent and invalid stores alike (R-175), so do not call that wider behavior fail-closed. The `cwd`
52
+ * check is R-27's: a copied file describes a directory nobody authorised it for.
52
53
  */
53
- export function parseGrantFile(text, cwd) {
54
+ export function parseStoredGrant(text, cwd) {
54
55
  try {
55
56
  const parsed = JSON.parse(text);
56
- if (parsed.version !== 1)
57
+ if (parsed.version !== 1 && parsed.version !== 2)
57
58
  return null;
58
59
  if (parsed.cwd !== cwd)
59
60
  return null;
@@ -61,12 +62,21 @@ export function parseGrantFile(text, cwd) {
61
62
  return null;
62
63
  if (!parsed.grant.every((c) => typeof c === "string" && c.length > 0))
63
64
  return null;
64
- return parsed.grant;
65
+ if (parsed.version === 2 && parsed.projectLedger !== true)
66
+ return null;
67
+ return {
68
+ grant: parsed.grant,
69
+ projectLedger: parsed.version === 2,
70
+ };
65
71
  }
66
72
  catch {
67
73
  return null;
68
74
  }
69
75
  }
76
+ /** Compatibility view for callers that need only the capability ceiling. */
77
+ export function parseGrantFile(text, cwd) {
78
+ return parseStoredGrant(text, cwd)?.grant ?? null;
79
+ }
70
80
  /**
71
81
  * Read this directory's stored grant, synchronously.
72
82
  *
@@ -76,25 +86,35 @@ export function parseGrantFile(text, cwd) {
76
86
  * decision it exists to inform, so the store would silently fail to grant delegation — the exact class of
77
87
  * defect R-38 and R-39 were.
78
88
  */
79
- export function loadGrantSync(cwd) {
89
+ export function loadStoredGrantSync(cwd) {
80
90
  try {
81
- return parseGrantFile(readFileSync(grantStorePath(cwd), "utf8"), cwd);
91
+ return parseStoredGrant(readFileSync(grantStorePath(cwd), "utf8"), cwd);
82
92
  }
83
93
  catch {
84
94
  return null;
85
95
  }
86
96
  }
97
+ export function loadGrantSync(cwd) {
98
+ return loadStoredGrantSync(cwd)?.grant ?? null;
99
+ }
87
100
  /** Async twin, for callers that already have one. Same parser, so they cannot disagree. */
88
- export async function loadGrant(cwd) {
101
+ export async function loadStoredGrant(cwd) {
89
102
  try {
90
- return parseGrantFile(await readFile(grantStorePath(cwd), "utf8"), cwd);
103
+ return parseStoredGrant(await readFile(grantStorePath(cwd), "utf8"), cwd);
91
104
  }
92
105
  catch {
93
106
  return null;
94
107
  }
95
108
  }
109
+ export async function loadGrant(cwd) {
110
+ return (await loadStoredGrant(cwd))?.grant ?? null;
111
+ }
112
+ /** The one project-local default ADR-0037 binds to an explicit v2 init choice. */
113
+ export function projectLedgerPath(cwd) {
114
+ return resolve(cwd, ".pi", "grants.jsonl");
115
+ }
96
116
  /**
97
- * Write this directory's grant.
117
+ * Write this directory's grant and optional project-ledger consent as one atomic choice.
98
118
  *
99
119
  * Locked with the same lock the ledger and the approval store use, for the same reason: two sessions in one
100
120
  * directory running `/grants init` concurrently must not interleave. A lock this cannot take yields `busy`
@@ -105,9 +125,12 @@ export async function loadGrant(cwd) {
105
125
  * filesystem, so a reader never sees a half-written grant. Both copied from `writeFileSafely`, deliberately
106
126
  * — a second, subtly different atomic-write is how the two come to disagree about what "safe" meant.
107
127
  */
108
- export async function saveGrant(cwd, grant) {
128
+ export async function saveGrant(cwd, grant, options = {}) {
109
129
  const path = grantStorePath(cwd);
110
- const file = { version: 1, cwd, grant: [...grant].sort(), writtenAt: new Date().toISOString() };
130
+ const common = { cwd, grant: [...grant].sort(), writtenAt: new Date().toISOString() };
131
+ const file = options.projectLedger
132
+ ? { version: 2, ...common, projectLedger: true }
133
+ : { version: 1, ...common };
111
134
  try {
112
135
  await mkdir(dirname(path), { recursive: true });
113
136
  return await withFileLock(path, "grant store", async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"grant-store.js","sourceRoot":"","sources":["../src/grant-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"grant-store.js","sourceRoot":"","sources":["../src/grant-store.ts"],"names":[],"mappings":"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"}
@@ -52,8 +52,8 @@ export interface GrantsCommandContext {
52
52
  */
53
53
  previewDelegation: (name: string) => Promise<GatedPlan>;
54
54
  /**
55
- * Run `/grants init`: scaffold definitions, ask about the withheld capabilities only, store the answer
56
- * outside the workspace, and apply it to this session (ADR-0030).
55
+ * Run `/grants init`: scaffold definitions, ask about withheld capabilities, atomically store grant plus
56
+ * project-ledger consent outside the workspace, and apply both now (ADR-0030, ADR-0037).
57
57
  *
58
58
  * Injected rather than imported so this module stays what it is — a read-only diagnostic that cannot
59
59
  * become a governance path by accident. The one command here that *writes* takes its ability to do so
@@ -345,7 +345,7 @@ handler: async (args: string, ctx: any) => {
345
345
  // two facts about what a spawn will be sit together.
346
346
  ` executor ${executor.disclosure}`,
347
347
  ` depth ${depth} of max ${maxDepth}${maxDepth <= 0 ? " (spawning disabled)" : ""}`,
348
- ` ledger ${ledgerPath ?? "(not recording — set PI_GRANTS_LEDGER)"}`,
348
+ ` ledger ${ledgerPath || "(not recording — set PI_GRANTS_LEDGER)"}`,
349
349
  ` approvals ${sessionApprovals.size} this session, ${valid.size} persisted` +
350
350
  `${inheritedApprovals.size > 0 ? `, ${inheritedApprovals.size} inherited` : ""}` +
351
351
  ` — /grants approvals`,
@@ -10,12 +10,12 @@
10
10
  import { discoverSkillPackages } from "../src/skill-packages.ts";
11
11
  import { applyInit, planInit } from "../src/init.ts";
12
12
  import { registeredWorkspaceIds } from "../src/workspace.ts";
13
- import { saveGrant, grantStorePath } from "../src/grant-store.ts";
13
+ import { grantStorePath, projectLedgerPath, saveGrant } from "../src/grant-store.ts";
14
14
  import { expandSubsumed, SUBSUMPTION, type Capability } from "../src/resolve.ts";
15
15
  import type { GrantsSession } from "./session.ts";
16
16
 
17
17
  /**
18
- * `/grants init` — scaffold, ask about what is withheld, store it outside the workspace, apply it now.
18
+ * `/grants init` — scaffold, ask about what is withheld, store grant + ledger consent, apply both now.
19
19
  *
20
20
  * **The dialog covers the withheld capabilities and nothing else** (ADR-0030). Asking about all of them
21
21
  * would be a dozen questions for a first run, and this project has a name for what that produces: R-25,
@@ -118,7 +118,8 @@ export async function runInit(
118
118
  }
119
119
 
120
120
  const finalGrant = [...grant].sort();
121
- const saved = await saveGrant(ctx.cwd, finalGrant);
121
+ const ledger = projectLedgerPath(ctx.cwd);
122
+ const saved = await saveGrant(ctx.cwd, finalGrant, { projectLedger: true });
122
123
  if (saved !== "saved") {
123
124
  lines.push(
124
125
  ` NOT STORED — ${saved === "busy" ? "another session holds the grant store" : "the store could not be written"}. ` +
@@ -130,12 +131,15 @@ export async function runInit(
130
131
 
131
132
  // Live, without a restart — the whole point of the store. Only after a successful write, so what runs and
132
133
  // what is recorded cannot disagree.
133
- session.adoptGrant(finalGrant);
134
+ session.adoptGrant(finalGrant, ledger);
134
135
  // Order matters: the grant first, then the reload, because `refreshSpawnable` filters the definitions it
135
136
  // advertises through `maySpawnDefinition` against the grant the session now holds.
136
137
  await refresh();
137
138
 
138
- lines.push(` stored at ${grantStorePath(ctx.cwd)} — outside this project, so no child can rewrite it`);
139
+ lines.push(
140
+ ` stored at ${grantStorePath(ctx.cwd)} — outside this project, so no child can rewrite it`,
141
+ ` ledger ${ledger} — default for future pi sessions; PI_GRANTS_LEDGER still overrides it`,
142
+ );
139
143
  if (granted.length > 0) lines.push(` GRANTED: ${granted.join(", ")}`);
140
144
  if (alreadyConferred.length > 0) {
141
145
  lines.push(` ALREADY CONFERRED, not asked about: ${alreadyConferred.join(", ")}`);
@@ -44,7 +44,7 @@ import type { Capability } from "../src/resolve.ts";
44
44
  import { loadDefinitions } from "../src/definitions.ts";
45
45
  import { buildCatalog } from "../src/catalog.ts";
46
46
  import { ENV_WORKSPACE_REGISTRY } from "../src/workspace.ts";
47
- import { loadGrantSync, grantStorePath } from "../src/grant-store.ts";
47
+ import { grantStorePath, loadStoredGrantSync, projectLedgerPath } from "../src/grant-store.ts";
48
48
  import { republishable } from "./approvals.ts";
49
49
 
50
50
  /**
@@ -184,7 +184,7 @@ export interface GrantsSession {
184
184
  */
185
185
  readonly storeCwd: string;
186
186
  /**
187
- * Adopt a grant decided DURING the session — `/grants init` answering a human — without a restart.
187
+ * Adopt the project choice made DURING the session — grant plus optional default ledger — without restart.
188
188
  *
189
189
  * Narrow by design: it sets the session's own grant and republishes, so the very next spawn is bounded by
190
190
  * it. It does **not** reach children that already exist; those are separate processes whose environment
@@ -194,7 +194,7 @@ export interface GrantsSession {
194
194
  * Only a human can reach this. Slash commands are user-invoked; no tool exposes it, so a model cannot
195
195
  * widen its own session's ceiling by calling something.
196
196
  */
197
- adoptGrant(grant: Capability[]): void;
197
+ adoptGrant(grant: Capability[], projectLedger?: string): void;
198
198
  }
199
199
 
200
200
  /**
@@ -258,9 +258,16 @@ export function createGrantsSession(extensionPath: string | undefined): GrantsSe
258
258
  // against each other and says so if they differ, which is the only case this can get wrong.
259
259
  const grantRaw = process.env[ENV_GRANT];
260
260
  const storeCwd = process.cwd();
261
- const stored = grantRaw === undefined ? loadGrantSync(storeCwd) : null;
261
+ // One root-only store read supplies both decisions made by `/grants init`. A child always has ENV_GRANT,
262
+ // so it cannot activate a ledger merely because its routed cwd happens to have a v2 store (ADR-0037).
263
+ const stored = grantRaw === undefined ? loadStoredGrantSync(storeCwd) : null;
262
264
  const governed = grantRaw !== undefined || stored !== null;
263
- const inherited: Capability[] = grantRaw !== undefined ? parseList(grantRaw) : (stored ?? [WILDCARD]);
265
+ const inherited: Capability[] = grantRaw !== undefined ? parseList(grantRaw) : (stored?.grant ?? [WILDCARD]);
266
+ const ledgerRaw = process.env[ENV_LEDGER];
267
+ // Capture provenance before publishChildEnv writes this session's derived default into process.env. A later
268
+ // `/grants init` for ctx.cwd must not mistake our own publication for an operator override.
269
+ const ledgerFromEnvironment = ledgerRaw !== undefined;
270
+ const storedLedger = stored?.projectLedger ? projectLedgerPath(storeCwd) : undefined;
264
271
  // G7 / A-S4 + B-I4: strict, three-way parsing that fails CLOSED. A malformed bound used to yield
265
272
  // `NaN`, and every comparison against `NaN` is false, so depth limiting switched itself off.
266
273
  const bounds = depthConfig(process.env[ENV_DEPTH], process.env[ENV_MAX_DEPTH]);
@@ -280,7 +287,9 @@ export function createGrantsSession(extensionPath: string | undefined): GrantsSe
280
287
  // ungoverned-descendant escape hatch, and doing that silently is what changes here.
281
288
  // `PI_GRANTS_GATED=""` turns the default off; absent and empty are deliberately distinguishable.
282
289
  gated: governed ? gatedFromEnv(process.env[ENV_GATED]) : parseList(process.env[ENV_GATED]),
283
- ledgerPath: process.env[ENV_LEDGER],
290
+ // Presence wins, including an explicitly empty value for a one-run opt-out. The store is eligible only
291
+ // when ENV_GRANT was absent above, preserving the environment as the child's single authority channel.
292
+ ledgerPath: ledgerRaw !== undefined ? ledgerRaw : storedLedger,
284
293
  // The un-probed reading. `resolveExecutor` replaces it at session start; until then a `1` already reads as
285
294
  // a refusal, which is the safe direction — a delegation that somehow ran before the probe would refuse
286
295
  // rather than quietly use the wrong executor.
@@ -342,13 +351,18 @@ export function createGrantsSession(extensionPath: string | undefined): GrantsSe
342
351
 
343
352
  storeCwd,
344
353
 
345
- adoptGrant: (grant: Capability[]) => {
354
+ adoptGrant: (grant: Capability[], projectLedger?: string) => {
346
355
  // Governed too, not just bounded. A session that starts with no grant and then runs `/grants init` is
347
356
  // governed from that moment: every spawn is bounded by what was just stored. Leaving this false made
348
357
  // `/grants` print "inactive" while holding thirteen capabilities — a status line contradicting the
349
358
  // enforcer, which is the defect R-28 is named for.
350
359
  session.governed = true;
351
360
  session.ownGrant = grant;
361
+ // An environment ledger remains the explicit answer. Otherwise init's v2 choice becomes live now,
362
+ // before publishChildEnv gives the same absolute path to descendants.
363
+ if (!ledgerFromEnvironment && projectLedger !== undefined) {
364
+ session.ledgerPath = projectLedger;
365
+ }
352
366
  session.publishChildEnv();
353
367
  },
354
368
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-daddy",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "description": "Capability governance for pi sub-agents: spawn Agent Skills (SKILL.md) definitions whose allowed-tools becomes a grant that can only narrow going down a delegation tree, enforced by pi's own --tools allowlist, with an append-only ledger.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/grant-env.ts CHANGED
@@ -204,8 +204,9 @@ export function renderGrantEnv(input: GrantEnvInput): string {
204
204
  "# tool:delegate is what registers the delegation tools at all — withhold it and this session is a leaf.",
205
205
  `export PI_GRANTS_GRANT="${input.live.join(",")}"`,
206
206
  "",
207
- "# Optional. Setting a ledger makes it LOAD-BEARING: a spawn that cannot be recorded is refused.",
208
- '#export PI_GRANTS_LEDGER=".pi/grants.jsonl"',
207
+ "# Init enables this project ledger. It is LOAD-BEARING: a spawn that cannot be recorded is refused.",
208
+ '# PI_GRANTS_LEDGER still overrides the stored default used by plain `pi` after `/grants init`.',
209
+ 'export PI_GRANTS_LEDGER=".pi/grants.jsonl"',
209
210
  "",
210
211
  "# Optional. tool:bash is ALREADY gated by default, so this line only matters if you widen it:",
211
212
  "# gating is closed under subsumption, so gating tool:write also gates tool:bash (bash can write).",
@@ -1,9 +1,10 @@
1
1
  /**
2
- * The grant for a directory, stored **outside** it.
2
+ * The init choice for a directory, stored **outside** it: its grant and, in v2, explicit project-ledger
3
+ * consent.
3
4
  *
4
- * `PI_GRANTS_GRANT` is the propagation channel to children and stays exactly as it was — a parent writes
5
- * it once per session and every child inherits it. What this adds is a second *source* for the root
6
- * session's own grant, so an operator does not have to `source` a file and restart pi to be governed.
5
+ * `PI_GRANTS_GRANT` and `PI_GRANTS_LEDGER` remain the propagation channel to children — a parent writes them
6
+ * and every child inherits them. The store is a root-session source only, so an operator does not have to
7
+ * `source` a file and restart pi after making the project choice.
7
8
  *
8
9
  * **Outside the workspace, and that is the whole design.** A grant is a ceiling; a ceiling a governed child
9
10
  * can rewrite is not a ceiling. `<cwd>/.pi/grants.env` is writable by any child holding `tool:write`, so
@@ -23,11 +24,11 @@ import { readFileSync } from "node:fs";
23
24
  import { mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
24
25
  import { randomUUID } from "node:crypto";
25
26
  import { homedir } from "node:os";
26
- import { basename, dirname, join } from "node:path";
27
+ import { basename, dirname, join, resolve } from "node:path";
27
28
  import { withFileLock, LockTimeoutError } from "./file-lock.ts";
28
29
  import type { Capability } from "./resolve.ts";
29
30
 
30
- interface GrantFile {
31
+ interface GrantFileV1 {
31
32
  version: 1;
32
33
  /** The directory this grant is for. Checked on read — an entry copied elsewhere is refused (R-27). */
33
34
  cwd: string;
@@ -36,6 +37,25 @@ interface GrantFile {
36
37
  writtenAt: string;
37
38
  }
38
39
 
40
+ interface GrantFileV2 {
41
+ version: 2;
42
+ cwd: string;
43
+ grant: Capability[];
44
+ /** Explicit consent from ADR-0037's `/grants init`; never inferred from a legacy file merely existing. */
45
+ projectLedger: true;
46
+ writtenAt: string;
47
+ }
48
+
49
+ export interface StoredGrant {
50
+ grant: Capability[];
51
+ /** Whether a root session defaults to `<cwd>/.pi/grants.jsonl`. False for every legacy v1 store. */
52
+ projectLedger: boolean;
53
+ }
54
+
55
+ export interface SaveGrantOptions {
56
+ projectLedger?: boolean;
57
+ }
58
+
39
59
  /** `$PI_CODING_AGENT_DIR`, or pi's default. Same resolution as the approval store. */
40
60
  function agentDir(): string {
41
61
  return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
@@ -55,28 +75,42 @@ export function grantStorePath(cwd: string): string {
55
75
  }
56
76
 
57
77
  /**
58
- * Parse a store file's text into a grant, or null.
78
+ * Parse a store file's text into its versioned project choice, or null.
59
79
  *
60
80
  * Split out from the readers so the **one** validation lives in one place: both the sync and async paths
61
81
  * must agree, and two parsers is how they come to disagree.
62
82
  *
63
- * Fails closed on every doubt a malformed file grants nothing rather than something. The `cwd` check is
64
- * R-27's: a file copied to another machine or another checkout describes a directory that is not this one,
65
- * and honouring it would let a grant travel somewhere nobody authorised it for.
83
+ * Rejects every doubt at this parser boundary it never constructs a partial ceiling. The session currently
84
+ * treats absent and invalid stores alike (R-175), so do not call that wider behavior fail-closed. The `cwd`
85
+ * check is R-27's: a copied file describes a directory nobody authorised it for.
66
86
  */
67
- export function parseGrantFile(text: string, cwd: string): Capability[] | null {
87
+ export function parseStoredGrant(text: string, cwd: string): StoredGrant | null {
68
88
  try {
69
- const parsed = JSON.parse(text) as Partial<GrantFile>;
70
- if (parsed.version !== 1) return null;
89
+ const parsed = JSON.parse(text) as {
90
+ version?: unknown;
91
+ cwd?: unknown;
92
+ grant?: unknown;
93
+ projectLedger?: unknown;
94
+ };
95
+ if (parsed.version !== 1 && parsed.version !== 2) return null;
71
96
  if (parsed.cwd !== cwd) return null;
72
97
  if (!Array.isArray(parsed.grant)) return null;
73
98
  if (!parsed.grant.every((c) => typeof c === "string" && c.length > 0)) return null;
74
- return parsed.grant as Capability[];
99
+ if (parsed.version === 2 && parsed.projectLedger !== true) return null;
100
+ return {
101
+ grant: parsed.grant as Capability[],
102
+ projectLedger: parsed.version === 2,
103
+ };
75
104
  } catch {
76
105
  return null;
77
106
  }
78
107
  }
79
108
 
109
+ /** Compatibility view for callers that need only the capability ceiling. */
110
+ export function parseGrantFile(text: string, cwd: string): Capability[] | null {
111
+ return parseStoredGrant(text, cwd)?.grant ?? null;
112
+ }
113
+
80
114
  /**
81
115
  * Read this directory's stored grant, synchronously.
82
116
  *
@@ -86,27 +120,40 @@ export function parseGrantFile(text: string, cwd: string): Capability[] | null {
86
120
  * decision it exists to inform, so the store would silently fail to grant delegation — the exact class of
87
121
  * defect R-38 and R-39 were.
88
122
  */
89
- export function loadGrantSync(cwd: string): Capability[] | null {
123
+ export function loadStoredGrantSync(cwd: string): StoredGrant | null {
90
124
  try {
91
- return parseGrantFile(readFileSync(grantStorePath(cwd), "utf8"), cwd);
125
+ return parseStoredGrant(readFileSync(grantStorePath(cwd), "utf8"), cwd);
92
126
  } catch {
93
127
  return null;
94
128
  }
95
129
  }
96
130
 
131
+ export function loadGrantSync(cwd: string): Capability[] | null {
132
+ return loadStoredGrantSync(cwd)?.grant ?? null;
133
+ }
134
+
97
135
  /** Async twin, for callers that already have one. Same parser, so they cannot disagree. */
98
- export async function loadGrant(cwd: string): Promise<Capability[] | null> {
136
+ export async function loadStoredGrant(cwd: string): Promise<StoredGrant | null> {
99
137
  try {
100
- return parseGrantFile(await readFile(grantStorePath(cwd), "utf8"), cwd);
138
+ return parseStoredGrant(await readFile(grantStorePath(cwd), "utf8"), cwd);
101
139
  } catch {
102
140
  return null;
103
141
  }
104
142
  }
105
143
 
144
+ export async function loadGrant(cwd: string): Promise<Capability[] | null> {
145
+ return (await loadStoredGrant(cwd))?.grant ?? null;
146
+ }
147
+
148
+ /** The one project-local default ADR-0037 binds to an explicit v2 init choice. */
149
+ export function projectLedgerPath(cwd: string): string {
150
+ return resolve(cwd, ".pi", "grants.jsonl");
151
+ }
152
+
106
153
  export type SaveOutcome = "saved" | "failed" | "busy";
107
154
 
108
155
  /**
109
- * Write this directory's grant.
156
+ * Write this directory's grant and optional project-ledger consent as one atomic choice.
110
157
  *
111
158
  * Locked with the same lock the ledger and the approval store use, for the same reason: two sessions in one
112
159
  * directory running `/grants init` concurrently must not interleave. A lock this cannot take yields `busy`
@@ -117,9 +164,16 @@ export type SaveOutcome = "saved" | "failed" | "busy";
117
164
  * filesystem, so a reader never sees a half-written grant. Both copied from `writeFileSafely`, deliberately
118
165
  * — a second, subtly different atomic-write is how the two come to disagree about what "safe" meant.
119
166
  */
120
- export async function saveGrant(cwd: string, grant: Capability[]): Promise<SaveOutcome> {
167
+ export async function saveGrant(
168
+ cwd: string,
169
+ grant: Capability[],
170
+ options: SaveGrantOptions = {},
171
+ ): Promise<SaveOutcome> {
121
172
  const path = grantStorePath(cwd);
122
- const file: GrantFile = { version: 1, cwd, grant: [...grant].sort(), writtenAt: new Date().toISOString() };
173
+ const common = { cwd, grant: [...grant].sort(), writtenAt: new Date().toISOString() };
174
+ const file: GrantFileV1 | GrantFileV2 = options.projectLedger
175
+ ? { version: 2, ...common, projectLedger: true }
176
+ : { version: 1, ...common };
123
177
  try {
124
178
  await mkdir(dirname(path), { recursive: true });
125
179
  return await withFileLock(path, "grant store", async () => {