pi-daddy 0.14.0 → 0.15.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
@@ -93,6 +93,38 @@ resolution, enforcement, approvals or the ledger changed — this is the part be
93
93
  defect as the `exports` map that worked in the tree and threw for every consumer, and the second time that
94
94
  script has caught it.
95
95
 
96
+ ## 0.15.0 — `/grants init`, a grant that survives without an env var, and a setup that was wrong
97
+
98
+ **Setup is two steps instead of five, and one of the five was wrong.**
99
+
100
+ ```bash
101
+ pi install npm:pi-daddy
102
+ pi install npm:principal-pi-skills
103
+ pi
104
+ /grants init
105
+ ```
106
+
107
+ - **`pi install`, not `npm install`.** This package's own documentation said `npm install`, and that is not
108
+ how pi packages are installed: `pi install` registers the package in pi's settings, which is what makes
109
+ the extension auto-load, and it installs to `$PI_CODING_AGENT_DIR/npm/node_modules` rather than the
110
+ project. **`init` searched only the project and so found nothing for anyone who followed pi's own
111
+ instructions** — telling them to install a package they had just installed. It now searches both roots,
112
+ project first so a pinned copy outranks the machine-wide one.
113
+ - **`/grants init`** scaffolds definitions, asks about the capabilities that can change your machine, and
114
+ applies the answer to the session you are already in — **no `source`, no restart**.
115
+ - **A grant can now come from a store**, at `$PI_CODING_AGENT_DIR/grants/<project>.json` — **outside** your
116
+ project, because a ceiling a child holding `tool:write` could rewrite is not a ceiling. `PI_GRANTS_GRANT`
117
+ **always wins**: it is how a child is governed and how CI is configured. `.pi/grants.env` is still written
118
+ and still worth committing — it is the reviewable record, no longer what the enforcer reads. (ADR-0030)
119
+ - **The dialog does not ask questions whose answers cannot matter.** `tool:bash` subsumes `write` and
120
+ `edit`, so once bash is granted those are conferred; asking anyway let an operator answer *no* to
121
+ `tool:write` and then watch `build` be allowed it. It now reports them as already conferred instead.
122
+ - **An unknown `/grants` subcommand is refused rather than ignored.** `/grants init` used to print the
123
+ ordinary status screen with the word silently dropped, so a command that did not exist looked like it had
124
+ worked.
125
+ - **`init` counted authorisations and called them declarations** — "7 skill(s), 3 declaring allowed-tools"
126
+ when all seven declared.
127
+
96
128
  ## 0.13.0 — the approvals file gets the lock the ledger already had, and two silences end
97
129
 
98
130
  Closing the last items that were open rather than out of scope, then **red-teaming the result**: an
package/README.md CHANGED
@@ -339,10 +339,18 @@ installed. Every transcript below is **copied from a run**, not retyped — the
339
339
  this section abridges output it says so.
340
340
 
341
341
  ```bash
342
- npm install pi-daddy principal-pi-skills
343
- npx pi-daddy init
342
+ pi install npm:pi-daddy
343
+ pi install npm:principal-pi-skills
344
+ pi # then, inside pi:
345
+ /grants init
344
346
  ```
345
347
 
348
+ **`pi install`, not `npm install`** — it registers the package with pi, which is what makes the extension
349
+ auto-load; presence in `node_modules` alone does nothing. `/grants init` asks only about the capabilities
350
+ that can change your machine and applies the answer to the running session, no restart (ADR-0030).
351
+ `npx pi-daddy init` does the same scaffolding from a shell, and both search pi's install root as well as
352
+ the project's.
353
+
346
354
  ```
347
355
  found principal-pi-skills@2.3.1 — 7 skill(s), 0 declaring allowed-tools
348
356
  wrote .pi/skills/decide/SKILL.md
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG;AAyBH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,oFAAoF;IACpF,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAoCpD;AA4HD,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA0B1D"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG;AAyBH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,oFAAoF;IACpF,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAoCpD;AAkID,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA0B1D"}
package/dist/cli.js CHANGED
@@ -18,7 +18,7 @@ import { relative, resolve as resolvePath } from "node:path";
18
18
  import { pathToFileURL } from "node:url";
19
19
  import { UnsafeGrantError } from "./grant-env.js";
20
20
  import { applyInit, countDeclaring, planInit } from "./init.js";
21
- import { discoverSkillPackages } from "./skill-packages.js";
21
+ import { discoverSkillPackages, skillPackageRoots } from "./skill-packages.js";
22
22
  const USAGE = `pi-daddy — capability governance for pi sub-agents
23
23
 
24
24
  Usage:
