superpowers-manager 0.3.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.
Files changed (41) hide show
  1. package/README.md +63 -325
  2. package/dist/adapter.js +91 -46
  3. package/dist/artifact-tree.js +110 -0
  4. package/dist/atomic.js +103 -7
  5. package/dist/cli-arguments.js +37 -0
  6. package/dist/cli.js +97 -40
  7. package/dist/codex-compatibility.js +126 -0
  8. package/dist/codex-conflicts.js +99 -0
  9. package/dist/codex-harness.js +321 -0
  10. package/dist/codex-json.js +7 -0
  11. package/dist/codex-prepare.js +255 -0
  12. package/dist/codex-presentation.js +282 -0
  13. package/dist/commands/install.js +187 -173
  14. package/dist/commands/mutation.js +71 -0
  15. package/dist/commands/prepare.js +87 -188
  16. package/dist/commands/probe.js +191 -176
  17. package/dist/commands/uninstall.js +52 -139
  18. package/dist/commands/update.js +33 -88
  19. package/dist/generated-plugin.js +3 -3
  20. package/dist/harness-compatibility.js +13 -0
  21. package/dist/harness.js +1 -0
  22. package/dist/lifecycle.js +1 -86
  23. package/dist/manifest-overlay.js +2 -2
  24. package/dist/pi-compatibility.js +73 -0
  25. package/dist/pi-harness.js +63 -0
  26. package/dist/pi-install.js +495 -0
  27. package/dist/pi-native.js +81 -0
  28. package/dist/pi-package.js +153 -0
  29. package/dist/pi-paths.js +30 -0
  30. package/dist/pi-prepare.js +111 -0
  31. package/dist/pi-presentation.js +67 -0
  32. package/dist/pi-settings.js +171 -0
  33. package/dist/pi-shared-skills.js +413 -0
  34. package/dist/pi-state.js +311 -0
  35. package/dist/resource-lock.js +265 -0
  36. package/dist/safe-path.js +21 -5
  37. package/dist/strict-json.js +1 -1
  38. package/dist/upstream-workspace.js +8 -0
  39. package/dist/validator.js +3 -2
  40. package/package.json +19 -18
  41. package/bin/superpowers-manager.js +0 -16
@@ -1,115 +1,7 @@
1
1
  import { assertFailureWritable, writeAdapterFailure, } from "../adapter-result.js";
2
2
  import { oneLine } from "../cli-arguments.js";
3
3
  import { computeEffectiveSelection } from "../effective-selection.js";
4
- import { generatedCommitOrEmpty } from "../provenance.js";
5
- import { displaySource } from "../selection.js";
6
- import { statusForCommits } from "../status.js";
7
4
  export const PROBE_USAGE = "error: usage: superpowers-manager probe [--porcelain]\n";
