superpowers-manager 0.4.0 → 0.5.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 +62 -533
- package/dist/adapter.js +8 -4
- package/dist/artifact-tree.js +110 -0
- package/dist/atomic.js +103 -7
- package/dist/cli-arguments.js +37 -0
- package/dist/cli.js +48 -11
- package/dist/codex-compatibility.js +126 -0
- package/dist/codex-conflicts.js +99 -0
- package/dist/codex-harness.js +51 -1
- package/dist/codex-json.js +7 -0
- package/dist/codex-prepare.js +69 -13
- package/dist/codex-presentation.js +24 -1
- package/dist/commands/install.js +115 -16
- package/dist/commands/mutation.js +71 -0
- package/dist/commands/prepare.js +19 -13
- package/dist/commands/probe.js +160 -10
- package/dist/commands/uninstall.js +6 -2
- package/dist/commands/update.js +5 -1
- package/dist/generated-plugin.js +3 -3
- package/dist/harness-compatibility.js +13 -0
- package/dist/manifest-overlay.js +1 -1
- package/dist/pi-compatibility.js +73 -0
- package/dist/pi-harness.js +63 -0
- package/dist/pi-install.js +495 -0
- package/dist/pi-native.js +81 -0
- package/dist/pi-package.js +153 -0
- package/dist/pi-paths.js +30 -0
- package/dist/pi-prepare.js +111 -0
- package/dist/pi-presentation.js +67 -0
- package/dist/pi-settings.js +171 -0
- package/dist/pi-shared-skills.js +413 -0
- package/dist/pi-state.js +311 -0
- package/dist/resource-lock.js +265 -0
- package/dist/safe-path.js +21 -5
- package/dist/upstream-workspace.js +8 -0
- package/dist/validator.js +2 -1
- package/package.json +11 -8
package/dist/commands/prepare.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { mkdir, stat } from "node:fs/promises";
|
|
2
|
-
import { dirname, isAbsolute, join,
|
|
2
|
+
import { dirname, isAbsolute, join, sep } from "node:path";
|
|
3
3
|
import { atomicReplaceDir } from "../atomic.js";
|
|
4
4
|
import { oneLine } from "../cli-arguments.js";
|
|
5
5
|
import { computeEffectiveSelection } from "../effective-selection.js";
|
|
6
6
|
import { runGit } from "../git.js";
|
|
7
7
|
import { SafetyError } from "../safety-error.js";
|
|
8
8
|
import { fetchExactCommit, gitSafeSource } from "../upstream.js";
|
|
9
|
+
import { upstreamCacheRoot } from "../upstream-workspace.js";
|
|
9
10
|
import { BOUNDED_EXECUTABLE, UNBOUNDED_LEGACY, launchFailureMessage, resolveValidator, runValidator, displayPath, } from "../validator.js";
|
|
10
11
|
import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
|
|
11
12
|
import { replayOutcome } from "./probe.js";
|
|
13
|
+
import { runWithMutation } from "./mutation.js";
|
|
12
14
|
// Every message this module writes is hand-written here. The cause is attached
|
|
13
15
|
// for debuggability and never reaches a stream: oneLine (src/cli-arguments.ts)
|
|
14
16
|
// reads .message only. Same arrangement as hookError
|
|
@@ -93,10 +95,8 @@ async function gatherPrepare(ctx) {
|
|
|
93
95
|
// getcwd(3) returns the physical path, so this matches `pwd -P` without a
|
|
94
96
|
// realpath call.
|
|
95
97
|
const cwd = process.cwd();
|
|
96
|
-
const
|
|
97
|
-
const cacheParent =
|
|
98
|
-
? configuredCache
|
|
99
|
-
: resolve(cwd, configuredCache);
|
|
98
|
+
const cache = upstreamCacheRoot(ctx.root, env, cwd);
|
|
99
|
+
const cacheParent = dirname(cache);
|
|
100
100
|
const adapterContext = { root: ctx.root, env };
|
|
101
101
|
let location;
|
|
102
102
|
try {
|
|
@@ -114,7 +114,6 @@ async function gatherPrepare(ctx) {
|
|
|
114
114
|
const pluginRoot = location.destinationRoot;
|
|
115
115
|
const additionalValidator = env.SUPERPOWERS_VALIDATOR || "";
|
|
116
116
|
const executableValidator = env.SUPERPOWERS_VALIDATOR_EXECUTABLE || "";
|
|
117
|
-
const cache = join(cacheParent, "superpowers");
|
|
118
117
|
const tmpParent = dirname(pluginRoot);
|
|
119
118
|
await owned(`cannot create directory: ${tmpParent}`, () => mkdir(tmpParent, { recursive: true }));
|
|
120
119
|
let cleanupWarning = null;
|
|
@@ -127,7 +126,7 @@ async function gatherPrepare(ctx) {
|
|
|
127
126
|
message,
|
|
128
127
|
});
|
|
129
128
|
const candidate = join(workspace, location.stagingLeaf);
|
|
130
|
-
const selection = await computeEffectiveSelection(ctx.root, env);
|
|
129
|
+
const selection = ctx.selection ?? (await computeEffectiveSelection(ctx.root, env));
|
|
131
130
|
let prefetch;
|
|
132
131
|
try {
|
|
133
132
|
prefetch =
|
|
@@ -211,6 +210,9 @@ async function gatherPrepare(ctx) {
|
|
|
211
210
|
if (prepared.outcome.result.commit !== selection.desiredCommit) {
|
|
212
211
|
return failed("adapter returned an unexpected preparation commit");
|
|
213
212
|
}
|
|
213
|
+
if (prepared.outcome.result.compatibility.kind === "unsupported") {
|
|
214
|
+
return failed(prepared.outcome.result.compatibility.reason);
|
|
215
|
+
}
|
|
214
216
|
let validator = NO_VALIDATOR_OUTPUT;
|
|
215
217
|
if (additionalValidator.length > 0) {
|
|
216
218
|
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:108::[ -f "$additional_validator` — `[ -f ]`.
|
|
@@ -284,11 +286,12 @@ async function gatherPrepare(ctx) {
|
|
|
284
286
|
// The swap must run inside the workspace callback: withWorkspace removes
|
|
285
287
|
// the workspace on return, and the candidate lives in it.
|
|
286
288
|
//
|
|
287
|
-
// atomicReplaceDir
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
// callee's message
|
|
289
|
+
// atomicReplaceDir delegates to beginDirectoryPublication, whose outer
|
|
290
|
+
// catch (`src/atomic.ts:341-348::if (cause`) wraps every non-SafetyError
|
|
291
|
+
// into a SafetyError, so the callee owns every failure on this path and
|
|
292
|
+
// re-emitting its own diagnostic is the sanctioned form of interpolation.
|
|
293
|
+
// The hand-written prefix carries the live root, which the callee's message
|
|
294
|
+
// does not.
|
|
292
295
|
try {
|
|
293
296
|
await atomicReplaceDir(candidate, pluginRoot);
|
|
294
297
|
}
|
|
@@ -319,6 +322,9 @@ async function gatherPrepare(ctx) {
|
|
|
319
322
|
return { outcome, cleanupWarning };
|
|
320
323
|
}
|
|
321
324
|
export async function runPrepare(argv, ctx) {
|
|
325
|
+
return await runWithMutation("prepare", ctx, async (scoped) => performPrepare(argv, scoped));
|
|
326
|
+
}
|
|
327
|
+
async function performPrepare(argv, ctx) {
|
|
322
328
|
// scripts/prepare never reads "$@", so extra arguments are ignored. This is a
|
|
323
329
|
// deliberate asymmetry with probe, whose shell original rejected unknown
|
|
324
330
|
// arguments and whose arity therefore moved into parseArgs in slice 2.
|
|
@@ -412,7 +418,7 @@ export async function runPrepare(argv, ctx) {
|
|
|
412
418
|
// completed before cleanup ran, so it is not being reported as unverified
|
|
413
419
|
// -- but something did still go wrong, and AGENTS.md's fail-closed rule
|
|
414
420
|
// extends to it. Mirrors
|
|
415
|
-
// `src/commands/install.ts:
|
|
421
|
+
// `src/commands/install.ts:546-554::if (cleanupWarning`.
|
|
416
422
|
ctx.stderr.write(`error: ${cleanupWarning}\n`);
|
|
417
423
|
return 1;
|
|
418
424
|
}
|
package/dist/commands/probe.js
CHANGED
|
@@ -33,6 +33,86 @@ export function replayOutcome(outcome, ctx) {
|
|
|
33
33
|
// stored, so scanning it here would re-check a population that cannot fail.
|
|
34
34
|
writeAdapterFailure(ctx, outcome);
|
|
35
35
|
}
|
|
36
|
+
function record(value) {
|
|
37
|
+
return value !== null && typeof value === "object"
|
|
38
|
+
? value
|
|
39
|
+
: null;
|
|
40
|
+
}
|
|
41
|
+
export function validOutput(value) {
|
|
42
|
+
const candidate = record(value);
|
|
43
|
+
return (candidate !== null &&
|
|
44
|
+
Array.isArray(candidate.stdout) &&
|
|
45
|
+
candidate.stdout.every((line) => typeof line === "string") &&
|
|
46
|
+
Array.isArray(candidate.stderr) &&
|
|
47
|
+
candidate.stderr.every((line) => typeof line === "string"));
|
|
48
|
+
}
|
|
49
|
+
function validDecision(value) {
|
|
50
|
+
const candidate = record(value);
|
|
51
|
+
return (candidate !== null &&
|
|
52
|
+
(candidate.kind === "allowed" ||
|
|
53
|
+
(candidate.kind === "blocked" && validOutput(candidate.output))));
|
|
54
|
+
}
|
|
55
|
+
function validCompatibility(value) {
|
|
56
|
+
const candidate = record(value);
|
|
57
|
+
if (candidate === null ||
|
|
58
|
+
typeof candidate.reason !== "string" ||
|
|
59
|
+
(candidate.kind !== "unknown" &&
|
|
60
|
+
candidate.kind !== "supported" &&
|
|
61
|
+
candidate.kind !== "experimental" &&
|
|
62
|
+
candidate.kind !== "unsupported"))
|
|
63
|
+
return false;
|
|
64
|
+
return ((candidate.kind !== "supported" && candidate.kind !== "experimental") ||
|
|
65
|
+
typeof candidate.generation === "string");
|
|
66
|
+
}
|
|
67
|
+
function validPreparedArtifact(value) {
|
|
68
|
+
const candidate = record(value);
|
|
69
|
+
return (candidate !== null &&
|
|
70
|
+
typeof candidate.root === "string" &&
|
|
71
|
+
typeof candidate.commit === "string" &&
|
|
72
|
+
typeof candidate.identity === "string" &&
|
|
73
|
+
validCompatibility(candidate.compatibility));
|
|
74
|
+
}
|
|
75
|
+
function validPrepared(value) {
|
|
76
|
+
const candidate = record(value);
|
|
77
|
+
return (candidate !== null &&
|
|
78
|
+
typeof candidate.observedIdentity === "string" &&
|
|
79
|
+
validCompatibility(candidate.compatibility) &&
|
|
80
|
+
(candidate.kind === "needs-prepare" ||
|
|
81
|
+
(candidate.kind === "current" &&
|
|
82
|
+
validPreparedArtifact(candidate.artifact))));
|
|
83
|
+
}
|
|
84
|
+
export function validInstalled(value) {
|
|
85
|
+
const candidate = record(value);
|
|
86
|
+
return (candidate !== null &&
|
|
87
|
+
(candidate.kind === "current" ||
|
|
88
|
+
candidate.kind === "mismatch" ||
|
|
89
|
+
candidate.kind === "absent") &&
|
|
90
|
+
typeof candidate.observedIdentity === "string" &&
|
|
91
|
+
(candidate.kind !== "absent" || candidate.observedIdentity === ""));
|
|
92
|
+
}
|
|
93
|
+
function validOwnership(value) {
|
|
94
|
+
const candidate = record(value);
|
|
95
|
+
return (candidate !== null &&
|
|
96
|
+
validDecision(candidate.installEligibility) &&
|
|
97
|
+
Object.prototype.hasOwnProperty.call(candidate, "removalInput") &&
|
|
98
|
+
validDecision(candidate.removalVerification) &&
|
|
99
|
+
validOutput(candidate.postRemovalOutput) &&
|
|
100
|
+
typeof candidate.presentationValue === "string" &&
|
|
101
|
+
(candidate.presentationConflicts === undefined ||
|
|
102
|
+
(Array.isArray(candidate.presentationConflicts) &&
|
|
103
|
+
candidate.presentationConflicts.every((conflict) => typeof conflict === "string"))));
|
|
104
|
+
}
|
|
105
|
+
function validControl(value) {
|
|
106
|
+
const candidate = record(value);
|
|
107
|
+
return (candidate !== null &&
|
|
108
|
+
validDecision(candidate.probeEligibility) &&
|
|
109
|
+
validDecision(candidate.mutationEligibility) &&
|
|
110
|
+
typeof candidate.presentationValue === "string" &&
|
|
111
|
+
(candidate.recoveryState === undefined ||
|
|
112
|
+
(candidate.recoveryState === "required" &&
|
|
113
|
+
candidate.probeEligibility.kind === "blocked" &&
|
|
114
|
+
candidate.mutationEligibility.kind === "blocked")));
|
|
115
|
+
}
|
|
36
116
|
// `runAdapter` reports a CONTROLLED failure by RETURN VALUE, not by throwing
|
|
37
117
|
// (`src/adapter-result.ts:32-35::export interface AdapterResult`). The shell got
|
|
38
118
|
// fail-closed behaviour for free: spw_invoke_adapter returned 1 and
|
|
@@ -48,10 +128,16 @@ export function replayOutcome(outcome, ctx) {
|
|
|
48
128
|
// adapter module: src/commands/prepare.ts and this module are the two the
|
|
49
129
|
// injected double must observe, because install reaches the adapter through
|
|
50
130
|
// gatherProbe and runPrepare. Spec §4.5.
|
|
51
|
-
async function inspect(call, unexpected, invalidStatus) {
|
|
131
|
+
async function inspect(call, unexpected, invalidStatus, valid) {
|
|
52
132
|
let result;
|
|
53
133
|
try {
|
|
54
134
|
result = await call();
|
|
135
|
+
if (!result ||
|
|
136
|
+
!Number.isInteger(result.status) ||
|
|
137
|
+
!result.outcome ||
|
|
138
|
+
typeof result.outcome.ok !== "boolean" ||
|
|
139
|
+
!Array.isArray(result.outcome.messages))
|
|
140
|
+
return { ok: false, result: null, message: invalidStatus };
|
|
55
141
|
}
|
|
56
142
|
catch {
|
|
57
143
|
return { ok: false, result: null, message: unexpected };
|
|
@@ -67,6 +153,14 @@ async function inspect(call, unexpected, invalidStatus) {
|
|
|
67
153
|
message: outcome.ok ? invalidStatus : null,
|
|
68
154
|
};
|
|
69
155
|
}
|
|
156
|
+
try {
|
|
157
|
+
if (!valid(outcome.result)) {
|
|
158
|
+
return { ok: false, result, message: invalidStatus };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return { ok: false, result, message: invalidStatus };
|
|
163
|
+
}
|
|
70
164
|
return { ok: true, result: { status: result.status, outcome } };
|
|
71
165
|
}
|
|
72
166
|
// Runs every step that can throw or fail closed, returning the outcome as
|
|
@@ -86,35 +180,84 @@ async function inspect(call, unexpected, invalidStatus) {
|
|
|
86
180
|
// gone.
|
|
87
181
|
export async function gatherProbe(ctx) {
|
|
88
182
|
// Order mirrors `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:24-40::spw_compute_effective_selection` exactly.
|
|
89
|
-
const selection = await computeEffectiveSelection(ctx.root, ctx.env);
|
|
183
|
+
const selection = ctx.selection ?? (await computeEffectiveSelection(ctx.root, ctx.env));
|
|
90
184
|
const outcomes = [];
|
|
91
185
|
const adapterContext = { root: ctx.root, env: ctx.env };
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
186
|
+
let resources;
|
|
187
|
+
try {
|
|
188
|
+
resources = [
|
|
189
|
+
ctx.adapter.preparationLocation(adapterContext).destinationRoot,
|
|
190
|
+
...(await ctx.adapter.mutationRoots(adapterContext)),
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
resources = null;
|
|
195
|
+
}
|
|
196
|
+
const before = resources === null
|
|
197
|
+
? []
|
|
198
|
+
: await ctx.coordination.observeResources(resources);
|
|
199
|
+
const collect = async (call, unexpected, invalidStatus, valid, replaySuccess = true) => {
|
|
200
|
+
const result = await inspect(call, unexpected, invalidStatus, valid);
|
|
201
|
+
if (result.result !== null && (replaySuccess || !result.result.outcome.ok))
|
|
95
202
|
outcomes.push(result.result.outcome);
|
|
96
203
|
return result;
|
|
97
204
|
};
|
|
98
|
-
const prepared = await collect(() => ctx.adapter.inspectPrepared(selection, adapterContext), "cannot inspect prepared harness state", "adapter reported a failure status for prepared harness inspection");
|
|
205
|
+
const prepared = await collect(() => ctx.adapter.inspectPrepared(selection, adapterContext), "cannot inspect prepared harness state", "adapter reported a failure status for prepared harness inspection", validPrepared);
|
|
99
206
|
if (!prepared.ok) {
|
|
100
207
|
return { status: 1, outcomes, message: prepared.message };
|
|
101
208
|
}
|
|
102
209
|
const failure = (site) => ctx.adapter.presentation.callFailure(site, adapterContext);
|
|
103
210
|
const installedFailure = failure("probe-installed");
|
|
104
|
-
const installed = await collect(() => ctx.adapter.inspectInstalled(selection, adapterContext), installedFailure.unexpected, installedFailure.invalidStatus);
|
|
211
|
+
const installed = await collect(() => ctx.adapter.inspectInstalled(selection, adapterContext), installedFailure.unexpected, installedFailure.invalidStatus, validInstalled);
|
|
105
212
|
if (!installed.ok) {
|
|
106
213
|
return { status: 1, outcomes, message: installed.message };
|
|
107
214
|
}
|
|
215
|
+
if (resources === null)
|
|
216
|
+
return {
|
|
217
|
+
status: 1,
|
|
218
|
+
outcomes,
|
|
219
|
+
message: "cannot determine harness observation resources",
|
|
220
|
+
};
|
|
108
221
|
const ownershipFailure = failure("probe-ownership");
|
|
109
|
-
const ownership = await collect(() => ctx.adapter.inspectOwnership(adapterContext), ownershipFailure.unexpected, ownershipFailure.invalidStatus);
|
|
222
|
+
const ownership = await collect(() => ctx.adapter.inspectOwnership(adapterContext), ownershipFailure.unexpected, ownershipFailure.invalidStatus, validOwnership);
|
|
110
223
|
if (!ownership.ok) {
|
|
111
224
|
return { status: 1, outcomes, message: ownership.message };
|
|
112
225
|
}
|
|
113
226
|
const controlFailure = failure("probe-control");
|
|
114
|
-
const control = await collect(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure.unexpected, controlFailure.invalidStatus);
|
|
227
|
+
const control = await collect(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure.unexpected, controlFailure.invalidStatus, validControl);
|
|
115
228
|
if (!control.ok) {
|
|
116
229
|
return { status: 1, outcomes, message: control.message };
|
|
117
230
|
}
|
|
231
|
+
const preparedAfter = await collect(() => ctx.adapter.inspectPrepared(selection, adapterContext), "cannot inspect prepared harness state", "adapter reported a failure status for prepared harness inspection", validPrepared, false);
|
|
232
|
+
if (!preparedAfter.ok)
|
|
233
|
+
return { status: 1, outcomes, message: preparedAfter.message };
|
|
234
|
+
const installedAfter = await collect(() => ctx.adapter.inspectInstalled(selection, adapterContext), installedFailure.unexpected, installedFailure.invalidStatus, validInstalled, false);
|
|
235
|
+
if (!installedAfter.ok)
|
|
236
|
+
return { status: 1, outcomes, message: installedAfter.message };
|
|
237
|
+
const controlAfter = await collect(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure.unexpected, controlFailure.invalidStatus, validControl, false);
|
|
238
|
+
if (!controlAfter.ok)
|
|
239
|
+
return { status: 1, outcomes, message: controlAfter.message };
|
|
240
|
+
const after = await ctx.coordination.observeResources(resources);
|
|
241
|
+
if (JSON.stringify(before) !== JSON.stringify(after) ||
|
|
242
|
+
JSON.stringify(prepared.result.outcome.result) !==
|
|
243
|
+
JSON.stringify(preparedAfter.result.outcome.result) ||
|
|
244
|
+
JSON.stringify(installed.result.outcome.result) !==
|
|
245
|
+
JSON.stringify(installedAfter.result.outcome.result) ||
|
|
246
|
+
JSON.stringify(control.result.outcome.result) !==
|
|
247
|
+
JSON.stringify(controlAfter.result.outcome.result)) {
|
|
248
|
+
return {
|
|
249
|
+
status: 1,
|
|
250
|
+
outcomes,
|
|
251
|
+
message: "harness state changed during observation; retry for a coherent probe",
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const unavailable = after.find((observation) => observation.state === "busy" || observation.state === "uninspectable");
|
|
255
|
+
if (unavailable)
|
|
256
|
+
return {
|
|
257
|
+
status: 1,
|
|
258
|
+
outcomes,
|
|
259
|
+
message: `harness resource is ${unavailable.state}: ${unavailable.resource}`,
|
|
260
|
+
};
|
|
118
261
|
const preparedState = prepared.result.outcome.result;
|
|
119
262
|
const installedState = installed.result.outcome.result;
|
|
120
263
|
return {
|
|
@@ -126,6 +269,13 @@ export async function gatherProbe(ctx) {
|
|
|
126
269
|
installed: installedState,
|
|
127
270
|
ownership: ownership.result.outcome.result,
|
|
128
271
|
control: control.result.outcome.result,
|
|
272
|
+
compatibility: preparedState.compatibility,
|
|
273
|
+
resources: after,
|
|
274
|
+
resourceState: control.result.outcome.result.recoveryState === "required"
|
|
275
|
+
? "recovery-required"
|
|
276
|
+
: after.some((observation) => observation.state === "owned")
|
|
277
|
+
? "owned"
|
|
278
|
+
: "idle",
|
|
129
279
|
status: preparedState.kind !== "current"
|
|
130
280
|
? "needs prepare"
|
|
131
281
|
: installedState.kind !== "current"
|
|
@@ -253,5 +403,5 @@ export async function runProbe(argv, ctx) {
|
|
|
253
403
|
}
|
|
254
404
|
const rendered = ctx.adapter.presentation.renderProbe(outcome.facts);
|
|
255
405
|
ctx.stdout.write(porcelain ? rendered.porcelain : rendered.human);
|
|
256
|
-
return 0;
|
|
406
|
+
return outcome.facts.resourceState === "recovery-required" ? 1 : 0;
|
|
257
407
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { oneLine } from "../cli-arguments.js";
|
|
6
6
|
import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
|
|
7
|
+
import { runWithMutation } from "./mutation.js";
|
|
7
8
|
import { replayOutcome } from "./probe.js";
|
|
8
9
|
async function invoke(call, failure, outcomes) {
|
|
9
10
|
let result;
|
|
@@ -52,7 +53,7 @@ class GatherFailure extends Error {
|
|
|
52
53
|
}
|
|
53
54
|
// Every step that can throw or fail closed, returning the outcome as data and
|
|
54
55
|
// performing NO writes. Same shape as gatherProbe
|
|
55
|
-
// (`src/commands/probe.ts
|
|
56
|
+
// (`src/commands/probe.ts::readonly facts: ProbeSnapshot<R>;`) and for the same
|
|
56
57
|
// reason: a write inside this try could raise EPIPE, be caught here, and be
|
|
57
58
|
// relabelled as a domain failure.
|
|
58
59
|
async function gatherUninstall(ctx) {
|
|
@@ -108,7 +109,7 @@ async function gatherUninstall(ctx) {
|
|
|
108
109
|
return {
|
|
109
110
|
status: 0,
|
|
110
111
|
outcomes,
|
|
111
|
-
output: ctx.adapter.presentation.renderRemovalCompletion(ownership),
|
|
112
|
+
output: ctx.adapter.presentation.renderRemovalCompletion(ownership, removalInput),
|
|
112
113
|
};
|
|
113
114
|
}, {
|
|
114
115
|
// Suppresses withWorkspace's throw on a POST-SUCCESS cleanup failure,
|
|
@@ -134,6 +135,9 @@ async function gatherUninstall(ctx) {
|
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
export async function runUninstall(argv, ctx) {
|
|
138
|
+
return await runWithMutation("uninstall", ctx, async (scoped) => performUninstall(argv, scoped));
|
|
139
|
+
}
|
|
140
|
+
async function performUninstall(argv, ctx) {
|
|
137
141
|
// scripts/uninstall never reads "$@", so extra arguments are silently
|
|
138
142
|
// ignored -- the same asymmetry runPrepare documents at its own
|
|
139
143
|
// `void argv;`.
|
package/dist/commands/update.js
CHANGED
|
@@ -9,6 +9,7 @@ import { oneLine } from "../cli-arguments.js";
|
|
|
9
9
|
import { gatherProbe, replayOutcome } from "./probe.js";
|
|
10
10
|
import { runInstall } from "./install.js";
|
|
11
11
|
import { runPrepare } from "./prepare.js";
|
|
12
|
+
import { runWithMutation } from "./mutation.js";
|
|
12
13
|
function writeOutput(output, ctx) {
|
|
13
14
|
for (const line of output.stdout)
|
|
14
15
|
ctx.stdout.write(`${line}\n`);
|
|
@@ -16,6 +17,9 @@ function writeOutput(output, ctx) {
|
|
|
16
17
|
ctx.stderr.write(`${line}\n`);
|
|
17
18
|
}
|
|
18
19
|
export async function runUpdate(argv, ctx) {
|
|
20
|
+
return await runWithMutation("update", ctx, async (scoped) => performUpdate(argv, scoped));
|
|
21
|
+
}
|
|
22
|
+
async function performUpdate(argv, ctx) {
|
|
19
23
|
// scripts/update never reads "$@", so extra arguments are silently
|
|
20
24
|
// ignored -- the same asymmetry runPrepare and runInstall document.
|
|
21
25
|
void argv;
|
|
@@ -30,7 +34,7 @@ export async function runUpdate(argv, ctx) {
|
|
|
30
34
|
// install's NOTE line.
|
|
31
35
|
//
|
|
32
36
|
// This is a THIRD consumer of gatherProbe's throw channel --
|
|
33
|
-
// `src/commands/probe.ts:
|
|
37
|
+
// `src/commands/probe.ts:469-527::THREE exceptions, all inherited and none a regression:`'s
|
|
34
38
|
// runProbe catch is the first and src/commands/install.ts's runInstall catch
|
|
35
39
|
// is the second. Because all three wrap the identical function, runProbe's
|
|
36
40
|
// long comment there enumerates exactly what can reach this stream too; not
|
package/dist/generated-plugin.js
CHANGED
|
@@ -14,7 +14,7 @@ export const DEFAULT_FS_DEPS = {
|
|
|
14
14
|
// The Python's own SEMVER_RE (:15-22). Deliberately not SEMVER_BASE_SOURCE
|
|
15
15
|
// from src/domain/refs.ts, which omits the `+build` component; widening the
|
|
16
16
|
// shared constant would change TAG_RE and is out of scope.
|
|
17
|
-
const SEMVER_RE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
17
|
+
export const SEMVER_RE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
18
18
|
const MANIFEST_JSON_PROFILE = {
|
|
19
19
|
duplicateKeys: "last-wins",
|
|
20
20
|
nonStandardConstants: "reject",
|
|
@@ -444,7 +444,7 @@ const REQUIRED_FILES = [
|
|
|
444
444
|
"CODE_OF_CONDUCT.md",
|
|
445
445
|
];
|
|
446
446
|
/** Python `sorted()` orders by code point; JavaScript's default sort does not. */
|
|
447
|
-
function compareByCodePoint(left, right) {
|
|
447
|
+
export function compareByCodePoint(left, right) {
|
|
448
448
|
// `Array.from` splits by code point, exactly as spreading would; oxlint's
|
|
449
449
|
// `no-misused-spread` rejects the spread form, and grapheme segmentation is
|
|
450
450
|
// the wrong unit here — Python compares code points.
|
|
@@ -459,7 +459,7 @@ function compareByCodePoint(left, right) {
|
|
|
459
459
|
}
|
|
460
460
|
return leftPoints.length - rightPoints.length;
|
|
461
461
|
}
|
|
462
|
-
async function validateSkillFrontmatter(skillMd, skillName, errors, deps) {
|
|
462
|
+
export async function validateSkillFrontmatter(skillMd, skillName, errors, deps) {
|
|
463
463
|
let contents;
|
|
464
464
|
try {
|
|
465
465
|
contents = STRICT_DECODER.decode(await deps.readFile(skillMd));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function activationBlock(value, allowExperimental) {
|
|
2
|
+
switch (value.kind) {
|
|
3
|
+
case "supported":
|
|
4
|
+
return null;
|
|
5
|
+
case "experimental":
|
|
6
|
+
return allowExperimental
|
|
7
|
+
? null
|
|
8
|
+
: "experimental integration requires --allow-experimental";
|
|
9
|
+
case "unknown":
|
|
10
|
+
case "unsupported":
|
|
11
|
+
return value.reason;
|
|
12
|
+
}
|
|
13
|
+
}
|
package/dist/manifest-overlay.js
CHANGED
|
@@ -69,7 +69,7 @@ function setMember(entries, key, value) {
|
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
71
|
* Map strict-json's diagnostics onto the CPython wording the callers assert.
|
|
72
|
-
* Repository policy at `AGENTS.md:
|
|
72
|
+
* Repository policy at `AGENTS.md:92::most diagnostics are asserted as`
|
|
73
73
|
* freezes most of these diagnostics as complete-string contracts.
|
|
74
74
|
*/
|
|
75
75
|
function translate(cause, source, path) {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { readArtifactFile, readArtifactObject, validateNativeSkill, } from "./artifact-tree.js";
|
|
5
|
+
import { SEMVER_RE } from "./generated-plugin.js";
|
|
6
|
+
import { validateSource } from "./selection.js";
|
|
7
|
+
const OFFICIAL_SOURCES = new Set([
|
|
8
|
+
"https://github.com/obra/superpowers",
|
|
9
|
+
"https://github.com/obra/superpowers.git",
|
|
10
|
+
"ssh://git@github.com/obra/superpowers.git",
|
|
11
|
+
"git@github.com:obra/superpowers.git",
|
|
12
|
+
]);
|
|
13
|
+
export function samePiSource(left, right) {
|
|
14
|
+
validateSource(left);
|
|
15
|
+
validateSource(right);
|
|
16
|
+
return (left === right ||
|
|
17
|
+
(OFFICIAL_SOURCES.has(left) && OFFICIAL_SOURCES.has(right)));
|
|
18
|
+
}
|
|
19
|
+
export async function assessPiCompatibility(root, selection) {
|
|
20
|
+
try {
|
|
21
|
+
validateSource(selection.effectiveSource);
|
|
22
|
+
const official = OFFICIAL_SOURCES.has(selection.effectiveSource);
|
|
23
|
+
const pkg = await readArtifactObject(root, join(root, "package.json"));
|
|
24
|
+
if (pkg.name !== "superpowers" ||
|
|
25
|
+
pkg.type !== "module" ||
|
|
26
|
+
typeof pkg.version !== "string" ||
|
|
27
|
+
!SEMVER_RE.test(pkg.version))
|
|
28
|
+
throw new Error("package metadata");
|
|
29
|
+
const pi = pkg.pi;
|
|
30
|
+
if (!pi ||
|
|
31
|
+
typeof pi !== "object" ||
|
|
32
|
+
Array.isArray(pi) ||
|
|
33
|
+
Object.keys(pi).length !== 2 ||
|
|
34
|
+
JSON.stringify(pi.extensions) !== '["./.pi/extensions/superpowers.ts"]' ||
|
|
35
|
+
JSON.stringify(pi.skills) !== '["./skills"]')
|
|
36
|
+
throw new Error("resource declaration");
|
|
37
|
+
for (const key of [
|
|
38
|
+
"dependencies",
|
|
39
|
+
"optionalDependencies",
|
|
40
|
+
"peerDependencies",
|
|
41
|
+
"bundleDependencies",
|
|
42
|
+
"bundledDependencies",
|
|
43
|
+
]) {
|
|
44
|
+
const value = pkg[key];
|
|
45
|
+
if (value !== undefined &&
|
|
46
|
+
(value === null ||
|
|
47
|
+
typeof value !== "object" ||
|
|
48
|
+
Object.keys(value).length !== 0))
|
|
49
|
+
throw new Error("runtime dependencies");
|
|
50
|
+
}
|
|
51
|
+
await validateNativeSkill(root);
|
|
52
|
+
const bootstrap = join(root, ".pi/extensions/superpowers.ts");
|
|
53
|
+
const bytes = await readArtifactFile(root, bootstrap, 4283);
|
|
54
|
+
if ((await lstat(bootstrap)).mode & 0o111 ||
|
|
55
|
+
bytes.length !== 4283 ||
|
|
56
|
+
createHash("sha256").update(bytes).digest("hex") !==
|
|
57
|
+
"39c27000c047f8a1399a76e032e4cd239d7bdb0be168a121f46a2ac719deaae0")
|
|
58
|
+
throw new Error("bootstrap implementation");
|
|
59
|
+
return {
|
|
60
|
+
kind: official ? "supported" : "experimental",
|
|
61
|
+
generation: "pi-native-bootstrap-v1",
|
|
62
|
+
reason: official
|
|
63
|
+
? "qualified upstream-native Pi bootstrap"
|
|
64
|
+
: "qualified Pi mechanics from a custom source",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return {
|
|
69
|
+
kind: "unsupported",
|
|
70
|
+
reason: `Pi package does not match the qualified native bootstrap profile: ${root}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { failureResult, successResult } from "./adapter-result.js";
|
|
2
|
+
import { classifyPathNoFollow } from "./safe-path.js";
|
|
3
|
+
import { installPi, removePi } from "./pi-install.js";
|
|
4
|
+
import { piPaths } from "./pi-paths.js";
|
|
5
|
+
import { inspectPiPrepared, piPreparationLocation, preparePiCandidate, readPiPrepared, } from "./pi-prepare.js";
|
|
6
|
+
import { piPresentation } from "./pi-presentation.js";
|
|
7
|
+
import { inspectPiControl, inspectPiInstalled, inspectPiOwnership, } from "./pi-state.js";
|
|
8
|
+
export const piHarness = {
|
|
9
|
+
preparationLocation: piPreparationLocation,
|
|
10
|
+
mutationRoots: async (ctx) => [
|
|
11
|
+
piPaths(ctx.env ?? {}, process.cwd()).agentDir,
|
|
12
|
+
],
|
|
13
|
+
validatePreparationBeforeFetch: async () => successResult("validate-pi-preparation", null, []),
|
|
14
|
+
prepareCandidate: preparePiCandidate,
|
|
15
|
+
inspectPrepared: inspectPiPrepared,
|
|
16
|
+
readPrepared: readPiPrepared,
|
|
17
|
+
inspectOwnership: inspectPiOwnership,
|
|
18
|
+
async inspectUpdateControl(ctx) {
|
|
19
|
+
const paths = piPaths(ctx.env ?? {}, process.cwd());
|
|
20
|
+
try {
|
|
21
|
+
if ((await classifyPathNoFollow(paths.recoveryRoot)) !== "missing") {
|
|
22
|
+
const decision = {
|
|
23
|
+
kind: "blocked",
|
|
24
|
+
output: {
|
|
25
|
+
stdout: [],
|
|
26
|
+
stderr: [
|
|
27
|
+
`error: Pi recovery required; preserve material at ${paths.recoveryRoot} for manual resolution`,
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
return successResult("inspect-pi-control", {
|
|
32
|
+
probeEligibility: decision,
|
|
33
|
+
mutationEligibility: decision,
|
|
34
|
+
presentationValue: `recovery required at ${paths.recoveryRoot}`,
|
|
35
|
+
recoveryState: "required",
|
|
36
|
+
}, []);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return failureResult("inspect-pi-control", "invalid-state", `cannot inspect Pi recovery state at ${paths.recoveryRoot}`, [], []);
|
|
41
|
+
}
|
|
42
|
+
return await inspectPiControl(ctx);
|
|
43
|
+
},
|
|
44
|
+
inspectInstalled: inspectPiInstalled,
|
|
45
|
+
install: installPi,
|
|
46
|
+
remove: removePi,
|
|
47
|
+
requirements(command, env) {
|
|
48
|
+
if (command !== "install" &&
|
|
49
|
+
command !== "update" &&
|
|
50
|
+
command !== "uninstall")
|
|
51
|
+
return [];
|
|
52
|
+
const executable = env.SUPERPOWERS_PI || "pi";
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
name: "pi",
|
|
56
|
+
executable,
|
|
57
|
+
lookup: "explicit-path-or-path",
|
|
58
|
+
missingMessage: `required command not found: ${executable} — install the Pi CLI or set SUPERPOWERS_PI`,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
},
|
|
62
|
+
presentation: piPresentation,
|
|
63
|
+
};
|