superpowers-manager 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +36 -16
  2. package/dist/atomic.js +1 -3
  3. package/dist/cli-arguments.js +4 -1
  4. package/dist/cli.js +14 -43
  5. package/dist/commands/adapter-call.js +24 -0
  6. package/dist/commands/install.js +16 -63
  7. package/dist/commands/mutation.js +5 -5
  8. package/dist/commands/pin.js +10 -5
  9. package/dist/commands/prepare.js +42 -108
  10. package/dist/commands/probe.js +33 -19
  11. package/dist/commands/uninstall.js +18 -46
  12. package/dist/commands/unpin.js +2 -1
  13. package/dist/commands/update.js +8 -29
  14. package/dist/domain/refs.js +0 -3
  15. package/dist/effective-selection.js +11 -15
  16. package/dist/git-tree.js +4 -6
  17. package/dist/harness-command-result.js +33 -3
  18. package/dist/harnesses/claude-code/harness.js +39 -0
  19. package/dist/harnesses/claude-code/install.js +380 -0
  20. package/dist/harnesses/claude-code/native.js +79 -0
  21. package/dist/harnesses/claude-code/paths.js +35 -0
  22. package/dist/harnesses/claude-code/prepare.js +117 -0
  23. package/dist/harnesses/claude-code/presentation.js +25 -0
  24. package/dist/harnesses/claude-code/state.js +253 -0
  25. package/dist/harnesses/codex/adapter.js +27 -68
  26. package/dist/harnesses/codex/conflicts.js +3 -8
  27. package/dist/harnesses/codex/generated-plugin.js +4 -11
  28. package/dist/harnesses/codex/harness.js +4 -26
  29. package/dist/harnesses/codex/hooks.js +11 -27
  30. package/dist/harnesses/codex/lifecycle.js +22 -107
  31. package/dist/harnesses/codex/manifest-overlay.js +9 -27
  32. package/dist/harnesses/codex/paths.js +29 -0
  33. package/dist/harnesses/codex/prepare.js +9 -43
  34. package/dist/harnesses/codex/presentation.js +1 -1
  35. package/dist/harnesses/codex/publication.js +5 -40
  36. package/dist/harnesses/codex/state.js +1 -6
  37. package/dist/harnesses/codex/stored-state.js +1 -6
  38. package/dist/harnesses/opencode/config.js +2 -4
  39. package/dist/harnesses/opencode/discovery.js +1 -5
  40. package/dist/harnesses/opencode/install.js +12 -93
  41. package/dist/harnesses/opencode/native.js +28 -49
  42. package/dist/harnesses/opencode/package.js +11 -55
  43. package/dist/harnesses/opencode/prepare.js +2 -7
  44. package/dist/harnesses/opencode/presentation.js +2 -7
  45. package/dist/harnesses/pi/install.js +31 -120
  46. package/dist/harnesses/pi/native.js +9 -34
  47. package/dist/harnesses/pi/package.js +9 -50
  48. package/dist/harnesses/pi/prepare.js +3 -8
  49. package/dist/harnesses/pi/presentation.js +2 -7
  50. package/dist/harnesses/pi/settings.js +5 -9
  51. package/dist/harnesses/pi/shared-skills.js +23 -53
  52. package/dist/harnesses/pi/state.js +3 -10
  53. package/dist/provenance.js +3 -19
  54. package/dist/resource-lock.js +11 -16
  55. package/dist/safe-path.js +28 -1
  56. package/dist/selection.js +15 -19
  57. package/dist/snapshot-journal.js +101 -0
  58. package/dist/snapshot-package.js +62 -1
  59. package/dist/snapshot-prepare.js +9 -2
  60. package/dist/snapshot-presentation.js +9 -0
  61. package/dist/upstream.js +8 -12
  62. package/dist/validator.js +26 -23
  63. package/dist/workspace.js +29 -35
  64. package/package.json +10 -9
  65. package/dist/python-json.js +0 -4
  66. package/dist/upstream-workspace.js +0 -8
@@ -1,20 +1,19 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { cp, lstat, mkdir, open, readFile, readdir, rm, rmdir, unlink, } from "node:fs/promises";
3
- import { basename, dirname, join } from "node:path";
2
+ import { cp, mkdir, readdir, rm } from "node:fs/promises";
4
3
  import { failureResult, successResult, } from "../../adapter-result.js";
5
- import { atomicWriteFile, beginDirectoryPublication, } from "../../atomic.js";
4
+ import { beginDirectoryPublication, } from "../../atomic.js";
6
5
  import { digestArtifactTree } from "../../artifact-tree.js";
