superpowers-manager 0.4.0 → 0.5.1
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 +76 -517
- package/dist/artifact-tree.js +111 -0
- package/dist/atomic.js +103 -7
- package/dist/cli-arguments.js +37 -0
- package/dist/cli.js +49 -12
- package/dist/commands/install.js +116 -17
- package/dist/commands/mutation.js +71 -0
- package/dist/commands/prepare.js +23 -17
- package/dist/commands/probe.js +164 -14
- package/dist/commands/uninstall.js +9 -5
- package/dist/commands/update.js +6 -2
- package/dist/domain/refs.js +4 -0
- package/dist/harness-compatibility.js +13 -0
- package/dist/{adapter.js → harnesses/codex/adapter.js} +17 -13
- package/dist/harnesses/codex/compatibility.js +126 -0
- package/dist/harnesses/codex/conflicts.js +99 -0
- package/dist/{generated-plugin.js → harnesses/codex/generated-plugin.js} +7 -73
- package/dist/{codex-harness.js → harnesses/codex/harness.js} +55 -5
- package/dist/{hooks.js → harnesses/codex/hooks.js} +6 -6
- package/dist/{codex-json.js → harnesses/codex/json.js} +9 -2
- package/dist/{lifecycle.js → harnesses/codex/lifecycle.js} +2 -2
- package/dist/{manifest-overlay.js → harnesses/codex/manifest-overlay.js} +5 -5
- package/dist/{codex-prepare.js → harnesses/codex/prepare.js} +72 -16
- package/dist/{codex-presentation.js → harnesses/codex/presentation.js} +27 -4
- package/dist/{codex-state.js → harnesses/codex/state.js} +3 -3
- package/dist/harnesses/pi/compatibility.js +73 -0
- package/dist/harnesses/pi/harness.js +63 -0
- package/dist/harnesses/pi/install.js +495 -0
- package/dist/harnesses/pi/native.js +81 -0
- package/dist/harnesses/pi/package.js +153 -0
- package/dist/harnesses/pi/paths.js +30 -0
- package/dist/harnesses/pi/prepare.js +111 -0
- package/dist/harnesses/pi/presentation.js +67 -0
- package/dist/harnesses/pi/settings.js +171 -0
- package/dist/harnesses/pi/shared-skills.js +413 -0
- package/dist/harnesses/pi/state.js +311 -0
- package/dist/python-text.js +16 -0
- package/dist/resource-lock.js +265 -0
- package/dist/safe-path.js +21 -5
- package/dist/skill-validation.js +52 -0
- package/dist/upstream-workspace.js +8 -0
- package/dist/validate-generated-plugin-cli.js +1 -1
- package/dist/validator.js +2 -1
- package/package.json +12 -8
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { computeEffectiveSelection } from "../effective-selection.js";
|
|
2
|
+
import { upstreamCacheRoot } from "../upstream-workspace.js";
|
|
3
|
+
import { SafetyError } from "../safety-error.js";
|
|
4
|
+
import { isAbsolute } from "node:path";
|
|
5
|
+
import { oneLine } from "../cli-arguments.js";
|
|
6
|
+
export async function withMutation(command, ctx, action) {
|
|
7
|
+
// Selection validation precedes every adapter call and lock creation. A
|
|
8
|
+
// nested mutation receives the already resolved value and cannot observe a
|
|
9
|
+
// saved-selection change midway through one invocation.
|
|
10
|
+
const selection = command === "uninstall"
|
|
11
|
+
? ctx.selection
|
|
12
|
+
: (ctx.selection ?? (await computeEffectiveSelection(ctx.root, ctx.env)));
|
|
13
|
+
const scoped = selection === undefined ? ctx : { ...ctx, selection };
|
|
14
|
+
const adapterContext = { root: ctx.root, env: ctx.env };
|
|
15
|
+
let location;
|
|
16
|
+
try {
|
|
17
|
+
location = ctx.adapter.preparationLocation(adapterContext);
|
|
18
|
+
}
|
|
19
|
+
catch (cause) {
|
|
20
|
+
throw new SafetyError("mutation", "cannot determine preparation location", {
|
|
21
|
+
cause,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (!isAbsolute(location.destinationRoot))
|
|
25
|
+
throw new SafetyError("mutation", "adapter returned a non-absolute preparation destination");
|
|
26
|
+
if (!location.stagingLeaf ||
|
|
27
|
+
location.stagingLeaf === "." ||
|
|
28
|
+
location.stagingLeaf === ".." ||
|
|
29
|
+
location.stagingLeaf.includes("/") ||
|
|
30
|
+
location.stagingLeaf.includes("\\"))
|
|
31
|
+
throw new SafetyError("mutation", "adapter returned an invalid preparation staging leaf");
|
|
32
|
+
let mutationRoots;
|
|
33
|
+
try {
|
|
34
|
+
mutationRoots = await ctx.adapter.mutationRoots(adapterContext);
|
|
35
|
+
if (!Array.isArray(mutationRoots) ||
|
|
36
|
+
mutationRoots.some((root) => typeof root !== "string" || !isAbsolute(root)))
|
|
37
|
+
throw new Error("invalid roots");
|
|
38
|
+
}
|
|
39
|
+
catch (cause) {
|
|
40
|
+
throw new SafetyError("mutation", "cannot determine harness mutation resources", { cause });
|
|
41
|
+
}
|
|
42
|
+
const resources = [location.destinationRoot, ...mutationRoots];
|
|
43
|
+
if (command === "prepare") {
|
|
44
|
+
resources.push(upstreamCacheRoot(ctx.root, ctx.env, process.cwd()));
|
|
45
|
+
}
|
|
46
|
+
// The coordinator sorts each acquisition and refuses conflicts immediately.
|
|
47
|
+
// A nested prepare may add a cache lock that sorts before an outer target;
|
|
48
|
+
// it cannot deadlock because no acquisition waits, while already-held roots
|
|
49
|
+
// are reentrant for this invocation.
|
|
50
|
+
return await ctx.coordination.withResources(resources, async () => action(scoped));
|
|
51
|
+
}
|
|
52
|
+
export async function runWithMutation(command, ctx, action) {
|
|
53
|
+
let actionThrew = false;
|
|
54
|
+
try {
|
|
55
|
+
return await withMutation(command, ctx, async (scoped) => {
|
|
56
|
+
try {
|
|
57
|
+
return await action(scoped);
|
|
58
|
+
}
|
|
59
|
+
catch (cause) {
|
|
60
|
+
actionThrew = true;
|
|
61
|
+
throw cause;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (cause) {
|
|
66
|
+
if (actionThrew)
|
|
67
|
+
throw cause;
|
|
68
|
+
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/commands/prepare.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
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
|
|
15
|
-
// (`src/hooks.ts:44::function hookError`).
|
|
17
|
+
// (`src/harnesses/codex/hooks.ts:44::function hookError`).
|
|
16
18
|
function prepareError(message, cause) {
|
|
17
19
|
return new SafetyError("prepare", message, { cause });
|
|
18
20
|
}
|
|
@@ -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.
|
|
@@ -335,8 +341,8 @@ export async function runPrepare(argv, ctx) {
|
|
|
335
341
|
// failure (its cause is the shared runner's (src/validator.ts) captured
|
|
336
342
|
// spawn error, which oneLine never reads -- it takes .message only);
|
|
337
343
|
// readManifest's three hookError messages
|
|
338
|
-
// (`src/hooks.ts:113-138::readManifest`), pinned by
|
|
339
|
-
// `tests/unit/hooks.test.ts:95::void test("readManifest diagnostics` as carrying no reader vocabulary or
|
|
344
|
+
// (`src/harnesses/codex/hooks.ts:113-138::readManifest`), pinned by
|
|
345
|
+
// `tests/unit/harnesses/codex/hooks.test.ts:95::void test("readManifest diagnostics` as carrying no reader vocabulary or
|
|
340
346
|
// errno; and SafetyErrors from gitSafeSource, writeProvenance, and
|
|
341
347
|
// withWorkspace.
|
|
342
348
|
//
|
|
@@ -376,7 +382,7 @@ export async function runPrepare(argv, ctx) {
|
|
|
376
382
|
// only, so it bounds how much of that text lands, not what it may
|
|
377
383
|
// contain.
|
|
378
384
|
//
|
|
379
|
-
// runAdapter's closing `throw cause` (src/adapter.ts) does NOT arrive
|
|
385
|
+
// runAdapter's closing `throw cause` (src/harnesses/codex/adapter.ts) does NOT arrive
|
|
380
386
|
// here -- the call site catches it and converts it to a hand-written
|
|
381
387
|
// message per AGENTS.md's reader-diagnostics rule.
|
|
382
388
|
//
|
|
@@ -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
|
@@ -11,7 +11,7 @@ export const PROBE_USAGE = "error: usage: superpowers-manager probe [--porcelain
|
|
|
11
11
|
// silent diagnostics regression, not a simplification.
|
|
12
12
|
//
|
|
13
13
|
// Interpolating error.message and each hint is the sanctioned form (AGENTS.md):
|
|
14
|
-
// src/adapter.ts has 52 `fail()` sites and none interpolates a caught error's
|
|
14
|
+
// src/harnesses/codex/adapter.ts has 52 `fail()` sites and none interpolates a caught error's
|
|
15
15
|
// message, so the callee owns every failure reachable on this path. Those two
|
|
16
16
|
// writes now live in writeAdapterFailure (src/adapter-result.ts), which
|
|
17
17
|
// validates all three strings before the first of them reaches the stream.
|
|
@@ -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
|
|
@@ -41,17 +121,23 @@ export function replayOutcome(outcome, ctx) {
|
|
|
41
121
|
// evidence and report it as success.
|
|
42
122
|
//
|
|
43
123
|
// It does still THROW for a non-AdapterFailure cause (runAdapter's closing
|
|
44
|
-
// `throw cause`, src/adapter.ts). That is caught here rather than in runProbe's
|
|
124
|
+
// `throw cause`, src/harnesses/codex/adapter.ts). That is caught here rather than in runProbe's
|
|
45
125
|
// outer catch, because the two need different diagnostics -- see spec §3.3a.
|
|
46
126
|
//
|
|
47
127
|
// Reached through ctx.adapter, not a direct module-level dependency on the
|
|
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"
|
|
@@ -227,9 +377,9 @@ export async function runProbe(argv, ctx) {
|
|
|
227
377
|
// which catches every failure and returns "".
|
|
228
378
|
//
|
|
229
379
|
// A non-AdapterFailure re-thrown by runAdapter's closing `throw cause`
|
|
230
|
-
// (src/adapter.ts) does NOT reach here: inspect() catches it and converts
|
|
380
|
+
// (src/harnesses/codex/adapter.ts) does NOT reach here: inspect() catches it and converts
|
|
231
381
|
// it to a hand-written message per AGENTS.md's reader-diagnostics rule -- a
|
|
232
|
-
// rethrown cause is exactly the failure src/adapter.ts declined to own, so
|
|
382
|
+
// rethrown cause is exactly the failure src/harnesses/codex/adapter.ts declined to own, so
|
|
233
383
|
// its text must never reach this stream. See §3.3a.
|
|
234
384
|
//
|
|
235
385
|
// gatherProbe performs no writes of its own, so this catch cannot also be
|
|
@@ -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
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Ports scripts/uninstall. The shell sourced common.sh, provenance.sh,
|
|
2
|
-
// lifecycle.sh and adapter.sh; the predicates now live in src/lifecycle.ts
|
|
2
|
+
// lifecycle.sh and adapter.sh; the predicates now live in src/harnesses/codex/lifecycle.ts
|
|
3
3
|
// and the adapter arrives through ctx.adapter.
|
|
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,
|
|
@@ -125,7 +126,7 @@ async function gatherUninstall(ctx) {
|
|
|
125
126
|
// Reachable only for mkdtemp failure, with nothing collected yet: this
|
|
126
127
|
// callback never throws -- every ctx.adapter throw is already caught
|
|
127
128
|
// inside invoke(), and presenceFlag/verifyUninstalledResources/
|
|
128
|
-
// reportLegacyState are pure (src/lifecycle.ts's header comment) -- so a
|
|
129
|
+
// reportLegacyState are pure (src/harnesses/codex/lifecycle.ts's header comment) -- so a
|
|
129
130
|
// post-success cleanup failure is handled by onCleanupFailure above and
|
|
130
131
|
// cannot reach here. Wrapping with `outcomes` anyway keeps the class
|
|
131
132
|
// total over its declared contract rather than assuming the callback's
|
|
@@ -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;`.
|
|
@@ -159,7 +163,7 @@ export async function runUninstall(argv, ctx) {
|
|
|
159
163
|
// blindly.
|
|
160
164
|
//
|
|
161
165
|
// A cause outside ctx.adapter's AdapterFailure guard
|
|
162
|
-
// (`src/adapter.ts:973-999::async function runCodexOperation(`) does NOT
|
|
166
|
+
// (`src/harnesses/codex/adapter.ts:973-999::async function runCodexOperation(`) does NOT
|
|
163
167
|
// reach here: invoke() catches it inside gatherUninstall and converts it
|
|
164
168
|
// to a hand-written message carried as UninstallOutcome data, exactly as
|
|
165
169
|
// src/commands/probe.ts's inspect() does for the same cause.
|
package/dist/commands/update.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Ports scripts/update. The shell sourced common.sh and lifecycle.sh; the
|
|
2
|
-
// predicates now live in src/lifecycle.ts. Unlike install and uninstall,
|
|
2
|
+
// predicates now live in src/harnesses/codex/lifecycle.ts. Unlike install and uninstall,
|
|
3
3
|
// update issues no ctx.adapter call of its own: every adapter interaction
|
|
4
4
|
// this module needs already happens inside gatherProbe, runPrepare or
|
|
5
5
|
// runInstall, each of which owns its own §4.2a-conformant invoke() gate. That
|
|
@@ -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/domain/refs.js
CHANGED
|
@@ -3,6 +3,10 @@ const SEMVER_BASE_SOURCE = "(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0
|
|
|
3
3
|
"(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?";
|
|
4
4
|
export const SEMVER_BASE_RE = new RegExp(`^${SEMVER_BASE_SOURCE}$`);
|
|
5
5
|
export const TAG_RE = new RegExp(`^v${SEMVER_BASE_SOURCE}$`);
|
|
6
|
+
// The Python's own SEMVER_RE (:15-22). Deliberately not SEMVER_BASE_SOURCE
|
|
7
|
+
// from this module, which omits the `+build` component; widening the shared
|
|
8
|
+
// constant would change TAG_RE and is out of scope.
|
|
9
|
+
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-]+)*)?$/;
|
|
6
10
|
export const COMMIT_RE = /^[0-9a-f]{40}$/;
|
|
7
11
|
export const COMMIT_INPUT_RE = /^[0-9A-Fa-f]{40}$/;
|
|
8
12
|
export function isTagRef(value) {
|
|
@@ -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
|
+
}
|
|
@@ -4,19 +4,20 @@ import { constants } from "node:fs";
|
|
|
4
4
|
import { access, copyFile, mkdir, readFile, realpath, stat, writeFile, } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { delimiter, isAbsolute, join } from "node:path";
|
|
7
|
-
import { AdapterMessageLog, failureResult, successResult, } from "
|
|
8
|
-
import { activePluginVersionFromJson, installedListingHas, marketplaceRootFromJson, } from "./
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
7
|
+
import { AdapterMessageLog, failureResult, successResult, } from "../../adapter-result.js";
|
|
8
|
+
import { activePluginVersionFromJson, installedListingHas, marketplaceRootFromJson, } from "./json.js";
|
|
9
|
+
import { CODEX_LEGACY_PLUGIN_ID, CODEX_MANAGER_PLUGIN_ID, inspectCodexConflicts, } from "./conflicts.js";
|
|
10
|
+
import { oneLine } from "../../cli-arguments.js";
|
|
11
|
+
import { installedCommitFromRoot, installedRootForVersion, pathsEqual, } from "./state.js";
|
|
11
12
|
import { validateGeneratedPlugin } from "./generated-plugin.js";
|
|
12
13
|
import { classifyHooks, materializeHooks, readManifest, } from "./hooks.js";
|
|
13
14
|
import { applyManifestOverlay } from "./manifest-overlay.js";
|
|
14
|
-
import { readCodexBuildSource } from "
|
|
15
|
-
import { isAcceptedSplitValue } from "
|
|
16
|
-
import { withWorkspace, workspaceRemovalFailure } from "
|
|
17
|
-
const PLUGIN_ID =
|
|
15
|
+
import { readCodexBuildSource } from "../../provenance.js";
|
|
16
|
+
import { isAcceptedSplitValue } from "../../validate-generated-plugin-cli.js";
|
|
17
|
+
import { withWorkspace, workspaceRemovalFailure } from "../../workspace.js";
|
|
18
|
+
const PLUGIN_ID = CODEX_MANAGER_PLUGIN_ID;
|
|
18
19
|
const MARKETPLACE_NAME = "superpowers-manager";
|
|
19
|
-
const LEGACY_PLUGIN_ID =
|
|
20
|
+
const LEGACY_PLUGIN_ID = CODEX_LEGACY_PLUGIN_ID;
|
|
20
21
|
const LEGACY_MARKETPLACE_NAME = "superpowers-wrapper";
|
|
21
22
|
class AdapterFailure extends Error {
|
|
22
23
|
code;
|
|
@@ -88,7 +89,7 @@ function runCommand(file, args, env) {
|
|
|
88
89
|
});
|
|
89
90
|
});
|
|
90
91
|
}
|
|
91
|
-
// Exported only so tests/unit/adapter.test.
|
|
92
|
+
// Exported only so tests/unit/harnesses/codex/adapter.test.ts can assert the env scrub
|
|
92
93
|
// directly. No production caller uses this name.
|
|
93
94
|
export { runCommand as runCommandForTest };
|
|
94
95
|
function commandFailed(result) {
|
|
@@ -280,7 +281,7 @@ async function runBuild(input, env, log) {
|
|
|
280
281
|
// path — three are the frozen CPython wording, and the fourth (the
|
|
281
282
|
// numeric-overflow diagnostic, which has no CPython oracle wording
|
|
282
283
|
// to match) now carries the path via its own rewrap in
|
|
283
|
-
// src/manifest-overlay.ts. Emit as-is, with no added prefix — a
|
|
284
|
+
// src/harnesses/codex/manifest-overlay.ts. Emit as-is, with no added prefix — a
|
|
284
285
|
// prefix here would double up the path these messages already
|
|
285
286
|
// name.
|
|
286
287
|
log.appendText("stderr", oneLine(cause));
|
|
@@ -486,7 +487,7 @@ async function runUninstall(input, env, log) {
|
|
|
486
487
|
throw cause;
|
|
487
488
|
}
|
|
488
489
|
}
|
|
489
|
-
async function runInspect(view, env, log) {
|
|
490
|
+
async function runInspect(view, context, env, log) {
|
|
490
491
|
if (view === "update-control") {
|
|
491
492
|
return { view: "update-control", update_control: "managed" };
|
|
492
493
|
}
|
|
@@ -552,9 +553,11 @@ async function runInspect(view, env, log) {
|
|
|
552
553
|
let legacyPlugin;
|
|
553
554
|
let managerMarketplace;
|
|
554
555
|
let legacyMarketplace;
|
|
556
|
+
let conflicts;
|
|
555
557
|
try {
|
|
556
558
|
managerPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", PLUGIN_ID);
|
|
557
559
|
legacyPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", LEGACY_PLUGIN_ID);
|
|
560
|
+
conflicts = await inspectCodexConflicts({ root: context.root, env }, plugins.stdout.toString("utf8"));
|
|
558
561
|
}
|
|
559
562
|
catch {
|
|
560
563
|
fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
|
|
@@ -585,6 +588,7 @@ async function runInspect(view, env, log) {
|
|
|
585
588
|
: legacyPresent
|
|
586
589
|
? "legacy"
|
|
587
590
|
: "neither",
|
|
591
|
+
conflicts: [...conflicts],
|
|
588
592
|
};
|
|
589
593
|
}, { onCleanupFailure: reportOrphanedWorkspace(log) });
|
|
590
594
|
}
|
|
@@ -622,7 +626,7 @@ export function codexRemove(input, context) {
|
|
|
622
626
|
return runCodexOperation("uninstall", context, (env, log) => runUninstall(input, env, log));
|
|
623
627
|
}
|
|
624
628
|
export function codexInspect(view, context) {
|
|
625
|
-
return runCodexOperation("inspect", context, (env, log) => runInspect(view, env, log));
|
|
629
|
+
return runCodexOperation("inspect", context, (env, log) => runInspect(view, context, env, log));
|
|
626
630
|
}
|
|
627
631
|
export async function runAdapter(argv, context) {
|
|
628
632
|
const rawOperation = argv[0];
|