@@ -85,9 +85,15 @@ export function parseArgs(argv) {
85
85
  async function init(cwd, force) {
86
86
  const packages = await discoverSkillPackages(cwd);
87
87
  if (packages.length === 0) {
88
- console.log(`pi-daddy init: no installed package under ${cwd}/node_modules declares skills\n` +
89
- `(a package.json "pi": {"skills": [...]} field). Nothing to scaffold.\n\n` +
90
- ` npm install principal-pi-skills # seven skills; then re-run this`);
88
+ // Names BOTH roots it looked in, and offers pi's own install command first. The previous message named
89
+ // only `<cwd>/node_modules` and said "npm install …" so an operator who had just run
90
+ // `pi install npm:principal-pi-skills`, which installs to the agent root, was told to install a package
91
+ // they had already installed (R-75).
92
+ console.log(`pi-daddy init: no installed package declares skills (a package.json "pi": {"skills": [...]} ` +
93
+ `field). Nothing to scaffold.\n\nLooked in:\n` +
94
+ skillPackageRoots(cwd).map((r) => ` ${r}\n`).join("") +
95
+ `\n pi install npm:principal-pi-skills # seven skills, and registers it with pi\n` +
96
+ ` npm install principal-pi-skills # or pin it in this project instead`);
91
97
  return 0;
92
98
  }
93
99
  let plan;
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The grant for a directory, stored **outside** it.
3
+ *
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.
7
+ *
8
+ * **Outside the workspace, and that is the whole design.** A grant is a ceiling; a ceiling a governed child
9
+ * can rewrite is not a ceiling. `<cwd>/.pi/grants.env` is writable by any child holding `tool:write`, so
10
+ * storing the live grant there would let a child widen the *next* session's ceiling — ADR-0014's
11
+ * self-defeating case verbatim, which is why persisted approvals were moved out of the workspace in the
12
+ * first place. This reuses that pattern exactly: `$PI_CODING_AGENT_DIR/grants/<slug>-<hash>.json`, keyed by
13
+ * the directory, unwritable by a narrowed child because a narrowed child holds no write access to `$HOME`.
14
+ *
15
+ * **It does not defend against a child holding `bash`** (ADR-0012). Nothing here does.
16
+ *
17
+ * `.pi/grants.env` is still written by `init` and is still worth committing — it is the *reviewable record*
18
+ * of the decision, diffable in a PR. It is simply no longer the thing the enforcer reads.
19
+ */
20
+ import type { Capability } from "./resolve.ts";
21
+ /**
22
+ * Where this directory's grant lives.
23
+ *
24
+ * Slug plus a 64-bit hash, exactly as `approvalsPath` does it and for the same two reasons: the basename
25
+ * keeps the directory legible to a human reading it, and the hash is what makes it unambiguous, since two
26
+ * checkouts can share a basename.
27
+ */
28
+ export declare function grantStorePath(cwd: string): string;
29
+ /**
30
+ * Parse a store file's text into a grant, or null.
31
+ *
32
+ * Split out from the readers so the **one** validation lives in one place: both the sync and async paths
33
+ * must agree, and two parsers is how they come to disagree.
34
+ *
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.
38
+ */
39
+ export declare function parseGrantFile(text: string, cwd: string): Capability[] | null;
40
+ /**
41
+ * Read this directory's stored grant, synchronously.
42
+ *
43
+ * **Sync on purpose, and this is the constraint that shapes the feature.** Whether `delegate` is registered
44
+ * at all is decided when the extension factory runs (S-5: a session without `tool:delegate` must not be
45
+ * offered the tool), and that is before any `await` is possible. An async read would resolve after the
46
+ * decision it exists to inform, so the store would silently fail to grant delegation — the exact class of
47
+ * defect R-38 and R-39 were.
48
+ */
49
+ export declare function loadGrantSync(cwd: string): Capability[] | null;
50
+ /** Async twin, for callers that already have one. Same parser, so they cannot disagree. */
51
+ export declare function loadGrant(cwd: string): Promise<Capability[] | null>;
52
+ export type SaveOutcome = "saved" | "failed" | "busy";
53
+ /**
54
+ * Write this directory's grant.
55
+ *
56
+ * Locked with the same lock the ledger and the approval store use, for the same reason: two sessions in one
57
+ * directory running `/grants init` concurrently must not interleave. A lock this cannot take yields `busy`
58
+ * and changes nothing — the caller reports it and the operator retries, which is the honest outcome for a
59
+ * write that never looked at the file (R-68).
60
+ *
61
+ * `wx` on the temp file refuses to follow a pre-existing symlink, and `rename` is atomic within a
62
+ * filesystem, so a reader never sees a half-written grant. Both copied from `writeFileSafely`, deliberately
63
+ * — a second, subtly different atomic-write is how the two come to disagree about what "safe" meant.
64
+ */
65
+ export declare function saveGrant(cwd: string, grant: Capability[]): Promise<SaveOutcome>;
66
+ /** Remove this directory's stored grant. True when a file was there to remove. */
67
+ export declare function clearGrant(cwd: string): Promise<boolean>;
68
+ //# sourceMappingURL=grant-store.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The grant for a directory, stored **outside** it.
3
+ *
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.
7
+ *
8
+ * **Outside the workspace, and that is the whole design.** A grant is a ceiling; a ceiling a governed child
9
+ * can rewrite is not a ceiling. `<cwd>/.pi/grants.env` is writable by any child holding `tool:write`, so
10
+ * storing the live grant there would let a child widen the *next* session's ceiling — ADR-0014's
11
+ * self-defeating case verbatim, which is why persisted approvals were moved out of the workspace in the
12
+ * first place. This reuses that pattern exactly: `$PI_CODING_AGENT_DIR/grants/<slug>-<hash>.json`, keyed by
13
+ * the directory, unwritable by a narrowed child because a narrowed child holds no write access to `$HOME`.
14
+ *
15
+ * **It does not defend against a child holding `bash`** (ADR-0012). Nothing here does.
16
+ *
17
+ * `.pi/grants.env` is still written by `init` and is still worth committing — it is the *reviewable record*
18
+ * of the decision, diffable in a PR. It is simply no longer the thing the enforcer reads.
19
+ */
20
+ import { createHash } from "node:crypto";
21
+ import { readFileSync } from "node:fs";
22
+ import { mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
23
+ import { randomUUID } from "node:crypto";
24
+ import { homedir } from "node:os";
25
+ import { basename, dirname, join } from "node:path";
26
+ import { withFileLock, LockTimeoutError } from "./file-lock.js";
27
+ /** `$PI_CODING_AGENT_DIR`, or pi's default. Same resolution as the approval store. */
28
+ function agentDir() {
29
+ return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
30
+ }
31
+ /**
32
+ * Where this directory's grant lives.
33
+ *
34
+ * Slug plus a 64-bit hash, exactly as `approvalsPath` does it and for the same two reasons: the basename
35
+ * keeps the directory legible to a human reading it, and the hash is what makes it unambiguous, since two
36
+ * checkouts can share a basename.
37
+ */
38
+ export function grantStorePath(cwd) {
39
+ const slug = (basename(cwd) || "root").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40);
40
+ const hash = createHash("sha256").update(cwd, "utf8").digest("hex").slice(0, 16);
41
+ return join(agentDir(), "grants", `${slug}-${hash}.json`);
42
+ }
43
+ /**
44
+ * Parse a store file's text into a grant, or null.
45
+ *
46
+ * Split out from the readers so the **one** validation lives in one place: both the sync and async paths
47
+ * must agree, and two parsers is how they come to disagree.
48
+ *
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.
52
+ */
53
+ export function parseGrantFile(text, cwd) {
54
+ try {
55
+ const parsed = JSON.parse(text);
56
+ if (parsed.version !== 1)
57
+ return null;
58
+ if (parsed.cwd !== cwd)
59
+ return null;
60
+ if (!Array.isArray(parsed.grant))
61
+ return null;
62
+ if (!parsed.grant.every((c) => typeof c === "string" && c.length > 0))
63
+ return null;
64
+ return parsed.grant;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ /**
71
+ * Read this directory's stored grant, synchronously.
72
+ *
73
+ * **Sync on purpose, and this is the constraint that shapes the feature.** Whether `delegate` is registered
74
+ * at all is decided when the extension factory runs (S-5: a session without `tool:delegate` must not be
75
+ * offered the tool), and that is before any `await` is possible. An async read would resolve after the
76
+ * decision it exists to inform, so the store would silently fail to grant delegation — the exact class of
77
+ * defect R-38 and R-39 were.
78
+ */
79
+ export function loadGrantSync(cwd) {
80
+ try {
81
+ return parseGrantFile(readFileSync(grantStorePath(cwd), "utf8"), cwd);
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /** Async twin, for callers that already have one. Same parser, so they cannot disagree. */
88
+ export async function loadGrant(cwd) {
89
+ try {
90
+ return parseGrantFile(await readFile(grantStorePath(cwd), "utf8"), cwd);
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ /**
97
+ * Write this directory's grant.
98
+ *
99
+ * Locked with the same lock the ledger and the approval store use, for the same reason: two sessions in one
100
+ * directory running `/grants init` concurrently must not interleave. A lock this cannot take yields `busy`
101
+ * and changes nothing — the caller reports it and the operator retries, which is the honest outcome for a
102
+ * write that never looked at the file (R-68).
103
+ *
104
+ * `wx` on the temp file refuses to follow a pre-existing symlink, and `rename` is atomic within a
105
+ * filesystem, so a reader never sees a half-written grant. Both copied from `writeFileSafely`, deliberately
106
+ * — a second, subtly different atomic-write is how the two come to disagree about what "safe" meant.
107
+ */
108
+ export async function saveGrant(cwd, grant) {
109
+ const path = grantStorePath(cwd);
110
+ const file = { version: 1, cwd, grant: [...grant].sort(), writtenAt: new Date().toISOString() };
111
+ try {
112
+ await mkdir(dirname(path), { recursive: true });
113
+ return await withFileLock(path, "grant store", async () => {
114
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
115
+ try {
116
+ await writeFile(temp, `${JSON.stringify(file, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
117
+ await rename(temp, path);
118
+ return "saved";
119
+ }
120
+ catch {
121
+ await unlink(temp).catch(() => undefined);
122
+ return "failed";
123
+ }
124
+ });
125
+ }
126
+ catch (error) {
127
+ return error instanceof LockTimeoutError ? "busy" : "failed";
128
+ }
129
+ }
130
+ /** Remove this directory's stored grant. True when a file was there to remove. */
131
+ export async function clearGrant(cwd) {
132
+ const path = grantStorePath(cwd);
133
+ try {
134
+ await readFile(path, "utf8");
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ await rm(path, { force: true }).catch(() => undefined);
140
+ return true;
141
+ }
142
+ //# sourceMappingURL=grant-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grant-store.js","sourceRoot":"","sources":["../src/grant-store.ts"],"names":[],"mappings":"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"}
@@ -108,5 +108,21 @@ export declare function readSkillPackage(packageDir: string): Promise<SkillPacka
108
108
  * `node_modules`: a transitive skill package is not something an operator asked to install definitions
109
109
  * from, and scaffolding one into their project would be a surprise wearing a helpful face.
110
110
  */
111
+ /**
112
+ * Every place a pi package can be installed, most specific first (R-75).
113
+ *
114
+ * **Two roots, because pi has two install paths and only one of them is `npm install`.** `pi install
115
+ * npm:principal-pi-skills` — the documented, pi-native way, and the only one that registers the package so
116
+ * pi will auto-load its extension — puts it in `$PI_CODING_AGENT_DIR/npm/node_modules`, and leaves the
117
+ * project without a `node_modules` at all. Searching only `<cwd>/node_modules` therefore found **nothing**
118
+ * for an operator who followed pi's own instructions, and said "install principal-pi-skills" to somebody
119
+ * who just had.
120
+ *
121
+ * Measured, in a fresh `PI_CODING_AGENT_DIR` with an empty project: `pi install` populates the agent root
122
+ * and creates no project root.
123
+ *
124
+ * Project first when both exist, because a package pinned in the repository is the one the team agreed on.
125
+ */
126
+ export declare function skillPackageRoots(cwd: string): string[];
111
127
  export declare function discoverSkillPackages(cwd: string): Promise<SkillPackage[]>;
112
128
  //# sourceMappingURL=skill-packages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"skill-packages.d.ts","sourceRoot":"","sources":["../src/skill-packages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,EAA8C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEpG,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/D,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,2GAA2G;IAC3G,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6FAA6F;AAC7F,MAAM,MAAM,aAAa;AACvB,uFAAuF;AACrF,aAAa;AACf,yFAAyF;GACvF,mBAAmB;AACrB,yGAAyG;GACvG,UAAU;AACZ,iFAAiF;GAC/E,UAAU,CAAC;AAEf,MAAM,WAAW,YAAY;IAC3B,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;IACtB,sDAAsD;IACtD,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,yGAAyG;IACzG,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,uGAAuG;IACvG,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,UAAU,GAAG,OAAO,CAMxD;AA2DD,2GAA2G;AAC3G,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAkCvF;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA6BhF"}
1
+ {"version":3,"file":"skill-packages.d.ts","sourceRoot":"","sources":["../src/skill-packages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAKH,OAAO,EAA8C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEpG,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/D,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,2GAA2G;IAC3G,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6FAA6F;AAC7F,MAAM,MAAM,aAAa;AACvB,uFAAuF;AACrF,aAAa;AACf,yFAAyF;GACvF,mBAAmB;AACrB,yGAAyG;GACvG,UAAU;AACZ,iFAAiF;GAC/E,UAAU,CAAC;AAEf,MAAM,WAAW,YAAY;IAC3B,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;IACtB,sDAAsD;IACtD,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,yGAAyG;IACzG,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,uGAAuG;IACvG,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,UAAU,GAAG,OAAO,CAMxD;AA2DD,2GAA2G;AAC3G,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAkCvF;AAED;;;;;;GAMG;AACH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAGvD;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBhF"}
@@ -25,6 +25,7 @@
25
25
  * `source`s; the only strings that may reach it are ones that survived a whitelist here.
26
26
  */
27
27
  import { readdir, readFile, realpath } from "node:fs/promises";
28
+ import { homedir } from "node:os";
28
29
  import { join, resolve, sep } from "node:path";
29
30
  import { ceilingForDefinition, parseSkillDefinition } from "./definitions.js";
30
31
  import { WILDCARD } from "./pi-tools.js";
@@ -179,16 +180,51 @@ export async function readSkillPackage(packageDir) {
179
180
  * `node_modules`: a transitive skill package is not something an operator asked to install definitions
180
181
  * from, and scaffolding one into their project would be a surprise wearing a helpful face.
181
182
  */
183
+ /**
184
+ * Every place a pi package can be installed, most specific first (R-75).
185
+ *
186
+ * **Two roots, because pi has two install paths and only one of them is `npm install`.** `pi install
187
+ * npm:principal-pi-skills` — the documented, pi-native way, and the only one that registers the package so
188
+ * pi will auto-load its extension — puts it in `$PI_CODING_AGENT_DIR/npm/node_modules`, and leaves the
189
+ * project without a `node_modules` at all. Searching only `<cwd>/node_modules` therefore found **nothing**
190
+ * for an operator who followed pi's own instructions, and said "install principal-pi-skills" to somebody
191
+ * who just had.
192
+ *
193
+ * Measured, in a fresh `PI_CODING_AGENT_DIR` with an empty project: `pi install` populates the agent root
194
+ * and creates no project root.
195
+ *
196
+ * Project first when both exist, because a package pinned in the repository is the one the team agreed on.
197
+ */
198
+ export function skillPackageRoots(cwd) {
199
+ const agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
200
+ return [join(cwd, "node_modules"), join(agentDir, "npm", "node_modules")];
201
+ }
182
202
  export async function discoverSkillPackages(cwd) {
183
- const root = join(cwd, "node_modules");
203
+ const dirs = [];
204
+ const seenNames = new Set();
205
+ for (const root of skillPackageRoots(cwd))
206
+ await collectFrom(root, dirs);
207
+ const packages = [];
208
+ for (const dir of dirs) {
209
+ const found = await readSkillPackage(dir);
210
+ // First root wins on a name collision: the project's pinned copy outranks the machine-wide one, and
211
+ // silently preferring the other would make a committed lockfile stop meaning anything.
212
+ if (found && !seenNames.has(found.name)) {
213
+ seenNames.add(found.name);
214
+ packages.push(found);
215
+ }
216
+ }
217
+ return packages.sort((a, b) => a.name.localeCompare(b.name));
218
+ }
219
+ /** Append every package directory under one `node_modules`, scoped packages included. */
220
+ async function collectFrom(root, dirs) {
184
221
  let entries;
185
222
  try {
186
223
  entries = await readdir(root);
187
224
  }
188
225
  catch {
189
- return []; // no node_modules is a normal state, not a failure
226
+ return; // a root that does not exist is a normal state, not a failure
190
227
  }
191
- const dirs = [];
192
228
  for (const entry of entries.sort()) {
193
229
  if (entry.startsWith("."))
194
230
  continue; // .bin, .package-lock.json
@@ -205,12 +241,5 @@ export async function discoverSkillPackages(cwd) {
205
241
  dirs.push(join(root, entry));
206
242
  }
207
243
  }
208
- const packages = [];
209
- for (const dir of dirs) {
210
- const found = await readSkillPackage(dir);
211
- if (found)
212
- packages.push(found);
213
- }
214
- return packages.sort((a, b) => a.name.localeCompare(b.name));
215
244
  }
216
245
  //# sourceMappingURL=skill-packages.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"skill-packages.js","sourceRoot":"","sources":["../src/skill-packages.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"skill-packages.js","sourceRoot":"","sources":["../src/skill-packages.ts"],"names":[],"mappings":"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"}
@@ -43,6 +43,15 @@ export interface GrantsCommandContext {
43
43
  * R-28 discipline where it belongs: this command asks what would happen instead of working it out.
44
44
  */
45
45
  previewDelegation: (name: string) => Promise<GatedPlan>;
46
+ /**
47
+ * Run `/grants init`: scaffold definitions, ask about the withheld capabilities only, store the answer
48
+ * outside the workspace, and apply it to this session (ADR-0030).
49
+ *
50
+ * Injected rather than imported so this module stays what it is — a read-only diagnostic that cannot
51
+ * become a governance path by accident. The one command here that *writes* takes its ability to do so
52
+ * from the caller, visibly.
53
+ */
54
+ runInit: () => Promise<void>;
46
55
  }
47
56
 
48
57
  /**
@@ -51,6 +60,9 @@ export interface GrantsCommandContext {
51
60
  */
52
61
  const PREVIEW_LIMIT = 12;
53
62
 
63
+ /** The verbs `/grants` answers to. Anything else is refused rather than silently treated as no verb. */
64
+ const KNOWN_SUBCOMMANDS: readonly string[] = ["init", "ledger", "approvals", "revoke"];
65
+
54
66
  export const grantsCommand = {
55
67
  description:
56
68
  "Show this session's capability grant, delegation depth, and known agent-type ceilings; " +
@@ -65,6 +77,11 @@ handler: async (args: string, ctx: any) => {
65
77
 
66
78
  const [sub, target] = args.trim().split(/\s+/).filter(Boolean);
67
79
 
80
+ if (sub === "init") {
81
+ await ctx.grants.runInit();
82
+ return;
83
+ }
84
+
68
85
  if (sub === "ledger") {
69
86
  // The detector, made reachable. `verifyLedger` exists because nothing in this package had ever read
70
87
  // a ledger back, so a torn line was indistinguishable from a spawn that never happened — and a
@@ -265,6 +282,20 @@ handler: async (args: string, ctx: any) => {
265
282
  return;
266
283
  }
267
284
 
285
+ // **An unrecognised subcommand is refused, not ignored (R-74).** Every `sub` that is not a known verb
286
+ // fell through to this status screen with the word silently dropped, so `/grants init` — a command that
287
+ // does not exist — printed a healthy-looking report and read as though it had run. `/grants ledgr` did
288
+ // the same. A diagnostic that answers a question nobody asked is worse than one that refuses: it is
289
+ // indistinguishable from success, which is the shape this package spends most of its risk register on.
290
+ if (sub !== undefined && !KNOWN_SUBCOMMANDS.includes(sub)) {
291
+ ctx.ui.notify(
292
+ `grants: unknown subcommand "${sub}" — did nothing. Known: ${KNOWN_SUBCOMMANDS.join(", ")}. ` +
293
+ `Plain /grants shows this session's status.`,
294
+ "warning",
295
+ );
296
+ return;
297
+ }
298
+
268
299
  const { valid } = await loadApprovals({ cwd, now: new Date(), snapshotOf });
269
300
  const lines = [
270
301
  governed ? "grants: ACTIVE" : "grants: inactive (set PI_GRANTS_GRANT to govern this session)",
@@ -30,12 +30,16 @@ import { legacyApprovalsPath, sharedApprovalsPath } from "../src/approval-store.
30
30
  import { buildCatalog } from "../src/catalog.ts";
31
31
  import { loadDefinitions } from "../src/definitions.ts";
32
32
  import { appendRecord, buildRecord, verifyLedger } from "../src/ledger.ts";
33
- import { deriveOwnGrant, observeToolNames } from "../src/propagation.ts";
33
+ import {
34
+ ENV_GRANT, deriveOwnGrant, observeToolNames } from "../src/propagation.ts";
34
35
  import { snapshotOf } from "./approvals.ts";
35
36
  import { registerDelegationTools } from "./delegation.ts";
36
37
  import { grantsCommand } from "./grants-command.ts";
38
+ import { runInit } from "./init-command.ts";
39
+ import type { Capability } from "../src/resolve.ts";
40
+
37
41
  import { planWithApprovals } from "./run-delegation.ts";
38
- import { createGrantsSession } from "./session.ts";
42
+ import { createGrantsSession, loadProjectDefinitions, type GrantsSession } from "./session.ts";
39
43
  import { renderSpawnableSummary, summariseSpawnable } from "./spawn-summary.ts";
40
44
 
41
45
  const SPAWN_TOOLS = new Set(["Agent", "subagent", "spawn_agent"]);
@@ -60,6 +64,20 @@ export default function (pi: ExtensionAPI) {
60
64
 
61
65
  pi.on("session_start", async (_event, ctx) => {
62
66
  session.cwd = ctx.cwd;
67
+ // The one case the stored-grant lookup can get wrong (ADR-0030). The factory reads the store keyed by
68
+ // `process.cwd()` because it runs before any hook and therefore before `ctx` exists — and S-5 forces
69
+ // that ordering, since whether `delegate` is registered is decided there. Almost always the two agree.
70
+ // When they do not, the session is governed by a different directory's decision than the one it is
71
+ // working in, which is exactly the confusion a grant must never cause, so it is said out loud rather
72
+ // than left to be inferred from a surprising refusal. Sync, so it is not the R-60 shape.
73
+ if (session.storeCwd !== ctx.cwd && process.env[ENV_GRANT] === undefined) {
74
+ ctx.ui.notify(
75
+ `grants: this session's stored grant was read for ${session.storeCwd}, but pi is working in ` +
76
+ `${ctx.cwd}. A grant belongs to a directory, so run /grants init here, or set PI_GRANTS_GRANT ` +
77
+ `explicitly — the environment always wins.`,
78
+ "warning",
79
+ );
80
+ }
63
81
  try {
64
82
  // Guarded together, and guarded at all because of R-60 rather than because either one throws today:
65
83
  // both loaders swallow their own filesystem errors, so this catch is currently unreachable. The point
@@ -69,9 +87,7 @@ export default function (pi: ExtensionAPI) {
69
87
  // delegate by `tools:`, and an operator whose `agent:` spawns have all started failing deserves to
70
88
  // know it was the *scan* that broke rather than the grant.
71
89
  try {
72
- session.definitions = await loadDefinitions(ctx.cwd);
73
- session.catalogReady = buildCatalog({ cwd: ctx.cwd, observedTools: session.observedTools });
74
- session.catalog = await session.catalogReady;
90
+ await loadProjectDefinitions(session, ctx.cwd);
75
91
  } catch (error) {
76
92
  ctx.ui.notify(
77
93
  `grants: could not read this project's definitions or capability catalog ` +
@@ -368,9 +384,15 @@ export default function (pi: ExtensionAPI) {
368
384
  // says so: stored approvals count exactly as they would for a spawn, and no human is asked
369
385
  // (R-38). Passing `ctx` here would let `/grants` raise a dialog, and passing `hasUI: false` would
370
386
  // make every gated definition report "no interactive user" instead of what actually blocks it.
387
+ runInit: () =>
388
+ runInit(session, ctx, async () => {
389
+ await loadProjectDefinitions(session, ctx.cwd);
390
+ delegation.refreshSpawnable();
391
+ }),
371
392
  previewDelegation: (name: string) =>
372
393
  planWithApprovals(session, { task: "(preview)", agent: name }, {}, null),
373
394
  },
374
395
  }),
375
396
  });
376
397
  }
398
+
@@ -0,0 +1,132 @@
1
+ /**
2
+ * `/grants init` — the one command in this package that writes.
3
+ *
4
+ * Split out of `extensions/grants.ts` when the file-size guard refused it, and the seam is the right one:
5
+ * `grants.ts` is wiring, and this is a decision procedure that asks a human questions and stores the
6
+ * answer. Keeping it here means the file that registers hooks stays readable, which is the property the
7
+ * guard exists to defend — every wiring bug this package has had lived in that file.
8
+ */
9
+
10
+ import { discoverSkillPackages } from "../src/skill-packages.ts";
11
+ import { applyInit, planInit } from "../src/init.ts";
12
+ import { saveGrant, grantStorePath } from "../src/grant-store.ts";
13
+ import { expandSubsumed, SUBSUMPTION, type Capability } from "../src/resolve.ts";
14
+ import type { GrantsSession } from "./session.ts";
15
+
16
+ /**
17
+ * `/grants init` — scaffold, ask about what is withheld, store it outside the workspace, apply it now.
18
+ *
19
+ * **The dialog covers the withheld capabilities and nothing else** (ADR-0030). Asking about all of them
20
+ * would be a dozen questions for a first run, and this project has a name for what that produces: R-25,
21
+ * where the operator learns to click through and the control becomes decorative. The read-only capabilities
22
+ * a skill declares are already bounded by the ceiling its author wrote and by pi's `--tools`; the ones that
23
+ * can change the machine are the decision, so they are the question.
24
+ *
25
+ * A refusal is not a failure. Answering *no* to `tool:bash` leaves four definitions unspawnable and says so
26
+ * — that is the same outcome `pi-daddy init` writes by default, reached deliberately rather than by
27
+ * omission.
28
+ */
29
+ export async function runInit(
30
+ session: GrantsSession,
31
+ ctx: any,
32
+ /**
33
+ * Reload definitions and the catalog, and re-describe the delegation tools.
34
+ *
35
+ * **Without this the grant goes live and the definitions do not** — `session.definitions` and the catalog
36
+ * are read at `session_start`, which is before `init` wrote a single file, so a session would hold
37
+ * `agent:review` while believing no definition of that name exists. `/grants` showed `0 skill,
38
+ * 0 agent-type` and no verdicts, and the model would have been told `Available: none` — R-39 exactly,
39
+ * reintroduced by a feature whose whole selling point is "no restart". Found by running it.
40
+ */
41
+ refresh: () => Promise<void>,
42
+ ): Promise<void> {
43
+ const packages = await discoverSkillPackages(ctx.cwd);
44
+ if (packages.length === 0) {
45
+ ctx.ui.notify(
46
+ "grants: no packages declaring skills found in node_modules. Install one — e.g. " +
47
+ "`npm i principal-pi-skills` — then run /grants init again.",
48
+ "warning",
49
+ );
50
+ return;
51
+ }
52
+
53
+ const plan = planInit(packages, ctx.cwd);
54
+ const outcome = await applyInit(plan);
55
+ const lines = [
56
+ `grants: ${plan.skills.length} definition(s) from ${packages.map((p) => `${p.name}@${p.version}`).join(", ")}`,
57
+ ` wrote ${outcome.written.length}, kept ${outcome.kept.length} already present` +
58
+ `${outcome.failed.length > 0 ? `, ${outcome.failed.length} FAILED` : ""}`,
59
+ ];
60
+ for (const f of outcome.failed) lines.push(` ${f.path}: ${f.error}`);
61
+
62
+ // The grant `init` would have written to `.pi/grants.env`: read-only, nothing that can change the
63
+ // machine. Everything below is added to it only by an explicit yes.
64
+ const grant = new Set<Capability>(plan.grant);
65
+ const granted: string[] = [];
66
+ const declined: string[] = [];
67
+ /** Withheld capabilities a previous *yes* already conferred, so no question was asked about them. */
68
+ const alreadyConferred: string[] = [];
69
+
70
+ for (const [capability, neededBy] of plan.withheldCapabilities) {
71
+ // **Do not ask a question whose answer cannot matter.** `tool:bash` subsumes `write`, `edit` and
72
+ // `edit-diff` (`SUBSUMPTION`, `src/resolve.ts`), so once bash is granted those are already conferred.
73
+ // The first version asked anyway: an operator could answer *no* to `tool:write`, watch `/grants` allow
74
+ // `build` with `tool:write`, and reasonably conclude the dialog was decorative. It was — that is R-47's
75
+ // shape, a control that appears to do something and does not, inside a control built to prevent it.
76
+ //
77
+ // Reported rather than silently skipped, because "you already granted this" is the useful sentence and
78
+ // silence is what made it confusing.
79
+ if (expandSubsumed([...grant]).includes(capability)) {
80
+ alreadyConferred.push(`${capability} (via ${subsumedBy([...grant], capability) ?? "a granted capability"})`);
81
+ for (const name of neededBy) grant.add(`agent:${name}` as Capability);
82
+ continue;
83
+ }
84
+ const answer = await ctx.ui.select(
85
+ `grants: grant ${capability} to sub-agents?\n needed by: ${neededBy.join(", ")}\n` +
86
+ ` this can change your machine — ${capability === "tool:bash" ? "and bash also confers write and edit" : "it is not gated, so no dialog at spawn time"}`,
87
+ ["No", "Yes"],
88
+ );
89
+ if (answer === "Yes") {
90
+ grant.add(capability);
91
+ granted.push(capability);
92
+ // The `agent:` ids too: a definition authorised but unable to receive what it declares would be
93
+ // allowed to run and then refused, which is a worse answer than not being authorised (ADR-0029).
94
+ for (const name of neededBy) grant.add(`agent:${name}` as Capability);
95
+ } else {
96
+ declined.push(`${capability} (${neededBy.join(", ")})`);
97
+ }
98
+ }
99
+
100
+ const finalGrant = [...grant].sort();
101
+ const saved = await saveGrant(ctx.cwd, finalGrant);
102
+ if (saved !== "saved") {
103
+ lines.push(
104
+ ` NOT STORED — ${saved === "busy" ? "another session holds the grant store" : "the store could not be written"}. ` +
105
+ `Nothing was changed; this session's grant is unchanged. Retry, or export PI_GRANTS_GRANT yourself.`,
106
+ );
107
+ ctx.ui.notify(lines.join("\n"), "error");
108
+ return;
109
+ }
110
+
111
+ // Live, without a restart — the whole point of the store. Only after a successful write, so what runs and
112
+ // what is recorded cannot disagree.
113
+ session.adoptGrant(finalGrant);
114
+ // Order matters: the grant first, then the reload, because `refreshSpawnable` filters the definitions it
115
+ // advertises through `maySpawnDefinition` against the grant the session now holds.
116
+ await refresh();
117
+
118
+ lines.push(` stored at ${grantStorePath(ctx.cwd)} — outside this project, so no child can rewrite it`);
119
+ if (granted.length > 0) lines.push(` GRANTED: ${granted.join(", ")}`);
120
+ if (alreadyConferred.length > 0) {
121
+ lines.push(` ALREADY CONFERRED, not asked about: ${alreadyConferred.join(", ")}`);
122
+ }
123
+ if (declined.length > 0) lines.push(` withheld: ${declined.join("; ")}`);
124
+ lines.push(` live now (${finalGrant.length} capabilities) — no restart. /grants shows the verdicts.`);
125
+ ctx.ui.notify(lines.join("\n"), "info");
126
+ }
127
+
128
+ /** Which held capability confers `capability`, for a message that names the cause rather than the effect. */
129
+ function subsumedBy(held: Capability[], capability: Capability): Capability | null {
130
+ for (const h of held) if ((SUBSUMPTION[h] ?? []).includes(capability)) return h;
131
+ return null;
132
+ }
@@ -37,6 +37,9 @@ import {
37
37
  parseList,
38
38
  } from "../src/propagation.ts";
39
39
  import type { Capability } from "../src/resolve.ts";
40
+ import { loadDefinitions } from "../src/definitions.ts";
41
+ import { buildCatalog } from "../src/catalog.ts";
42
+ import { loadGrantSync, grantStorePath } from "../src/grant-store.ts";
40
43
  import { republishable } from "./approvals.ts";
41
44
 
42
45
  /**
@@ -54,8 +57,17 @@ export const ENV_HERDR_WORKSPACE = "PI_GRANTS_HERDR_WORKSPACE";
54
57
  export const ENV_HERDR_KEEP_PANE = "PI_GRANTS_HERDR_KEEP_PANE";
55
58
 
56
59
  export interface GrantsSession {
57
- /** False when `PI_GRANTS_GRANT` is unset: the session holds the wildcard and nothing is governed. */
58
- readonly governed: boolean;
60
+ /**
61
+ * False when neither `PI_GRANTS_GRANT` nor a stored grant applies: the session holds the wildcard and
62
+ * nothing is governed.
63
+ *
64
+ * **Mutable, because `/grants init` makes an ungoverned session governed mid-run.** The first version of
65
+ * ADR-0030 left this readonly on the reasoning that a store is read at creation and `init` only runs where
66
+ * one exists — which is false for the first `init` in a directory, the most common case there is. The
67
+ * session then bounded every spawn by the new grant while `/grants` reported "inactive", so the status
68
+ * line contradicted the enforcer. Found by running it.
69
+ */
70
+ governed: boolean;
59
71
  /** The upper bound handed down by the delegator, before this session's own tools are observed. */
60
72
  readonly inherited: Capability[];
61
73
  readonly depth: number;
@@ -129,6 +141,23 @@ export interface GrantsSession {
129
141
  * prompted the human. A value scoped to one specific child is never written to this global channel.
130
142
  */
131
143
  publishChildEnv(): void;
144
+ /**
145
+ * The directory whose stored grant this session read, or would read. `process.cwd()` — see the note in
146
+ * `createGrantsSession` for why the factory cannot use `ctx.cwd`.
147
+ */
148
+ readonly storeCwd: string;
149
+ /**
150
+ * Adopt a grant decided DURING the session — `/grants init` answering a human — without a restart.
151
+ *
152
+ * Narrow by design: it sets the session's own grant and republishes, so the very next spawn is bounded by
153
+ * it. It does **not** reach children that already exist; those are separate processes whose environment
154
+ * was fixed when they started, and reaching into them is neither possible nor desirable — a child's
155
+ * ceiling should not move under it mid-run.
156
+ *
157
+ * Only a human can reach this. Slash commands are user-invoked; no tool exposes it, so a model cannot
158
+ * widen its own session's ceiling by calling something.
159
+ */
160
+ adoptGrant(grant: Capability[]): void;
132
161
  }
133
162
 
134
163
  /**
@@ -138,12 +167,40 @@ export interface GrantsSession {
138
167
  * extension**, so a child granted `tool:delegate` can be started with `-e <that file>`. `grants.ts` is that
139
168
  * file, and only `grants.ts` can say so about itself.
140
169
  */
170
+ /**
171
+ * Load this project's definitions and capability catalog into the session.
172
+ *
173
+ * **One loader, two callers.** `session_start` runs it, and so does `/grants init` — which writes the very
174
+ * files it reads, so a session that skipped this held `agent:review` while believing no definition of that
175
+ * name existed, and the model was told `Available: none` (R-39's shape, reintroduced by the feature whose
176
+ * selling point is "no restart"). Two copies of these three steps is how the two callers come to disagree
177
+ * about what loading means, so there is one.
178
+ */
179
+ export async function loadProjectDefinitions(session: GrantsSession, cwd: string): Promise<void> {
180
+ session.definitions = await loadDefinitions(cwd);
181
+ session.catalogReady = buildCatalog({ cwd, observedTools: session.observedTools });
182
+ session.catalog = await session.catalogReady;
183
+ }
184
+
141
185
  export function createGrantsSession(extensionPath: string | undefined): GrantsSession {
142
- // Governance is opt-in: with PI_GRANTS_GRANT unset the session holds the wildcard and nothing is
143
- // blocked. This extension must never silently tighten a normal workflow.
186
+ // Governance is opt-in: with PI_GRANTS_GRANT unset AND no stored grant for this directory, the session
187
+ // holds the wildcard and nothing is blocked. This extension must never silently tighten a normal
188
+ // workflow.
189
+ //
190
+ // **Two sources, and the environment always wins** (ADR-0030). The variable is how a CHILD is governed
191
+ // and how CI is configured, so a store that could override it would let a directory quietly widen or
192
+ // narrow a child its parent had already bounded. The store is consulted only when the variable is absent,
193
+ // which is exactly the case it was added for: a human at a terminal who ran `/grants init` here.
194
+ //
195
+ // `process.cwd()` rather than `ctx.cwd`, because this runs in the extension factory — before any hook,
196
+ // and therefore before `ctx` exists. That ordering is forced by S-5: whether `delegate` is registered at
197
+ // all is decided here, and a grant arriving later could not inform it. `session_start` re-checks the two
198
+ // against each other and says so if they differ, which is the only case this can get wrong.
144
199
  const grantRaw = process.env[ENV_GRANT];
145
- const governed = grantRaw !== undefined;
146
- const inherited: Capability[] = governed ? parseList(grantRaw) : [WILDCARD];
200
+ const storeCwd = process.cwd();
201
+ const stored = grantRaw === undefined ? loadGrantSync(storeCwd) : null;
202
+ const governed = grantRaw !== undefined || stored !== null;
203
+ const inherited: Capability[] = grantRaw !== undefined ? parseList(grantRaw) : (stored ?? [WILDCARD]);
147
204
  // G7 / A-S4 + B-I4: strict, three-way parsing that fails CLOSED. A malformed bound used to yield
148
205
  // `NaN`, and every comparison against `NaN` is false, so depth limiting switched itself off.
149
206
  const bounds = depthConfig(process.env[ENV_DEPTH], process.env[ENV_MAX_DEPTH]);
@@ -214,6 +271,18 @@ export function createGrantsSession(extensionPath: string | undefined): GrantsSe
214
271
  ...(approved ? { approved } : {}),
215
272
  }),
216
273
 
274
+ storeCwd,
275
+
276
+ adoptGrant: (grant: Capability[]) => {
277
+ // Governed too, not just bounded. A session that starts with no grant and then runs `/grants init` is
278
+ // governed from that moment: every spawn is bounded by what was just stored. Leaving this false made
279
+ // `/grants` print "inactive" while holding thirteen capabilities — a status line contradicting the
280
+ // enforcer, which is the defect R-28 is named for.
281
+ session.governed = true;
282
+ session.ownGrant = grant;
283
+ session.publishChildEnv();
284
+ },
285
+
217
286
  publishChildEnv: () => {
218
287
  const env = childEnv({
219
288
  ownGrant: session.ownGrant,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-daddy",
3
- "version": "0.14.0",
3
+ "version": "0.15.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/cli.ts CHANGED
@@ -19,7 +19,7 @@ import { relative, resolve as resolvePath } from "node:path";
19
19
  import { pathToFileURL } from "node:url";
20
20
  import { UnsafeGrantError } from "./grant-env.ts";
21
21
  import { applyInit, countDeclaring, planInit, type InitPlan } from "./init.ts";
22
- import { discoverSkillPackages, type RefusedSkill, type SkillPackage } from "./skill-packages.ts";
22
+ import { discoverSkillPackages, skillPackageRoots, type RefusedSkill, type SkillPackage } from "./skill-packages.ts";
23
23
 
24
24
  const USAGE = `pi-daddy — capability governance for pi sub-agents
25
25
 
@@ -93,10 +93,16 @@ export function parseArgs(argv: string[]): ParsedArgs {
93
93
  async function init(cwd: string, force: boolean): Promise<number> {
94
94
  const packages = await discoverSkillPackages(cwd);
95
95
  if (packages.length === 0) {
96
+ // Names BOTH roots it looked in, and offers pi's own install command first. The previous message named
97
+ // only `<cwd>/node_modules` and said "npm install …" — so an operator who had just run
98
+ // `pi install npm:principal-pi-skills`, which installs to the agent root, was told to install a package
99
+ // they had already installed (R-75).
96
100
  console.log(
97
- `pi-daddy init: no installed package under ${cwd}/node_modules declares skills\n` +
98
- `(a package.json "pi": {"skills": [...]} field). Nothing to scaffold.\n\n` +
99
- ` npm install principal-pi-skills # seven skills; then re-run this`,
101
+ `pi-daddy init: no installed package declares skills (a package.json "pi": {"skills": [...]} ` +
102
+ `field). Nothing to scaffold.\n\nLooked in:\n` +
103
+ skillPackageRoots(cwd).map((r) => ` ${r}\n`).join("") +
104
+ `\n pi install npm:principal-pi-skills # seven skills, and registers it with pi\n` +
105
+ ` npm install principal-pi-skills # or pin it in this project instead`,
100
106
  );
101
107
  return 0;
102
108
  }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The grant for a directory, stored **outside** it.
3
+ *
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.
7
+ *
8
+ * **Outside the workspace, and that is the whole design.** A grant is a ceiling; a ceiling a governed child
9
+ * can rewrite is not a ceiling. `<cwd>/.pi/grants.env` is writable by any child holding `tool:write`, so
10
+ * storing the live grant there would let a child widen the *next* session's ceiling — ADR-0014's
11
+ * self-defeating case verbatim, which is why persisted approvals were moved out of the workspace in the
12
+ * first place. This reuses that pattern exactly: `$PI_CODING_AGENT_DIR/grants/<slug>-<hash>.json`, keyed by
13
+ * the directory, unwritable by a narrowed child because a narrowed child holds no write access to `$HOME`.
14
+ *
15
+ * **It does not defend against a child holding `bash`** (ADR-0012). Nothing here does.
16
+ *
17
+ * `.pi/grants.env` is still written by `init` and is still worth committing — it is the *reviewable record*
18
+ * of the decision, diffable in a PR. It is simply no longer the thing the enforcer reads.
19
+ */
20
+
21
+ import { createHash } from "node:crypto";
22
+ import { readFileSync } from "node:fs";
23
+ import { mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
24
+ import { randomUUID } from "node:crypto";
25
+ import { homedir } from "node:os";
26
+ import { basename, dirname, join } from "node:path";
27
+ import { withFileLock, LockTimeoutError } from "./file-lock.ts";
28
+ import type { Capability } from "./resolve.ts";
29
+
30
+ interface GrantFile {
31
+ version: 1;
32
+ /** The directory this grant is for. Checked on read — an entry copied elsewhere is refused (R-27). */
33
+ cwd: string;
34
+ grant: Capability[];
35
+ /** When `/grants init` wrote it. Informational; nothing expires. */
36
+ writtenAt: string;
37
+ }
38
+
39
+ /** `$PI_CODING_AGENT_DIR`, or pi's default. Same resolution as the approval store. */
40
+ function agentDir(): string {
41
+ return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
42
+ }
43
+
44
+ /**
45
+ * Where this directory's grant lives.
46
+ *
47
+ * Slug plus a 64-bit hash, exactly as `approvalsPath` does it and for the same two reasons: the basename
48
+ * keeps the directory legible to a human reading it, and the hash is what makes it unambiguous, since two
49
+ * checkouts can share a basename.
50
+ */
51
+ export function grantStorePath(cwd: string): string {
52
+ const slug = (basename(cwd) || "root").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40);
53
+ const hash = createHash("sha256").update(cwd, "utf8").digest("hex").slice(0, 16);
54
+ return join(agentDir(), "grants", `${slug}-${hash}.json`);
55
+ }
56
+
57
+ /**
58
+ * Parse a store file's text into a grant, or null.
59
+ *
60
+ * Split out from the readers so the **one** validation lives in one place: both the sync and async paths
61
+ * must agree, and two parsers is how they come to disagree.
62
+ *
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.
66
+ */
67
+ export function parseGrantFile(text: string, cwd: string): Capability[] | null {
68
+ try {
69
+ const parsed = JSON.parse(text) as Partial<GrantFile>;
70
+ if (parsed.version !== 1) return null;
71
+ if (parsed.cwd !== cwd) return null;
72
+ if (!Array.isArray(parsed.grant)) return null;
73
+ if (!parsed.grant.every((c) => typeof c === "string" && c.length > 0)) return null;
74
+ return parsed.grant as Capability[];
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Read this directory's stored grant, synchronously.
82
+ *
83
+ * **Sync on purpose, and this is the constraint that shapes the feature.** Whether `delegate` is registered
84
+ * at all is decided when the extension factory runs (S-5: a session without `tool:delegate` must not be
85
+ * offered the tool), and that is before any `await` is possible. An async read would resolve after the
86
+ * decision it exists to inform, so the store would silently fail to grant delegation — the exact class of
87
+ * defect R-38 and R-39 were.
88
+ */
89
+ export function loadGrantSync(cwd: string): Capability[] | null {
90
+ try {
91
+ return parseGrantFile(readFileSync(grantStorePath(cwd), "utf8"), cwd);
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /** Async twin, for callers that already have one. Same parser, so they cannot disagree. */
98
+ export async function loadGrant(cwd: string): Promise<Capability[] | null> {
99
+ try {
100
+ return parseGrantFile(await readFile(grantStorePath(cwd), "utf8"), cwd);
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ export type SaveOutcome = "saved" | "failed" | "busy";
107
+
108
+ /**
109
+ * Write this directory's grant.
110
+ *
111
+ * Locked with the same lock the ledger and the approval store use, for the same reason: two sessions in one
112
+ * directory running `/grants init` concurrently must not interleave. A lock this cannot take yields `busy`
113
+ * and changes nothing — the caller reports it and the operator retries, which is the honest outcome for a
114
+ * write that never looked at the file (R-68).
115
+ *
116
+ * `wx` on the temp file refuses to follow a pre-existing symlink, and `rename` is atomic within a
117
+ * filesystem, so a reader never sees a half-written grant. Both copied from `writeFileSafely`, deliberately
118
+ * — a second, subtly different atomic-write is how the two come to disagree about what "safe" meant.
119
+ */
120
+ export async function saveGrant(cwd: string, grant: Capability[]): Promise<SaveOutcome> {
121
+ const path = grantStorePath(cwd);
122
+ const file: GrantFile = { version: 1, cwd, grant: [...grant].sort(), writtenAt: new Date().toISOString() };
123
+ try {
124
+ await mkdir(dirname(path), { recursive: true });
125
+ return await withFileLock(path, "grant store", async () => {
126
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
127
+ try {
128
+ await writeFile(temp, `${JSON.stringify(file, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
129
+ await rename(temp, path);
130
+ return "saved" as const;
131
+ } catch {
132
+ await unlink(temp).catch(() => undefined);
133
+ return "failed" as const;
134
+ }
135
+ });
136
+ } catch (error) {
137
+ return error instanceof LockTimeoutError ? "busy" : "failed";
138
+ }
139
+ }
140
+
141
+ /** Remove this directory's stored grant. True when a file was there to remove. */
142
+ export async function clearGrant(cwd: string): Promise<boolean> {
143
+ const path = grantStorePath(cwd);
144
+ try {
145
+ await readFile(path, "utf8");
146
+ } catch {
147
+ return false;
148
+ }
149
+ await rm(path, { force: true }).catch(() => undefined);
150
+ return true;
151
+ }
@@ -26,6 +26,7 @@
26
26
  */
27
27
 
28
28
  import { readdir, readFile, realpath } from "node:fs/promises";
29
+ import { homedir } from "node:os";
29
30
  import { join, resolve, sep } from "node:path";
30
31
  import { ceilingForDefinition, parseSkillDefinition, type SkillDefinition } from "./definitions.ts";
31
32
  import { WILDCARD } from "./pi-tools.ts";
@@ -219,16 +220,53 @@ export async function readSkillPackage(packageDir: string): Promise<SkillPackage
219
220
  * `node_modules`: a transitive skill package is not something an operator asked to install definitions
220
221
  * from, and scaffolding one into their project would be a surprise wearing a helpful face.
221
222
  */
223
+ /**
224
+ * Every place a pi package can be installed, most specific first (R-75).
225
+ *
226
+ * **Two roots, because pi has two install paths and only one of them is `npm install`.** `pi install
227
+ * npm:principal-pi-skills` — the documented, pi-native way, and the only one that registers the package so
228
+ * pi will auto-load its extension — puts it in `$PI_CODING_AGENT_DIR/npm/node_modules`, and leaves the
229
+ * project without a `node_modules` at all. Searching only `<cwd>/node_modules` therefore found **nothing**
230
+ * for an operator who followed pi's own instructions, and said "install principal-pi-skills" to somebody
231
+ * who just had.
232
+ *
233
+ * Measured, in a fresh `PI_CODING_AGENT_DIR` with an empty project: `pi install` populates the agent root
234
+ * and creates no project root.
235
+ *
236
+ * Project first when both exist, because a package pinned in the repository is the one the team agreed on.
237
+ */
238
+ export function skillPackageRoots(cwd: string): string[] {
239
+ const agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
240
+ return [join(cwd, "node_modules"), join(agentDir, "npm", "node_modules")];
241
+ }
242
+
222
243
  export async function discoverSkillPackages(cwd: string): Promise<SkillPackage[]> {
223
- const root = join(cwd, "node_modules");
244
+ const dirs: string[] = [];
245
+ const seenNames = new Set<string>();
246
+ for (const root of skillPackageRoots(cwd)) await collectFrom(root, dirs);
247
+
248
+ const packages: SkillPackage[] = [];
249
+ for (const dir of dirs) {
250
+ const found = await readSkillPackage(dir);
251
+ // First root wins on a name collision: the project's pinned copy outranks the machine-wide one, and
252
+ // silently preferring the other would make a committed lockfile stop meaning anything.
253
+ if (found && !seenNames.has(found.name)) {
254
+ seenNames.add(found.name);
255
+ packages.push(found);
256
+ }
257
+ }
258
+ return packages.sort((a, b) => a.name.localeCompare(b.name));
259
+ }
260
+
261
+ /** Append every package directory under one `node_modules`, scoped packages included. */
262
+ async function collectFrom(root: string, dirs: string[]): Promise<void> {
224
263
  let entries: string[];
225
264
  try {
226
265
  entries = await readdir(root);
227
266
  } catch {
228
- return []; // no node_modules is a normal state, not a failure
267
+ return; // a root that does not exist is a normal state, not a failure
229
268
  }
230
269
 
231
- const dirs: string[] = [];
232
270
  for (const entry of entries.sort()) {
233
271
  if (entry.startsWith(".")) continue; // .bin, .package-lock.json
234
272
  if (entry.startsWith("@")) {
@@ -241,11 +279,4 @@ export async function discoverSkillPackages(cwd: string): Promise<SkillPackage[]
241
279
  dirs.push(join(root, entry));
242
280
  }
243
281
  }
244
-
245
- const packages: SkillPackage[] = [];
246
- for (const dir of dirs) {
247
- const found = await readSkillPackage(dir);
248
- if (found) packages.push(found);
249
- }
250
- return packages.sort((a, b) => a.name.localeCompare(b.name));
251
282
  }