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 +32 -0
- package/README.md +10 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -4
- package/dist/cli.js.map +1 -1
- package/dist/grant-store.d.ts +68 -0
- package/dist/grant-store.d.ts.map +1 -0
- package/dist/grant-store.js +142 -0
- package/dist/grant-store.js.map +1 -0
- package/dist/skill-packages.d.ts +16 -0
- package/dist/skill-packages.d.ts.map +1 -1
- package/dist/skill-packages.js +39 -10
- package/dist/skill-packages.js.map +1 -1
- package/extensions/grants-command.ts +31 -0
- package/extensions/grants.ts +27 -5
- package/extensions/init-command.ts +132 -0
- package/extensions/session.ts +75 -6
- package/package.json +1 -1
- package/src/cli.ts +10 -4
- package/src/grant-store.ts +151 -0
- package/src/skill-packages.ts +41 -10
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
|
-
|
|
343
|
-
|
|
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;
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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":";AACA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAiB,MAAM,WAAW,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAwC,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAiB,MAAM,WAAW,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAwC,MAAM,qBAAqB,CAAC;AAErH,MAAM,KAAK,GAAG;;;;;;;;;;;;;4FAa8E,CAAC;AAU7F;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,IAAc;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC;IACxF,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACzG,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAE/G,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAChC,IAAI,OAAO,KAAK,MAAM;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,oBAAoB,OAAO,GAAG,CAAC,EAAE,CAAC;IAE3G,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,GAAuB,CAAC;IAC5B,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,KAAK,GAAG,IAAI,CAAC;QACf,CAAC;aAAM,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1B,kGAAkG;YAClG,mGAAmG;YACnG,8DAA8D;YAC9D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;iBAC/E,CAAC;gBACJ,GAAG,GAAG,KAAK,CAAC;gBACZ,CAAC,IAAI,CAAC,CAAC;YACT,CAAC;QACH,CAAC;aAAM,CAAC;YACN,qGAAqG;YACrG,oGAAoG;YACpG,kGAAkG;YAClG,yFAAyF;YACzF,MAAM,CAAC,IAAI,CAAC,kBAAkB,GAAG,EAAE,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AACjD,CAAC;AAED,iFAAiF;AACjF,KAAK,UAAU,IAAI,CAAC,GAAW,EAAE,KAAc;IAC7C,MAAM,QAAQ,GAAG,MAAM,qBAAqB,CAAC,GAAG,CAAC,CAAC;IAClD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,uGAAuG;QACvG,uFAAuF;QACvF,wGAAwG;QACxG,qCAAqC;QACrC,OAAO,CAAC,GAAG,CACT,8FAA8F;YAC5F,8CAA8C;YAC9C,iBAAiB,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACtD,sFAAsF;YACtF,4EAA4E,CAC/E,CAAC;QACF,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,IAAc,CAAC;IACnB,IAAI,CAAC;QACH,IAAI,GAAG,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,mGAAmG;QACnG,mGAAmG;QACnG,IAAI,KAAK,YAAY,gBAAgB,EAAE,CAAC;YACtC,OAAO,CAAC,KAAK,CAAC,kBAAkB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YACjD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,MAAM,SAAS,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5E,OAAO,CAAC,GAAG,CACT,SAAS,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,OAAO,MAAM,GAAG,CAAC,MAAM,CAAC,MAAM,aAAa;YAClE,GAAG,SAAS,0BAA0B;YACtC,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,UAAU,CAAC,MAAM,6BAA6B,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACtH,gGAAgG;YAChG,kDAAkD;YAClD,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC,MAAM,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CACpE,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxF,KAAK,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,OAAO;QAAE,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;IACnF,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,UAAU;QAAE,OAAO,CAAC,GAAG,CAAC,aAAa,SAAS,EAAE,CAAC,CAAC;IAE/E,sGAAsG;IACtG,yDAAyD;IACzD,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;QACpC,OAAO,CAAC,GAAG,CACT,8BAA8B,QAAQ,gBAAgB,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,uBAAuB;YACvG,oGAAoG,CACvG,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC;IAC5D,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,OAAO;QAAE,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxE,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,IAAI;QAAE,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAC;IAC/G,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,MAAM;QAAE,OAAO,CAAC,KAAK,CAAC,UAAU,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IAEvG,MAAM,CAAC,IAAI,CAAC,CAAC;IACb,qGAAqG;IACrG,qFAAqF;IACrF,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACjE,CAAC;AAED,iFAAiF;AACjF,SAAS,aAAa,CAAC,GAAiB,EAAE,OAAqB;IAC7D,MAAM,IAAI,GAAG,WAAW,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;IACvE,QAAQ,OAAO,CAAC,MAAM,EAAE,CAAC;QACvB,KAAK,aAAa;YAChB,OAAO,CACL,GAAG,IAAI,uFAAuF;gBAC9F,8FAA8F,CAC/F,CAAC;QACJ,KAAK,mBAAmB;YACtB,OAAO,CACL,GAAG,IAAI,aAAa,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,kDAAkD;gBAC/F,8FAA8F;gBAC9F,oDAAoD,CACrD,CAAC;QACJ,KAAK,UAAU;YACb,OAAO,CACL,GAAG,IAAI,aAAa,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,uDAAuD;gBACpG,6DAA6D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,mCAAmC,CAAC,CAAC,CAAC,6CAA6C,IAAI;gBACxL,6DAA6D,CAC9D,CAAC;QACJ,KAAK,UAAU;YACb,OAAO,GAAG,IAAI,6FAA6F,CAAC;IAChH,CAAC;AACH,CAAC;AAED,4FAA4F;AAC5F,SAAS,MAAM,CAAC,IAAc;IAC5B,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC;IAC1E,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC;IAEtE,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ;QAAE,OAAO,CAAC,GAAG,CAAC,cAAc,OAAO,EAAE,CAAC,CAAC;IAC1E,KAAK,MAAM,KAAK,IAAI,SAAS;QAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEvG,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,GAAG,CACT,KAAK,UAAU,CAAC,MAAM,0EAA0E;YAC9F,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;YAChD,mGAAmG;YACnG,mGAAmG;YACnG,uCAAuC,CAC1C,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,oBAAoB,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QACvC,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvG,OAAO,CAAC,GAAG,CACT,0BAA0B,MAAM,KAAK;YACnC,iGAAiG;YACjG,kGAAkG,CACrG,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,GAAG,CACT,iBAAiB,IAAI,CAAC,KAAK,CAAC,MAAM,mBAAmB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM;QAC9E,gEAAgE;QAChE,mFAAmF,CACtF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAc;IACvC,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAE/B,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACjC,iGAAiG;QACjG,6FAA6F;QAC7F,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QACpG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;QAC/E,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;QACxG,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACnB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,wGAAwG;IACxG,yGAAyG;IACzG,0GAA0G;IAC1G,+EAA+E;IAC/E,OAAO,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE;IAC5B,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IACzG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC,CAAC,EAAE,CAAC;AACL,IAAI,eAAe,EAAE,CAAC;IACpB,OAAO,CAAC,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -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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"}
|
package/dist/skill-packages.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/skill-packages.js
CHANGED
|
@@ -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
|
|
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
|
|
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)",
|
package/extensions/grants.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
+
}
|
package/extensions/session.ts
CHANGED
|
@@ -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
|
-
/**
|
|
58
|
-
|
|
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
|
|
143
|
-
// blocked. This extension must never silently tighten a normal
|
|
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
|
|
146
|
-
const
|
|
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.
|
|
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
|
|
98
|
-
`
|
|
99
|
-
`
|
|
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
|
+
}
|
package/src/skill-packages.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|