7
6
  import { normalizeSnapshotRuntimeVersion } from "../../harness-command-result.js";
8
7
  import { runPi } from "./native.js";
9
8
  import { readPiPackageAssessment, readPiReceipt, } from "./package.js";
10
9
  import { piPaths } from "./paths.js";
11
- import { readPiSettings, resolveCanonicalPiLocalSource, } from "./settings.js";
10
+ import { readPiSettings } from "./settings.js";
12
11
  import { inspectPiControl, inspectPiOwnership, } from "./state.js";
13
12
  import { assertNoFollowType, canonicalizeProspectivePath, classifyPathNoFollow, } from "../../safe-path.js";
13
+ import { createJournal, createSnapshotJournal, hasRecovery, identity, journalPath, publicationPaths, requireNoRecovery, } from "../../snapshot-journal.js";
14
14
  const DEFAULTS = {
15
15
  run: runPi,
16
16
  beginPublication: beginDirectoryPublication,
17
- readSettings: readPiSettings,
18
17
  };
19
18
  function fail(code, message) {
20
19
  return failureResult("pi-lifecycle", code, message, [], []);
@@ -24,20 +23,6 @@ function accepted(result) {
24
23
  throw new Error("Pi operation did not succeed");
25
24
  return result.outcome.result;
26
25
  }
27
- function sameIdentity(a, b) {
28
- return a.dev === b.dev && a.ino === b.ino;
29
- }
30
- async function identity(path) {
31
- await assertNoFollowType(path, ["directory"]);
32
- return await lstat(path);
33
- }
34
- async function requireIdentity(path, expected) {
35
- if (!sameIdentity(await identity(path), expected))
36
- throw new Error("Pi directory changed");
37
- }
38
- function journalPath(p) {
39
- return join(p.paths.recoveryRoot, "transaction.json");
40
- }
41
26
  function journalBytes(journal) {
42
27
  return Buffer.from(JSON.stringify(journal) + "\n");
43
28
  }
@@ -47,38 +32,15 @@ async function validatePaths(paths) {
47
32
  await assertNoFollowType(paths.installedRoot, ["directory", "missing"]);
48
33
  return await canonicalizeProspectivePath(paths.installedRoot);
49
34
  }
50
- async function requireNoRecovery(paths) {
51
- await assertNoFollowType(paths.recoveryRoot, ["missing"]);
52
- }
53
- async function syncDirectoryStrict(path) {
54
- const directory = await open(path, "r");
55
- try {
56
- await directory.sync();
57
- }
58
- finally {
59
- await directory.close();
60
- }
61
- }
62
- async function hasRecovery(paths) {
63
- try {
64
- return (await classifyPathNoFollow(paths.recoveryRoot)) !== "missing";
65
- }
66
- catch {
67
- return true;
68
- }
69
- }
70
- async function registration(paths, deps) {
71
- const settings = await deps.readSettings(paths.settingsFile, paths.homeDir);
72
- const canonicalRoot = await canonicalizeProspectivePath(paths.installedRoot);
73
- const matches = [];
74
- for (const entry of settings.packages) {
75
- if ((await resolveCanonicalPiLocalSource(entry.source, paths.agentDir, paths.homeDir)) === canonicalRoot) {
76
- matches.push(entry);
77
- }
78
- }
79
- if (matches.length > 1)
80
- throw new Error("duplicate Pi registration");
81
- return matches[0] ?? null;
35
+ const { requireIdentity, requireJournal, phase, retireJournal } = createSnapshotJournal({
36
+ label: "Pi",
37
+ validatePaths,
38
+ serialize: journalBytes,
39
+ readCap: 64 * 1024,
40
+ });
41
+ async function registration(paths) {
42
+ return (await readPiSettings(paths.settingsFile, paths.homeDir))
43
+ .managerRegistration;
82
44
  }
83
45
  function sameRegistration(a, b) {
84
46
  return a === null
@@ -115,39 +77,6 @@ async function requireSnapshot(root, expected) {
115
77
  if (!sameArtifact(await snapshot(root), expected))
116
78
  throw new Error("Pi artifact changed");
117
79
  }
118
- async function requireJournal(p) {
119
- if ((await validatePaths(p.paths)) !== p.canonicalRoot ||
120
- p.journal.installedRoot !== p.canonicalRoot ||
121
- !/^[a-f0-9]{32}$/u.test(p.journal.token))
122
- throw new Error("Pi recovery root changed");
123
- await requireIdentity(p.paths.recoveryRoot, p.recoveryIdentity);
124
- const path = journalPath(p);
125
- await assertNoFollowType(path, ["regular-file"]);
126
- const stat = await lstat(path);
127
- if (!sameIdentity(stat, p.journalIdentity) ||
128
- stat.size > 64 * 1024 ||
129
- !(await readFile(path)).equals(journalBytes(p.journal)))
130
- throw new Error("Pi recovery journal changed");
131
- if (p.backup !==
132
- join(dirname(p.canonicalRoot), `.${basename(p.canonicalRoot)}.bak.${p.journal.token}`) ||
133
- p.stage !==
134
- join(dirname(p.canonicalRoot), `.${basename(p.canonicalRoot)}.stage.${p.journal.token}`))
135
- throw new Error("Pi recovery paths changed");
136
- }
137
- async function phase(p, next, changes = {}) {
138
- await requireJournal(p);
139
- const journal = { ...p.journal, ...changes, phase: next };
140
- const bytes = journalBytes(journal);
141
- await atomicWriteFile(journalPath(p), bytes, {
142
- validate: async (temporary) => {
143
- if (!(await readFile(temporary)).equals(bytes))
144
- throw new Error("Pi journal write changed");
145
- },
146
- });
147
- await syncDirectoryStrict(p.paths.recoveryRoot);
148
- p.journal = journal;
149
- p.journalIdentity = await lstat(journalPath(p));
150
- }
151
80
  async function beginJournal(paths, oldArtifact, newArtifact, priorRegistration, ctx, deps) {
152
81
  const canonicalRoot = await validatePaths(paths);
153
82
  await mkdir(paths.managerRoot, { recursive: true });
@@ -166,38 +95,20 @@ async function beginJournal(paths, oldArtifact, newArtifact, priorRegistration,
166
95
  createdRegistration: null,
167
96
  phase: newArtifact === null ? "removing" : "staging",
168
97
  };
169
- const path = join(paths.recoveryRoot, "transaction.json");
170
- const handle = await open(path, "wx", 0o600);
171
- try {
172
- await handle.writeFile(journalBytes(journal));
173
- await handle.sync();
174
- }
175
- finally {
176
- await handle.close();
177
- }
178
98
  // The first journal is durable before either a staging tree or backup exists.
179
- await syncDirectoryStrict(paths.recoveryRoot);
99
+ const journalIdentity = await createJournal(paths.recoveryRoot, journalBytes(journal));
180
100
  return {
181
101
  paths,
182
102
  canonicalRoot,
183
103
  recoveryIdentity,
184
- backup: join(dirname(canonicalRoot), `.${basename(canonicalRoot)}.bak.${token}`),
185
- stage: join(dirname(canonicalRoot), `.${basename(canonicalRoot)}.stage.${token}`),
104
+ ...publicationPaths(canonicalRoot, token),
186
105
  ctx,
187
106
  deps,
188
107
  journal,
189
- journalIdentity: await lstat(path),
108
+ journalIdentity,
190
109
  settled: false,
191
110
  };
192
111
  }
193
- async function retireJournal(p) {
194
- await requireJournal(p);
195
- if ((await readdir(p.paths.recoveryRoot)).some((name) => name !== "transaction.json"))
196
- throw new Error("unknown Pi recovery material");
197
- await unlink(journalPath(p));
198
- await requireIdentity(p.paths.recoveryRoot, p.recoveryIdentity);
199
- await rmdir(p.paths.recoveryRoot);
200
- }
201
112
  async function journalRetirementGuidance(p) {
202
113
  try {
203
114
  const kind = await classifyPathNoFollow(p.paths.recoveryRoot);
@@ -243,7 +154,7 @@ async function finalizePiPublication(p, publication) {
243
154
  let publicationCleanupCompleted = false;
244
155
  try {
245
156
  await requirePublication(p);
246
- const current = await registration(p.paths, p.deps);
157
+ const current = await registration(p.paths);
247
158
  const expectedRegistration = p.journal.priorRegistration ?? p.journal.createdRegistration;
248
159
  if (expectedRegistration === null ||
249
160
  !sameRegistration(current, expectedRegistration) ||
@@ -273,7 +184,7 @@ async function rollbackPiPublication(p, publication) {
273
184
  let retirementStarted = false;
274
185
  try {
275
186
  await requirePublication(p);
276
- const current = await registration(p.paths, p.deps);
187
+ const current = await registration(p.paths);
277
188
  if (p.journal.priorRegistration !== null) {
278
189
  if (!sameRegistration(current, p.journal.priorRegistration))
279
190
  throw new Error("prior Pi registration changed");
@@ -288,13 +199,13 @@ async function rollbackPiPublication(p, publication) {
288
199
  await p.deps
289
200
  .run(["remove", p.canonicalRoot, "--no-approve"], p.paths, p.ctx)
290
201
  .catch(() => undefined);
291
- if ((await registration(p.paths, p.deps)) !== null)
202
+ if ((await registration(p.paths)) !== null)
292
203
  throw new Error("Pi deregistration unverified");
293
204
  }
294
205
  await requirePublication(p);
295
206
  await publication.rollback();
296
207
  await requireSnapshot(p.canonicalRoot, p.journal.oldArtifact);
297
- if (!sameRegistration(await registration(p.paths, p.deps), p.journal.priorRegistration))
208
+ if (!sameRegistration(await registration(p.paths), p.journal.priorRegistration))
298
209
  throw new Error("Pi registration restoration unverified");
299
210
  await phase(p, "restored");
300
211
  retirementStarted = true;
@@ -312,7 +223,7 @@ async function recoverBeforePublication(p) {
312
223
  await requireJournal(p);
313
224
  await requireSnapshot(p.canonicalRoot, p.journal.oldArtifact);
314
225
  await assertNoFollowType(p.backup, ["missing"]);
315
- if (!sameRegistration(await registration(p.paths, p.deps), p.journal.priorRegistration))
226
+ if (!sameRegistration(await registration(p.paths), p.journal.priorRegistration))
316
227
  return false;
317
228
  if ((await assertNoFollowType(p.stage, ["directory", "missing"])) !==
318
229
  "missing") {
@@ -346,13 +257,13 @@ export async function installPi(artifact, ctx, deps = DEFAULTS) {
346
257
  throw new Error("Pi prepared artifact changed");
347
258
  await requireActivationEligibility(ctx);
348
259
  const previous = await snapshot(paths.installedRoot);
349
- const priorRegistration = await registration(paths, deps);
260
+ const priorRegistration = await registration(paths);
350
261
  if (previous === null && priorRegistration !== null)
351
262
  throw new Error("unowned Pi registration");
352
263
  accepted(normalizeSnapshotRuntimeVersion("Pi", await deps.run(["--version"], paths, ctx)));
353
264
  // Recheck after the native preflight, before claiming mutation ownership.
354
265
  await requireSnapshot(paths.installedRoot, previous);
355
- if (!sameRegistration(await registration(paths, deps), priorRegistration))
266
+ if (!sameRegistration(await registration(paths), priorRegistration))
356
267
  throw new Error("Pi registration changed");
357
268
  pending = await beginJournal(paths, previous, assessment.receipt, priorRegistration, ctx, deps);
358
269
  await mkdir(pending.stage);
@@ -371,20 +282,20 @@ export async function installPi(artifact, ctx, deps = DEFAULTS) {
371
282
  await requireIdentity(pending.stage, pending.stageIdentity);
372
283
  await requireActivationEligibility(ctx);
373
284
  await requireSnapshot(paths.installedRoot, previous);
374
- if (!sameRegistration(await registration(paths, deps), priorRegistration))
285
+ if (!sameRegistration(await registration(paths), priorRegistration))
375
286
  throw new Error("Pi registration changed");
376
287
  publication = await deps.beginPublication(pending.stage, pending.canonicalRoot, { backupPath: pending.backup });
377
288
  pending.publishedIdentity = pending.stageIdentity;
378
289
  await phase(pending, "published");
379
290
  if (priorRegistration === null) {
380
291
  await phase(pending, "registering");
381
- if ((await registration(paths, deps)) !== null)
292
+ if ((await registration(paths)) !== null)
382
293
  throw new Error("Pi registration changed");
383
294
  const result = await deps
384
295
  .run(["install", pending.canonicalRoot, "--no-approve"], paths, ctx)
385
296
  .catch(() => fail("native-failed", "Pi native installation failed"));
386
297
  // Even an unsuccessful command may have changed settings. Read them first.
387
- const registered = await registration(paths, deps);
298
+ const registered = await registration(paths);
388
299
  if (registered === null || registered.resourceState !== "enabled")
389
300
  throw new Error("Pi activation unverified");
390
301
  await phase(pending, "registered", {
@@ -393,7 +304,7 @@ export async function installPi(artifact, ctx, deps = DEFAULTS) {
393
304
  accepted(result);
394
305
  }
395
306
  await requirePublication(pending);
396
- const registered = await registration(paths, deps);
307
+ const registered = await registration(paths);
397
308
  if (registered === null || registered.resourceState !== "enabled")
398
309
  throw new Error("Pi activation unverified");
399
310
  await phase(pending, "ready");
@@ -444,7 +355,7 @@ export async function removePi(input, ctx, deps = DEFAULTS) {
444
355
  input.receiptDigest !== current.receiptDigest)
445
356
  throw new Error("stale Pi removal input");
446
357
  const previous = await snapshot(paths.installedRoot);
447
- const registered = await registration(paths, deps);
358
+ const registered = await registration(paths);
448
359
  if (previous === null) {
449
360
  if (registered !== null)
450
361
  throw new Error("unowned Pi registration");
@@ -453,7 +364,7 @@ export async function removePi(input, ctx, deps = DEFAULTS) {
453
364
  if (registered !== null)
454
365
  accepted(normalizeSnapshotRuntimeVersion("Pi", await deps.run(["--version"], paths, ctx)));
455
366
  await requireSnapshot(paths.installedRoot, previous);
456
- if (!sameRegistration(await registration(paths, deps), registered))
367
+ if (!sameRegistration(await registration(paths), registered))
457
368
  throw new Error("Pi registration changed");
458
369
  pending = await beginJournal(paths, previous, null, registered, ctx, deps);
459
370
  const installedIdentity = await identity(paths.installedRoot);
@@ -461,7 +372,7 @@ export async function removePi(input, ctx, deps = DEFAULTS) {
461
372
  const result = await deps
462
373
  .run(["remove", pending.canonicalRoot, "--no-approve"], paths, ctx)
463
374
  .catch(() => fail("native-failed", "Pi native removal failed"));
464
- const after = await registration(paths, deps);
375
+ const after = await registration(paths);
465
376
  if (after !== null) {
466
377
  if (sameRegistration(after, registered)) {
467
378
  await requireSnapshot(paths.installedRoot, previous);
@@ -480,7 +391,7 @@ export async function removePi(input, ctx, deps = DEFAULTS) {
480
391
  await phase(pending, "deregistered");
481
392
  await requireIdentity(paths.installedRoot, installedIdentity);
482
393
  await requireSnapshot(paths.installedRoot, previous);
483
- if ((await registration(paths, deps)) !== null)
394
+ if ((await registration(paths)) !== null)
484
395
  throw new Error("Pi registration returned");
485
396
  await rm(paths.installedRoot, { recursive: true });
486
397
  await requireSnapshot(paths.installedRoot, null);
@@ -1,37 +1,12 @@
1
- import { tmpdir } from "node:os";
2
- import { isAbsolute, resolve, sep } from "node:path";
3
- import { failureResult, } from "../../adapter-result.js";
4
- import { nativeCommandResult, } from "../../harness-command-result.js";
1
+ import {} from "../../adapter-result.js";
2
+ import { runIsolatedNative, } from "../../harness-command-result.js";
5
3
  import { BOUNDED_EXECUTABLE, runValidator as runBoundedCommand, } from "../../validator.js";
6
- import { withWorkspace } from "../../workspace.js";
7
- function selectedExecutable(env, invocationCwd) {
8
- const configured = env.SUPERPOWERS_PI || "pi";
9
- return configured.includes(sep) && !isAbsolute(configured)
10
- ? resolve(invocationCwd, configured)
11
- : configured;
12
- }
13
4
  export async function runPi(args, paths, ctx, execute = runBoundedCommand) {
14
- const operation = "pi-command";
15
- const env = ctx.env ?? {};
16
- const invocationCwd = process.cwd();
17
- const executable = selectedExecutable(env, invocationCwd);
18
- let entered = false;
19
- try {
20
- return await withWorkspace(env.TMPDIR ?? tmpdir(), "superpowers-manager.pi.", async (workspace) => {
21
- entered = true;
22
- const result = await execute([executable, ...args], BOUNDED_EXECUTABLE, {
23
- ...env,
24
- HOME: paths.homeDir,
25
- PI_CODING_AGENT_DIR: paths.agentDir,
26
- PI_OFFLINE: "1",
27
- PI_SKIP_VERSION_CHECK: "1",
28
- }, workspace, workspace);
29
- return nativeCommandResult("Pi", result);
30
- });
31
- }
32
- catch {
33
- return failureResult(operation, "workspace-failed", entered
34
- ? "cannot complete Pi command in its isolated workspace"
35
- : "cannot create an isolated Pi command workspace", [], []);
36
- }
5
+ return await runIsolatedNative("Pi", (ctx.env ?? {}).SUPERPOWERS_PI || "pi", ctx, async (executable, workspace, env) => await execute([executable, ...args], BOUNDED_EXECUTABLE, {
6
+ ...env,
7
+ HOME: paths.homeDir,
8
+ PI_CODING_AGENT_DIR: paths.agentDir,
9
+ PI_OFFLINE: "1",
10
+ PI_SKIP_VERSION_CHECK: "1",
11
+ }, workspace, workspace));
37
12
  }
@@ -1,51 +1,10 @@
1
- import { join } from "node:path";
2
- import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, } from "../../artifact-tree.js";
3
- import { COMMIT_RE } from "../../domain/refs.js";
4
- import { SafetyError } from "../../safety-error.js";
5
- import { validateSource } from "../../selection.js";
1
+ import { createSnapshotReceipts, } from "../../snapshot-package.js";
6
2
  import { assessPiCompatibility } from "./compatibility.js";
7
- import { snapshotReceiptBinding } from "../../snapshot-package.js";
8
- export async function readPiReceipt(root) {
9
- const path = join(root, ARTIFACT_RECEIPT);
10
- try {
11
- const value = await readArtifactObject(root, path);
12
- if (value.schema !== 1 ||
13
- value.manager !== "superpowers-manager" ||
14
- value.harness !== "pi" ||
15
- typeof value.source !== "string" ||
16
- typeof value.commit !== "string" ||
17
- !COMMIT_RE.test(value.commit) ||
18
- typeof value.digest !== "string" ||
19
- !ARTIFACT_DIGEST_RE.test(value.digest) ||
20
- typeof value.binding !== "string" ||
21
- !ARTIFACT_DIGEST_RE.test(value.binding))
22
- throw new Error("receipt fields");
23
- validateSource(value.source);
24
- if (snapshotReceiptBinding(value) !== value.binding)
25
- throw new Error("receipt binding");
26
- const compatibility = value.compatibility;
27
- if (!compatibility ||
28
- typeof compatibility !== "object" ||
29
- Array.isArray(compatibility) ||
30
- typeof compatibility.reason !== "string" ||
31
- typeof compatibility.kind !== "string" ||
32
- !["unknown", "unsupported", "supported", "experimental"].includes(compatibility.kind) ||
33
- ((compatibility.kind === "supported" ||
34
- compatibility.kind === "experimental") &&
35
- typeof compatibility.generation !== "string"))
36
- throw new Error("receipt compatibility");
37
- return value;
38
- }
39
- catch (cause) {
40
- throw new SafetyError("pi-package", `invalid Pi artifact receipt: ${path}`, { cause });
41
- }
42
- }
43
- export async function readPiPackageAssessment(root) {
44
- const receipt = await readPiReceipt(root);
45
- if ((await digestArtifactTree(root)) !== receipt.digest)
46
- throw new SafetyError("pi-package", `Pi artifact digest mismatch: ${root}`);
47
- const compatibility = await assessPiCompatibility(root, {
48
- effectiveSource: receipt.source,
49
- });
50
- return { receipt, compatibility };
51
- }
3
+ const receipts = createSnapshotReceipts({
4
+ harness: "pi",
5
+ label: "Pi",
6
+ strictGeneration: false,
7
+ assessCompatibility: assessPiCompatibility,
8
+ });
9
+ export const readPiReceipt = receipts.readReceipt;
10
+ export const readPiPackageAssessment = receipts.readAssessment;
@@ -1,20 +1,15 @@
1
1
  import { createSnapshotPreparation } from "../../snapshot-prepare.js";
2
- import { piPaths } from "./paths.js";
3
2
  import { assessPiCompatibility } from "./compatibility.js";
4
3
  import { readPiPackageAssessment } from "./package.js";
5
- export function piPreparationLocation(ctx) {
6
- return {
7
- destinationRoot: piPaths(ctx.env ?? {}, process.cwd()).preparedRoot,
8
- stagingLeaf: "superpowers",
9
- };
10
- }
4
+ import { piPaths } from "./paths.js";
11
5
  const preparation = createSnapshotPreparation({
12
6
  harness: "pi",
13
7
  label: "Pi",
14
- preparationLocation: piPreparationLocation,
8
+ paths: piPaths,
15
9
  assessCompatibility: assessPiCompatibility,
16
10
  readAssessment: readPiPackageAssessment,
17
11
  });
12
+ export const piPreparationLocation = preparation.preparationLocation;
18
13
  export const preparePiCandidate = preparation.prepareCandidate;
19
14
  export const inspectPiPrepared = preparation.inspectPrepared;
20
15
  export const readPiPrepared = preparation.readPrepared;
@@ -1,4 +1,4 @@
1
- import { createSnapshotPresentation } from "../../snapshot-presentation.js";
1
+ import { createSnapshotPresentation, probeOutput, } from "../../snapshot-presentation.js";
2
2
  const renderProbe = (facts) => {
3
3
  const fields = [
4
4
  ["harness", "pi"],
@@ -14,11 +14,6 @@ const renderProbe = (facts) => {
14
14
  ["compatibility_reason", facts.compatibility.reason],
15
15
  ["status", facts.status],
16
16
  ];
17
- return {
18
- human: fields
19
- .map(([key, value]) => `${key.replaceAll("_", " ")}: ${value || "not present"}\n`)
20
- .join(""),
21
- porcelain: fields.map(([key, value]) => `${key}=${value}\n`).join(""),
22
- };
17
+ return probeOutput(fields);
23
18
  };
24
19
  export const piPresentation = createSnapshotPresentation("Pi", renderProbe, (input) => input.receiptDigest === null && input.registrationIdentity === null);
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { canonicalizeProspectivePath } from "../../safe-path.js";
4
+ import { canonicalizeProspectivePath, isErrno } from "../../safe-path.js";
5
5
  import { SafetyError } from "../../safety-error.js";
6
6
  import { parseStrictJson, } from "../../strict-json.js";
7
7
  const SETTINGS_PROFILE = {
@@ -88,12 +88,6 @@ function parsePackage(value, index, path) {
88
88
  }),
89
89
  };
90
90
  }
91
- function isMissing(cause) {
92
- return (typeof cause === "object" &&
93
- cause !== null &&
94
- "code" in cause &&
95
- cause.code === "ENOENT");
96
- }
97
91
  export function resolvePiLocalSource(source, agentDir, homeDir) {
98
92
  let normalized = source.trim();
99
93
  if (normalized.length === 0 ||
@@ -127,8 +121,9 @@ export async function readPiSettings(path, homeDir) {
127
121
  bytes = await readFile(path);
128
122
  }
129
123
  catch (cause) {
130
- if (isMissing(cause))
131
- return { packages: [] };
124
+ if (isErrno(cause, "ENOENT")) {
125
+ return { packages: [], managerRegistration: null };
126
+ }
132
127
  throw new SafetyError("pi-settings", `cannot read Pi settings ${path}`, {
133
128
  cause,
134
129
  });
@@ -166,6 +161,7 @@ export async function readPiSettings(path, homeDir) {
166
161
  }
167
162
  return {
168
163
  packages,
164
+ managerRegistration: managerRegistrations[0] ?? null,
169
165
  ...(settings.skills === undefined ? {} : { skills: settings.skills }),
170
166
  };
171
167
  }
@@ -1,7 +1,12 @@
1
1
  import { readdir, readFile, readlink, realpath } from "node:fs/promises";
2
2
  import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { classifyPathNoFollow, isContained } from "../../safe-path.js";
4
+ import { classifyPathNoFollow, isContained, } from "../../safe-path.js";
5
+ // Every caller in this module treats a classification failure as its own
6
+ // indeterminate/false outcome; null carries that failure without a try block.
7
+ function classifyOrNull(path) {
8
+ return classifyPathNoFollow(path).catch(() => null);
9
+ }
5
10
  const PI_IGNORE_FILES = [".gitignore", ".ignore", ".fdignore"];
6
11
  function toPosixPath(path) {
7
12
  return path.split(sep).join("/");
@@ -9,13 +14,9 @@ function toPosixPath(path) {
9
14
  async function ignoreControlState(directory) {
10
15
  for (const filename of PI_IGNORE_FILES) {
11
16
  const path = join(directory, filename);
12
- let kind;
13
- try {
14
- kind = await classifyPathNoFollow(path);
15
- }
16
- catch {
17
+ const kind = await classifyOrNull(path);
18
+ if (kind === null)
17
19
  return "indeterminate";
18
- }
19
20
  if (kind === "missing")
20
21
  continue;
21
22
  if (kind !== "regular-file")
@@ -42,13 +43,9 @@ async function collectSharedSkillDirectory(directory) {
42
43
  return { kind: "indeterminate" };
43
44
  }
44
45
  const declaredSkill = join(directory, "SKILL.md");
45
- let declaredKind;
46
- try {
47
- declaredKind = await classifyPathNoFollow(declaredSkill);
48
- }
49
- catch {
46
+ const declaredKind = await classifyOrNull(declaredSkill);
47
+ if (declaredKind === null)
50
48
  return { kind: "indeterminate" };
51
- }
52
49
  if (declaredKind === "regular-file") {
53
50
  return { kind: "candidates", paths: [declaredSkill] };
54
51
  }
@@ -68,13 +65,9 @@ async function collectSharedSkillDirectory(directory) {
68
65
  if (entry.name === "SKILL.md")
69
66
  return { kind: "indeterminate" };
70
67
  const path = join(directory, entry.name);
71
- let kind;
72
- try {
73
- kind = await classifyPathNoFollow(path);
74
- }
75
- catch {
68
+ const kind = await classifyOrNull(path);
69
+ if (kind === null)
76
70
  return { kind: "indeterminate" };
77
- }
78
71
  if (kind === "symlink" || kind === "missing") {
79
72
  return { kind: "indeterminate" };
80
73
  }
@@ -96,38 +89,26 @@ async function collectSharedPiSkills(paths) {
96
89
  const skillsRoot = join(agentsRoot, "skills");
97
90
  const sharedRoot = join(skillsRoot, "superpowers");
98
91
  for (const directory of [agentsRoot, skillsRoot]) {
99
- let kind;
100
- try {
101
- kind = await classifyPathNoFollow(directory);
102
- }
103
- catch {
92
+ const kind = await classifyOrNull(directory);
93
+ if (kind === null)
104
94
  return { kind: "indeterminate" };
105
- }
106
95
  if (kind === "missing")
107
96
  return { kind: "none" };
108
97
  if (kind !== "directory")
109
98
  return { kind: "indeterminate" };
110
99
  }
111
- let sharedRootKind;
112
- try {
113
- sharedRootKind = await classifyPathNoFollow(sharedRoot);
114
- }
115
- catch {
100
+ const sharedRootKind = await classifyOrNull(sharedRoot);
101
+ if (sharedRootKind === null)
116
102
  return { kind: "indeterminate" };
117
- }
118
103
  if (sharedRootKind === "missing")
119
104
  return { kind: "none" };
120
105
  if ((await ignoreControlState(skillsRoot)) === "indeterminate") {
121
106
  return { kind: "indeterminate" };
122
107
  }
123
108
  const discoveryRootSkill = join(skillsRoot, "SKILL.md");
124
- let discoveryRootSkillKind;
125
- try {
126
- discoveryRootSkillKind = await classifyPathNoFollow(discoveryRootSkill);
127
- }
128
- catch {
109
+ const discoveryRootSkillKind = await classifyOrNull(discoveryRootSkill);
110
+ if (discoveryRootSkillKind === null)
129
111
  return { kind: "indeterminate" };
130
- }
131
112
  if (discoveryRootSkillKind === "regular-file")
132
113
  return { kind: "none" };
133
114
  if (discoveryRootSkillKind !== "missing") {
@@ -248,13 +229,8 @@ async function hasSafeSharedSkillAncestors(path, sharedRoot) {
248
229
  directories.push(cursor);
249
230
  }
250
231
  for (const directory of directories) {
251
- try {
252
- if ((await classifyPathNoFollow(directory)) !== "directory")
253
- return false;
254
- }
255
- catch {
232
+ if ((await classifyOrNull(directory)) !== "directory")
256
233
  return false;
257
- }
258
234
  }
259
235
  return true;
260
236
  }
@@ -305,11 +281,8 @@ async function explicitSharedSkillActivities(settings, paths) {
305
281
  }
306
282
  continue;
307
283
  }
308
- let lexicalKind;
309
- try {
310
- lexicalKind = await classifyPathNoFollow(path);
311
- }
312
- catch {
284
+ const lexicalKind = await classifyOrNull(path);
285
+ if (lexicalKind === null) {
313
286
  if (isContained(sharedRoot, path) || isContained(path, sharedRoot)) {
314
287
  indeterminate = true;
315
288
  }
@@ -343,11 +316,8 @@ async function explicitSharedSkillActivities(settings, paths) {
343
316
  isContained(canonicalPath, canonicalSharedRoot);
344
317
  if (!related)
345
318
  continue;
346
- let canonicalKind;
347
- try {
348
- canonicalKind = await classifyPathNoFollow(canonicalPath);
349
- }
350
- catch {
319
+ const canonicalKind = await classifyOrNull(canonicalPath);
320
+ if (canonicalKind === null) {
351
321
  indeterminate = true;
352
322
  continue;
353
323
  }