8
- // One ordered table drives both formats, so the porcelain key order
9
- // (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:43-59::printf 'requested_ref`) and the human label order (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:61-77::printf 'requested ref`) cannot
10
- // drift apart. Two parallel lists could each stay self-consistent while
11
- // disagreeing with one another.
12
- function fields(f) {
13
- return [
14
- { key: "requested_ref", label: "requested ref", value: f.requestedRef },
15
- { key: "resolved_ref", label: "resolved ref", value: f.resolvedRef },
16
- { key: "desired_commit", label: "desired commit", value: f.desiredCommit },
17
- {
18
- key: "generated_commit",
19
- label: "generated plugin commit",
20
- value: f.generatedCommit,
21
- absent: "not present",
22
- },
23
- {
24
- key: "installed_commit",
25
- label: "installed manager commit or fingerprint",
26
- value: f.installedCommit,
27
- absent: "not detected",
28
- },
29
- {
30
- key: "identity_state",
31
- label: "Codex identity state",
32
- value: f.identityState,
33
- },
34
- { key: "status", label: "status", value: f.status },
35
- {
36
- key: "selection_origin",
37
- label: "selection origin",
38
- value: f.selectionOrigin,
39
- },
40
- { key: "selection_mode", label: "selection mode", value: f.selectionMode },
41
- {
42
- key: "upstream_source_origin",
43
- label: "upstream source origin",
44
- value: f.upstreamSourceOrigin,
45
- },
46
- {
47
- key: "effective_source",
48
- label: "effective source",
49
- value: f.effectiveSource,
50
- },
51
- { key: "saved_mode", label: "saved mode", value: f.savedMode },
52
- { key: "saved_source", label: "saved source", value: f.savedSource },
53
- {
54
- key: "saved_requested_ref",
55
- label: "saved requested ref",
56
- value: f.savedRequestedRef,
57
- },
58
- {
59
- key: "saved_resolved_ref",
60
- label: "saved resolved ref",
61
- value: f.savedResolvedRef,
62
- },
63
- { key: "saved_commit", label: "saved commit", value: f.savedCommit },
64
- { key: "update_control", label: "update control", value: f.updateControl },
65
- ];
66
- }
67
- // Derived from the same fields() table, so the key list a test asserts and
68
- // the key list formatPorcelain emits cannot be different lists. The facts
69
- // argument is irrelevant here — fields() is total over ProbeFacts and the
70
- // keys do not depend on the values.
71
- const NO_FACTS = {
72
- requestedRef: "",
73
- resolvedRef: "",
74
- desiredCommit: "",
75
- generatedCommit: "",
76
- installedCommit: "",
77
- identityState: "",
78
- status: "",
79
- selectionOrigin: "",
80
- selectionMode: "",
81
- upstreamSourceOrigin: "",
82
- effectiveSource: "",
83
- savedMode: "",
84
- savedSource: "",
85
- savedRequestedRef: "",
86
- savedResolvedRef: "",
87
- savedCommit: "",
88
- updateControl: "",
89
- };
90
- export const PROBE_PORCELAIN_KEYS = fields(NO_FACTS).map((field) => field.key);
91
- export function formatPorcelain(f) {
92
- return fields(f)
93
- .map((field) => `${field.key}=${field.value}\n`)
94
- .join("");
95
- }
96
- export function formatHuman(f) {
97
- let text = fields(f)
98
- .map((field) => {
99
- const shown = field.value.length === 0 && field.absent !== undefined
100
- ? field.absent
101
- : field.value;
102
- return `${field.label}: ${shown}\n`;
103
- })
104
- .join("");
105
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:78-81::if [ "$SPW_SELECTION_ORIGIN`.
106
- if (f.selectionOrigin !== f.upstreamSourceOrigin) {
107
- text +=
108
- "warning: effective ref and source have mixed origins " +
109
- `(ref: ${f.selectionOrigin}, source: ${f.upstreamSourceOrigin})\n`;
110
- }
111
- return text;
112
- }
113
5
  // Ports scripts/core/validate-adapter-response.py's replay (:235-238) and its
114
6
  // error/hint block (:269-272). The shell ran that validator on EVERY adapter
115
7
  // response, so adapter messages reached the operator on their declared streams
@@ -141,6 +33,86 @@ export function replayOutcome(outcome, ctx) {
141
33
  // stored, so scanning it here would re-check a population that cannot fail.
142
34
  writeAdapterFailure(ctx, outcome);
143
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
+ }
144
116
  // `runAdapter` reports a CONTROLLED failure by RETURN VALUE, not by throwing
145
117
  // (`src/adapter-result.ts:32-35::export interface AdapterResult`). The shell got
146
118
  // fail-closed behaviour for free: spw_invoke_adapter returned 1 and
@@ -156,25 +128,19 @@ export function replayOutcome(outcome, ctx) {
156
128
  // adapter module: src/commands/prepare.ts and this module are the two the
157
129
  // injected double must observe, because install reaches the adapter through
158
130
  // gatherProbe and runPrepare. Spec §4.5.
159
- async function inspect(view, key, ctx) {
131
+ async function inspect(call, unexpected, invalidStatus, valid) {
160
132
  let result;
161
133
  try {
162
- result = await ctx.adapter(["inspect", "--view", view], {
163
- root: ctx.root,
164
- env: ctx.env,
165
- });
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 };
166
141
  }
167
142
  catch {
168
- // Deliberately does NOT interpolate the cause. A rethrown non-
169
- // AdapterFailure is by construction the one failure src/adapter.ts chose
170
- // not to own: free-form text of unknown provenance, which AGENTS.md bars
171
- // from this stream. `view` is a bounded token -- one of three literals
172
- // this function is ever called with -- so naming the input is safe.
173
- return {
174
- ok: false,
175
- outcome: null,
176
- message: `cannot inspect Codex adapter state for view ${view}`,
177
- };
143
+ return { ok: false, result: null, message: unexpected };
178
144
  }
179
145
  const outcome = result.outcome;
180
146
  if (result.status !== 0 || !outcome.ok) {
@@ -183,27 +149,19 @@ async function inspect(view, key, ctx) {
183
149
  // rather than falling through to a replay that would print nothing.
184
150
  return {
185
151
  ok: false,
186
- outcome,
187
- message: outcome.ok
188
- ? `adapter reported a failure status for inspect --view ${view}`
189
- : null,
152
+ result,
153
+ message: outcome.ok ? invalidStatus : null,
190
154
  };
191
155
  }
192
- const value = outcome.result?.[key];
193
- // The Python reader printed the empty string for a JSON null
194
- // (scripts/core/provenance.sh's spw_json_get), and `fingerprint` is null
195
- // whenever no plugin version is active (`src/adapter.ts:820::fingerprint: null`).
196
- if (value === null || value === undefined) {
197
- return { ok: true, value: "", outcome };
156
+ try {
157
+ if (!valid(outcome.result)) {
158
+ return { ok: false, result, message: invalidStatus };
159
+ }
198
160
  }
199
- if (typeof value !== "string") {
200
- return {
201
- ok: false,
202
- outcome,
203
- message: `adapter returned a non-string ${key} for inspect --view ${view}`,
204
- };
161
+ catch {
162
+ return { ok: false, result, message: invalidStatus };
205
163
  }
206
- return { ok: true, value, outcome };
164
+ return { ok: true, result: { status: result.status, outcome } };
207
165
  }
208
166
  // Runs every step that can throw or fail closed, returning the outcome as
209
167
  // data. It performs no writes of its own, so nothing inside runProbe's try can
@@ -222,51 +180,107 @@ async function inspect(view, key, ctx) {
222
180
  // gone.
223
181
  export async function gatherProbe(ctx) {
224
182
  // Order mirrors `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:24-40::spw_compute_effective_selection` exactly.
225
- const selection = await computeEffectiveSelection(ctx.root, ctx.env);
226
- const generatedCommit = await generatedCommitOrEmpty(ctx.root);
183
+ const selection = ctx.selection ?? (await computeEffectiveSelection(ctx.root, ctx.env));
227
184
  const outcomes = [];
228
- const collect = async (view, key) => {
229
- const result = await inspect(view, key, ctx);
230
- if (result.outcome !== null)
231
- outcomes.push(result.outcome);
185
+ const adapterContext = { root: ctx.root, env: ctx.env };
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))
202
+ outcomes.push(result.result.outcome);
232
203
  return result;
233
204
  };
234
- const fingerprint = await collect("fingerprint", "fingerprint");
235
- if (!fingerprint.ok) {
236
- return { status: 1, outcomes, message: fingerprint.message };
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);
206
+ if (!prepared.ok) {
207
+ return { status: 1, outcomes, message: prepared.message };
208
+ }
209
+ const failure = (site) => ctx.adapter.presentation.callFailure(site, adapterContext);
210
+ const installedFailure = failure("probe-installed");
211
+ const installed = await collect(() => ctx.adapter.inspectInstalled(selection, adapterContext), installedFailure.unexpected, installedFailure.invalidStatus, validInstalled);
212
+ if (!installed.ok) {
213
+ return { status: 1, outcomes, message: installed.message };
237
214
  }
238
- const ownership = await collect("ownership", "identity_state");
215
+ if (resources === null)
216
+ return {
217
+ status: 1,
218
+ outcomes,
219
+ message: "cannot determine harness observation resources",
220
+ };
221
+ const ownershipFailure = failure("probe-ownership");
222
+ const ownership = await collect(() => ctx.adapter.inspectOwnership(adapterContext), ownershipFailure.unexpected, ownershipFailure.invalidStatus, validOwnership);
239
223
  if (!ownership.ok) {
240
224
  return { status: 1, outcomes, message: ownership.message };
241
225
  }
242
- const updateControl = await collect("update-control", "update_control");
243
- if (!updateControl.ok) {
244
- return { status: 1, outcomes, message: updateControl.message };
226
+ const controlFailure = failure("probe-control");
227
+ const control = await collect(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure.unexpected, controlFailure.invalidStatus, validControl);
228
+ if (!control.ok) {
229
+ return { status: 1, outcomes, message: control.message };
245
230
  }
246
- const saved = selection.saved;
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
+ };
261
+ const preparedState = prepared.result.outcome.result;
262
+ const installedState = installed.result.outcome.result;
247
263
  return {
248
264
  status: 0,
249
265
  outcomes,
250
266
  facts: {
251
- requestedRef: selection.requestedRef,
252
- resolvedRef: selection.resolvedRef,
253
- desiredCommit: selection.desiredCommit,
254
- generatedCommit,
255
- installedCommit: fingerprint.value,
256
- identityState: ownership.value,
257
- status: statusForCommits(selection.desiredCommit, generatedCommit, fingerprint.value),
258
- selectionOrigin: selection.selectionOrigin,
259
- selectionMode: selection.selectionMode,
260
- upstreamSourceOrigin: selection.upstreamSourceOrigin,
261
- effectiveSource: displaySource(selection.effectiveSource),
262
- savedMode: saved.saved_mode,
263
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:26-30::if [ -`: an absent saved source stays empty rather than
264
- // being run through displaySource, which would render <redacted-source>.
265
- savedSource: saved.saved_source.length > 0 ? displaySource(saved.saved_source) : "",
266
- savedRequestedRef: saved.saved_requested_ref,
267
- savedResolvedRef: saved.saved_resolved_ref,
268
- savedCommit: saved.saved_commit,
269
- updateControl: updateControl.value,
267
+ selection,
268
+ prepared: preparedState,
269
+ installed: installedState,
270
+ ownership: ownership.result.outcome.result,
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",
279
+ status: preparedState.kind !== "current"
280
+ ? "needs prepare"
281
+ : installedState.kind !== "current"
282
+ ? "needs install"
283
+ : "current",
270
284
  },
271
285
  };
272
286
  }
@@ -319,7 +333,7 @@ export async function runProbe(argv, ctx) {
319
333
  // every unpinned run does: resolveRef's COMMIT_INPUT_RE branch returns
320
334
  // before any runGit call, so a 40-hex requested ref splices nothing.
321
335
  // Pinned by
322
- // `tests/unit/upstream.test.js:460::void test("resolveRef reports a query failure for latest`,
336
+ // `tests/unit/upstream.test.ts:453::void test("resolveRef reports a query failure for latest`,
323
337
  // :471, and :483.
324
338
  // 2. The
325
339
  // `src/selection-store.ts:120-124::cause.module === "selection") {`
@@ -387,6 +401,7 @@ export async function runProbe(argv, ctx) {
387
401
  }
388
402
  return 1;
389
403
  }
390
- ctx.stdout.write(porcelain ? formatPorcelain(outcome.facts) : formatHuman(outcome.facts));
391
- return 0;
404
+ const rendered = ctx.adapter.presentation.renderProbe(outcome.facts);
405
+ ctx.stdout.write(porcelain ? rendered.porcelain : rendered.human);
406
+ return outcome.facts.resourceState === "recovery-required" ? 1 : 0;
392
407
  }