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.
- package/README.md +36 -16
- package/dist/atomic.js +1 -3
- package/dist/cli-arguments.js +4 -1
- package/dist/cli.js +14 -43
- package/dist/commands/adapter-call.js +24 -0
- package/dist/commands/install.js +16 -63
- package/dist/commands/mutation.js +5 -5
- package/dist/commands/pin.js +10 -5
- package/dist/commands/prepare.js +42 -108
- package/dist/commands/probe.js +33 -19
- package/dist/commands/uninstall.js +18 -46
- package/dist/commands/unpin.js +2 -1
- package/dist/commands/update.js +8 -29
- package/dist/domain/refs.js +0 -3
- package/dist/effective-selection.js +11 -15
- package/dist/git-tree.js +4 -6
- package/dist/harness-command-result.js +33 -3
- package/dist/harnesses/claude-code/harness.js +39 -0
- package/dist/harnesses/claude-code/install.js +380 -0
- package/dist/harnesses/claude-code/native.js +79 -0
- package/dist/harnesses/claude-code/paths.js +35 -0
- package/dist/harnesses/claude-code/prepare.js +117 -0
- package/dist/harnesses/claude-code/presentation.js +25 -0
- package/dist/harnesses/claude-code/state.js +253 -0
- package/dist/harnesses/codex/adapter.js +27 -68
- package/dist/harnesses/codex/conflicts.js +3 -8
- package/dist/harnesses/codex/generated-plugin.js +4 -11
- package/dist/harnesses/codex/harness.js +4 -26
- package/dist/harnesses/codex/hooks.js +11 -27
- package/dist/harnesses/codex/lifecycle.js +22 -107
- package/dist/harnesses/codex/manifest-overlay.js +9 -27
- package/dist/harnesses/codex/paths.js +29 -0
- package/dist/harnesses/codex/prepare.js +9 -43
- package/dist/harnesses/codex/presentation.js +1 -1
- package/dist/harnesses/codex/publication.js +5 -40
- package/dist/harnesses/codex/state.js +1 -6
- package/dist/harnesses/codex/stored-state.js +1 -6
- package/dist/harnesses/opencode/config.js +2 -4
- package/dist/harnesses/opencode/discovery.js +1 -5
- package/dist/harnesses/opencode/install.js +12 -93
- package/dist/harnesses/opencode/native.js +28 -49
- package/dist/harnesses/opencode/package.js +11 -55
- package/dist/harnesses/opencode/prepare.js +2 -7
- package/dist/harnesses/opencode/presentation.js +2 -7
- package/dist/harnesses/pi/install.js +31 -120
- package/dist/harnesses/pi/native.js +9 -34
- package/dist/harnesses/pi/package.js +9 -50
- package/dist/harnesses/pi/prepare.js +3 -8
- package/dist/harnesses/pi/presentation.js +2 -7
- package/dist/harnesses/pi/settings.js +5 -9
- package/dist/harnesses/pi/shared-skills.js +23 -53
- package/dist/harnesses/pi/state.js +3 -10
- package/dist/provenance.js +3 -19
- package/dist/resource-lock.js +11 -16
- package/dist/safe-path.js +28 -1
- package/dist/selection.js +15 -19
- package/dist/snapshot-journal.js +101 -0
- package/dist/snapshot-package.js +62 -1
- package/dist/snapshot-prepare.js +9 -2
- package/dist/snapshot-presentation.js +9 -0
- package/dist/upstream.js +8 -12
- package/dist/validator.js +26 -23
- package/dist/workspace.js +29 -35
- package/package.json +10 -9
- package/dist/python-json.js +0 -4
- package/dist/upstream-workspace.js +0 -8
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { cp,
|
|
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 {
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
99
|
+
const journalIdentity = await createJournal(paths.recoveryRoot, journalBytes(journal));
|
|
180
100
|
return {
|
|
181
101
|
paths,
|
|
182
102
|
canonicalRoot,
|
|
183
103
|
recoveryIdentity,
|
|
184
|
-
|
|
185
|
-
stage: join(dirname(canonicalRoot), `.${basename(canonicalRoot)}.stage.${token}`),
|
|
104
|
+
...publicationPaths(canonicalRoot, token),
|
|
186
105
|
ctx,
|
|
187
106
|
deps,
|
|
188
107
|
journal,
|
|
189
|
-
journalIdentity
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
}
|