superpowers-manager 0.6.0 → 0.7.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 (75) hide show
  1. package/README.md +42 -26
  2. package/dist/adapter-result.js +5 -9
  3. package/dist/atomic.js +4 -14
  4. package/dist/cli-arguments.js +1 -27
  5. package/dist/cli.js +25 -40
  6. package/dist/commands/adapter-call.js +28 -0
  7. package/dist/commands/install.js +25 -105
  8. package/dist/commands/pin.js +1 -1
  9. package/dist/commands/prepare.js +37 -81
  10. package/dist/commands/probe.js +35 -262
  11. package/dist/commands/track-latest.js +1 -1
  12. package/dist/commands/uninstall.js +15 -91
  13. package/dist/commands/update.js +5 -25
  14. package/dist/effective-selection.js +4 -6
  15. package/dist/git-tree.js +85 -0
  16. package/dist/harness-command-result.js +47 -0
  17. package/dist/harnesses/codex/adapter.js +12 -41
  18. package/dist/harnesses/codex/conflicts.js +2 -3
  19. package/dist/harnesses/codex/hooks.js +1 -1
  20. package/dist/harnesses/codex/json.js +1 -11
  21. package/dist/harnesses/codex/lifecycle.js +0 -48
  22. package/dist/harnesses/codex/manifest-overlay.js +1 -1
  23. package/dist/harnesses/codex/paths.js +14 -29
  24. package/dist/harnesses/codex/presentation.js +2 -23
  25. package/dist/harnesses/codex/publication.js +0 -6
  26. package/dist/harnesses/codex/recovery.js +5 -5
  27. package/dist/harnesses/codex/stored-state.js +1 -1
  28. package/dist/harnesses/opencode/config.js +256 -0
  29. package/dist/harnesses/opencode/discovery.js +587 -0
  30. package/dist/harnesses/opencode/harness.js +37 -0
  31. package/dist/harnesses/opencode/install.js +601 -0
  32. package/dist/harnesses/opencode/native.js +57 -0
  33. package/dist/harnesses/opencode/package.js +89 -0
  34. package/dist/harnesses/opencode/paths.js +38 -0
  35. package/dist/harnesses/opencode/prepare.js +30 -0
  36. package/dist/harnesses/opencode/presentation.js +28 -0
  37. package/dist/harnesses/opencode/state.js +174 -0
  38. package/dist/harnesses/pi/compatibility.js +5 -37
  39. package/dist/harnesses/pi/install.js +7 -5
  40. package/dist/harnesses/pi/native.js +3 -47
  41. package/dist/harnesses/pi/package.js +5 -107
  42. package/dist/harnesses/pi/prepare.js +13 -104
  43. package/dist/harnesses/pi/presentation.js +23 -66
  44. package/dist/harnesses/pi/shared-skills.js +28 -39
  45. package/dist/harnesses/pi/state.js +7 -27
  46. package/dist/provenance.js +2 -2
  47. package/dist/safe-path.js +2 -2
  48. package/dist/selection-store.js +2 -22
  49. package/dist/snapshot-package.js +78 -0
  50. package/dist/snapshot-prepare.js +99 -0
  51. package/dist/snapshot-presentation.js +44 -0
  52. package/dist/validator.js +4 -1
  53. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  54. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  55. package/node_modules/jsonc-parser/README.md +364 -0
  56. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  57. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  58. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  59. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  60. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  61. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  62. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  63. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  64. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  65. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  66. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  67. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  68. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  69. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  70. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  71. package/node_modules/jsonc-parser/package.json +37 -0
  72. package/package.json +8 -4
  73. package/dist/domain/fingerprint.js +0 -4
  74. package/dist/status.js +0 -24
  75. package/dist/validate-generated-plugin-cli.js +0 -118
@@ -1,72 +1,21 @@
1
- // Ports scripts/uninstall. The shell sourced common.sh, provenance.sh,
2
- // lifecycle.sh and adapter.sh; the predicates now live in src/harnesses/codex/lifecycle.ts
3
- // and the adapter arrives through ctx.adapter.
4
1
  import { tmpdir } from "node:os";
5
2
  import { oneLine } from "../cli-arguments.js";
6
3
  import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
4
+ import { GatherFailure, callAdapter, } from "./adapter-call.js";
7
5
  import { runWithMutation } from "./mutation.js";
8
6
  import { replayOutcome } from "./probe.js";
9
7
  async function invoke(call, failure, outcomes) {
10
- let result;
11
- try {
12
- result = await call();
13
- }
14
- catch {
15
- return { ok: false, message: failure.unexpected };
16
- }
17
- outcomes.push(result.outcome);
18
- const outcome = result.outcome;
19
- if (result.status !== 0 || !outcome.ok) {
20
- return {
21
- ok: false,
22
- message: outcome.ok ? failure.invalidStatus : null,
23
- };
24
- }
25
- return {
26
- ok: true,
27
- result: { status: result.status, outcome },
28
- };
29
- }
30
- // withWorkspace throws for mkdtemp failure before the callback ever runs
31
- // ("cannot create workspace",
32
- // `src/workspace.ts:120::throw new SafetyError("workspace", "cannot create workspace"`).
33
- // A bare re-throw would silently drop every outcome collected before that
34
- // point -- a narrow
35
- // DIAG-ADAPTER-01 regression the shell never had, since it replayed each
36
- // adapter response as it went rather than batching replay to the end. This
37
- // carries the outcomes collected so far alongside the original cause, so
38
- // runUninstall's catch can still replay them before reporting the cause.
39
- //
40
- // The post-success cleanup failure no longer reaches here: onCleanupFailure
41
- // below suppresses withWorkspace's throw for that case and records the
42
- // warning as data, so the computed UninstallOutcome survives it. The
43
- // outcome-carrying is still load-bearing for mkdtemp, and the shape stays
44
- // identical to src/commands/install.ts's GatherFailure.
45
- class GatherFailure extends Error {
46
- inner;
47
- outcomes;
48
- constructor(inner, outcomes) {
49
- super("uninstall gather failed");
50
- this.inner = inner;
51
- this.outcomes = outcomes;
52
- }
8
+ const result = await callAdapter(call, failure);
9
+ if (result.result !== null)
10
+ outcomes.push(result.result.outcome);
11
+ return result;
53
12
  }
54
- // Every step that can throw or fail closed, returning the outcome as data and
55
- // performing NO writes. Same shape as gatherProbe
56
- // (`src/commands/probe.ts::readonly facts: ProbeSnapshot<R>;`) and for the same
57
- // reason: a write inside this try could raise EPIPE, be caught here, and be
58
- // relabelled as a domain failure.
13
+ // Collect outcomes without writing so output failures cannot be classified as
14
+ // inspection failures. Replay collected outcomes after gathering completes.
59
15
  async function gatherUninstall(ctx) {
60
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/uninstall:20-21::TMPDIR=` exported TMPDIR="$uninstall_workspace" so every
61
- // child confined its temporary files to the tree the workspace trap
62
- // removed. The AdapterContext passed to ctx.adapter below does the same.
63
16
  const parent = ctx.env.TMPDIR ?? tmpdir();
64
- // Declared OUTSIDE the withWorkspace callback (rather than inside, as an
65
- // earlier draft had it) so the catch below -- which wraps the ENTIRE
66
- // withWorkspace call, not just the callback -- can still see whatever was
67
- // collected before a workspace throw. Both `mkdtemp` failure (nothing
68
- // collected yet) and a post-success cleanup failure (everything the
69
- // callback collected) reach this same array.
17
+ // Kept outside the workspace callback so a workspace failure preserves every
18
+ // outcome collected before it.
70
19
  const outcomes = [];
71
20
  let cleanupWarning = null;
72
21
  try {
@@ -90,9 +39,8 @@ async function gatherUninstall(ctx) {
90
39
  const removed = await invoke(() => ctx.adapter.remove(removalInput, adapterContext), removeFailure, outcomes);
91
40
  if (!removed.ok)
92
41
  return failed(removed.message);
93
- // Stage 3: inspect ownership AGAIN. This overwrites the first
94
- // inspection (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/uninstall:29-30::spw_verify_uninstalled_resources`), so everything below reads the
95
- // POST-uninstall state, not the pre-uninstall one read above.
42
+ // Stage 3: inspect ownership again. Everything below reads this
43
+ // post-removal state, not the inspection before removal.
96
44
  const afterFailure = ctx.adapter.presentation.callFailure("post-remove", adapterContext);
97
45
  const after = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), afterFailure, outcomes);
98
46
  if (!after.ok)
@@ -131,7 +79,7 @@ async function gatherUninstall(ctx) {
131
79
  // cannot reach here. Wrapping with `outcomes` anyway keeps the class
132
80
  // total over its declared contract rather than assuming the callback's
133
81
  // purity at the throw site.
134
- throw new GatherFailure(cause, outcomes);
82
+ throw new GatherFailure("uninstall gather failed", cause, outcomes);
135
83
  }
136
84
  }
137
85
  export async function runUninstall(argv, ctx) {
@@ -147,30 +95,8 @@ async function performUninstall(argv, ctx) {
147
95
  run = await gatherUninstall(ctx);
148
96
  }
149
97
  catch (cause) {
150
- // gatherUninstall throws exactly one shape: GatherFailure, wrapping
151
- // withWorkspace's "cannot create workspace" SafetyError
152
- // (`src/workspace.ts:120::throw new SafetyError("workspace", "cannot create workspace"`),
153
- // alongside whatever outcomes were collected before that throw -- none,
154
- // for that cause. Replaying first, before
155
- // reporting the cause, keeps the arm honest for any outcome-bearing
156
- // throw the class is declared to carry (DIAG-ADAPTER-01). The
157
- // post-success cleanup failure no longer arrives here: gatherUninstall's
158
- // onCleanupFailure records it as `cleanupWarning` and the computed
159
- // outcome survives, which is what `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/uninstall:34-35::complete` did. The
160
- // `instanceof` guard is defensive rather than load-bearing --
161
- // gatherUninstall's own catch is the only thing that can throw here, and
162
- // it always wraps -- but this catch does not assume that invariant
163
- // blindly.
164
- //
165
- // A cause outside ctx.adapter's AdapterFailure guard
166
- // (`src/harnesses/codex/adapter.ts:829::async function runCodexOperation<T = JsonValue>(`) does NOT
167
- // reach here: invoke() catches it inside gatherUninstall and converts it
168
- // to a hand-written message carried as UninstallOutcome data, exactly as
169
- // src/commands/probe.ts's inspect() does for the same cause.
170
- //
171
- // gatherUninstall performs no writes of its own, so this catch cannot
172
- // also be reached by an EPIPE from uninstall's own output -- every write
173
- // below runs only after this try/catch has resolved.
98
+ // Replay collected outcomes before reporting a gather failure. The
99
+ // cleanup reporter retains post-success cleanup failures as data.
174
100
  const outcomes = cause instanceof GatherFailure ? cause.outcomes : [];
175
101
  for (const outcome of outcomes)
176
102
  replayOutcome(outcome, ctx);
@@ -179,9 +105,7 @@ async function performUninstall(argv, ctx) {
179
105
  return 1;
180
106
  }
181
107
  const { outcome, cleanupWarning } = run;
182
- // Replay first, on both paths: the shell validator replayed every
183
- // response's messages whether or not that response was a failure
184
- // (`git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/validate-adapter-response.py:268::replay(messages)`).
108
+ // Replay before reporting a command-level result on either path.
185
109
  for (const each of outcome.outcomes)
186
110
  replayOutcome(each, ctx);
187
111
  let status;
@@ -1,21 +1,10 @@
1
- // Ports scripts/update. The shell sourced common.sh and lifecycle.sh; the
2
- // predicates now live in src/harnesses/codex/lifecycle.ts. Unlike install and uninstall,
3
- // update issues no ctx.adapter call of its own: every adapter interaction
4
- // this module needs already happens inside gatherProbe, runPrepare or
5
- // runInstall, each of which owns its own §4.2a-conformant invoke() gate. That
6
- // is also why update opens no temporary workspace of its own -- there is no
7
- // mutation here for one to protect.
1
+ // Update delegates adapter calls to probe, prepare, and install. Those
2
+ // mutating commands own their workspaces, so update opens none itself.
8
3
  import { oneLine } from "../cli-arguments.js";
4
+ import { runInstall, writeOutput } from "./install.js";
9
5
  import { gatherProbe, replayOutcome } from "./probe.js";
10
- import { runInstall } from "./install.js";
11
6
  import { runPrepare } from "./prepare.js";
12
7
  import { runWithMutation } from "./mutation.js";
13
- function writeOutput(output, ctx) {
14
- for (const line of output.stdout)
15
- ctx.stdout.write(`${line}\n`);
16
- for (const line of output.stderr)
17
- ctx.stderr.write(`${line}\n`);
18
- }
19
8
  export async function runUpdate(argv, ctx) {
20
9
  return await runWithMutation("update", ctx, async (scoped) => performUpdate(argv, scoped));
21
10
  }
@@ -28,17 +17,8 @@ async function performUpdate(argv, ctx) {
28
17
  probe = await gatherProbe(ctx);
29
18
  }
30
19
  catch (cause) {
31
- // gatherProbe performs no writes of its own (src/commands/probe.ts), so
32
- // this catch cannot also be reached by an EPIPE from update's own
33
- // output -- update writes nothing before this call resolves, unlike
34
- // install's NOTE line.
35
- //
36
- // This is a THIRD consumer of gatherProbe's throw channel --
37
- // `src/commands/probe.ts:469-527::THREE exceptions, all inherited and none a regression:`'s
38
- // runProbe catch is the first and src/commands/install.ts's runInstall catch
39
- // is the second. Because all three wrap the identical function, runProbe's
40
- // long comment there enumerates exactly what can reach this stream too; not
41
- // repeated here.
20
+ // Gathering does not write, so this catch cannot misclassify an output
21
+ // failure. oneLine() bounds any inherited diagnostic to one line.
42
22
  ctx.stderr.write(`error: ${oneLine(cause)}\n`);
43
23
  return 1;
44
24
  }
@@ -43,10 +43,9 @@ export async function loadSavedSelection(env) {
43
43
  export const UPSTREAM_URL_DEFAULT = "https://github.com/obra/superpowers";
44
44
  // Ports the env > saved > package-default precedence ladder from
45
45
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/selection.sh:88-162::spw_compute_effective_selection`.
46
- // validateSource runs before any ref resolution -- pinned by
47
- // tests/unit/effective-selection.test.js's "source validation precedes ref
48
- // resolution": a credential-bearing source must fail before Git is ever
49
- // invoked.
46
+ // validateSource runs before any ref resolution. A credential-bearing source
47
+ // must fail before Git is invoked; the resolver-log check lives in
48
+ // `tests/baseline/selection-location.test.ts::SEL-PRECEDENCE-VALIDATE-01 invalid saved state stops resolution`.
50
49
  export async function computeEffectiveSelection(root, env) {
51
50
  const saved = await loadSavedSelection(env);
52
51
  let upstreamSourceOrigin;
@@ -63,8 +62,7 @@ export async function computeEffectiveSelection(root, env) {
63
62
  upstreamSourceOrigin = "package-default";
64
63
  effectiveSource = UPSTREAM_URL_DEFAULT;
65
64
  }
66
- // Before any Git access. Pinned by tests/unit/effective-selection.test.js's
67
- // "source validation precedes ref resolution".
65
+ // Before any Git access; the selection-location test above checks the log.
68
66
  validateSource(effectiveSource);
69
67
  let selectionOrigin;
70
68
  let selectionMode;
@@ -0,0 +1,85 @@
1
+ import { execFile } from "node:child_process";
2
+ import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { ARTIFACT_RECEIPT } from "./artifact-tree.js";
6
+ import { COMMIT_RE } from "./domain/refs.js";
7
+ import { assertNoFollowType, assertSymlinkTargetContained, } from "./safe-path.js";
8
+ import { SafetyError } from "./safety-error.js";
9
+ const exec = promisify(execFile);
10
+ const DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
11
+ async function gitBytes(repository, args) {
12
+ try {
13
+ return (await exec("git", ["--no-replace-objects", "-C", repository, ...args], {
14
+ encoding: null,
15
+ maxBuffer: 256 * 1024 * 1024,
16
+ })).stdout;
17
+ }
18
+ catch (cause) {
19
+ throw new SafetyError("git-tree", `cannot read Git tree objects from repository: ${repository}`, { cause });
20
+ }
21
+ }
22
+ export async function materializeGitTree(repository, commit, destination) {
23
+ try {
24
+ if (!COMMIT_RE.test(commit))
25
+ throw new Error("commit");
26
+ if (!(await gitBytes(repository, ["cat-file", "-t", commit])).equals(Buffer.from("commit\n")))
27
+ throw new Error("not a commit");
28
+ await assertNoFollowType(destination, ["missing"]);
29
+ const bytes = await gitBytes(repository, [
30
+ "ls-tree",
31
+ "-rz",
32
+ "--full-tree",
33
+ commit,
34
+ ]);
35
+ const entries = [];
36
+ let offset = 0;
37
+ while (offset < bytes.length) {
38
+ const end = bytes.indexOf(0, offset);
39
+ if (end < 0)
40
+ throw new Error("tree framing");
41
+ const record = DECODER.decode(bytes.subarray(offset, end));
42
+ offset = end + 1;
43
+ const match = /^(100644|100755|120000) blob ([a-f0-9]{40})\t([\s\S]+)$/.exec(record);
44
+ if (!match)
45
+ throw new Error("tree entry");
46
+ const path = match[3];
47
+ if (path
48
+ .split("/")
49
+ .some((part) => !part ||
50
+ part === "." ||
51
+ part === ".." ||
52
+ part.toLowerCase() === ".git") ||
53
+ isAbsolute(path) ||
54
+ path.includes("\\") ||
55
+ path.split("/")[0] === ARTIFACT_RECEIPT)
56
+ throw new Error("tree path");
57
+ entries.push({ mode: match[1], oid: match[2], path });
58
+ }
59
+ await mkdir(destination, { recursive: true });
60
+ for (const entry of entries)
61
+ await mkdir(dirname(join(destination, entry.path)), { recursive: true });
62
+ for (const entry of entries.filter((entry) => entry.mode !== "120000")) {
63
+ const path = join(destination, entry.path), mode = entry.mode === "100755" ? 0o755 : 0o644;
64
+ await writeFile(path, await gitBytes(repository, ["cat-file", "blob", entry.oid]), { flag: "wx", mode });
65
+ await chmod(path, mode);
66
+ }
67
+ for (const entry of entries.filter((entry) => entry.mode === "120000")) {
68
+ const target = DECODER.decode(await gitBytes(repository, ["cat-file", "blob", entry.oid]));
69
+ const path = join(destination, entry.path), suffix = relative(resolve(destination), resolve(dirname(path), target));
70
+ if (!target ||
71
+ target.includes("\0") ||
72
+ isAbsolute(target) ||
73
+ suffix === ".." ||
74
+ suffix.startsWith("../") ||
75
+ isAbsolute(suffix))
76
+ throw new Error("link escape");
77
+ await symlink(target, path);
78
+ }
79
+ for (const entry of entries.filter((entry) => entry.mode === "120000"))
80
+ await assertSymlinkTargetContained(destination, join(destination, entry.path));
81
+ }
82
+ catch (cause) {
83
+ throw new SafetyError("git-tree", `cannot materialize Git tree from ${repository} into ${destination}`, { cause });
84
+ }
85
+ }
@@ -0,0 +1,47 @@
1
+ import { AdapterMessageLog, failureResult, successResult, } from "./adapter-result.js";
2
+ import { SEMVER_RE } from "./domain/refs.js";
3
+ function appendCaptured(log, label, channel, captured) {
4
+ log.appendBytes(channel, Buffer.from(captured.text));
5
+ if (captured.droppedBytes > 0)
6
+ log.appendText(channel, `${label} ${channel} omitted ${captured.droppedBytes} bytes at the capture limit`);
7
+ }
8
+ export function nativeCommandResult(label, result) {
9
+ const operation = `${label.toLowerCase()}-command`;
10
+ if (result.kind === "launchFailed") {
11
+ const errno = /^[A-Z][A-Z0-9_]*$/u.test(result.errno)
12
+ ? result.errno
13
+ : "UNKNOWN";
14
+ return failureResult(operation, "launch-failed", `cannot launch configured ${label} executable (${errno})`, [], []);
15
+ }
16
+ const log = new AdapterMessageLog();
17
+ if (result.kind === "timedOut") {
18
+ appendCaptured(log, label, "stdout", result.stdout);
19
+ appendCaptured(log, label, "stderr", result.stderr);
20
+ return failureResult(operation, "timeout", `${label} command timed out after ${result.afterMs} ms`, [], log.snapshot());
21
+ }
22
+ if (result.code !== 0) {
23
+ appendCaptured(log, label, "stdout", result.stdout);
24
+ appendCaptured(log, label, "stderr", result.stderr);
25
+ return failureResult(operation, "nonzero-exit", result.code === null
26
+ ? `${label} command exited without a status`
27
+ : `${label} command exited with status ${result.code}`, [], log.snapshot());
28
+ }
29
+ if (result.stdout.droppedBytes > 0 || result.stderr.droppedBytes > 0) {
30
+ appendCaptured(log, label, "stdout", result.stdout);
31
+ appendCaptured(log, label, "stderr", result.stderr);
32
+ return failureResult(operation, "output-limit", `${label} command output exceeded the capture limit`, [], log.snapshot());
33
+ }
34
+ appendCaptured(log, label, "stderr", result.stderr);
35
+ return successResult(operation, { stdout: result.stdout.text }, log.snapshot());
36
+ }
37
+ export function normalizeSnapshotRuntimeVersion(label, result) {
38
+ if (!result.outcome.ok)
39
+ return { status: result.status, outcome: result.outcome };
40
+ const operation = `${label.toLowerCase()}-runtime`;
41
+ if (result.status !== 0)
42
+ return failureResult(operation, "invalid-status", `${label} runtime inspection returned a nonzero status`, [], result.outcome.messages);
43
+ const version = result.outcome.result.stdout.trim();
44
+ if (!SEMVER_RE.test(version))
45
+ return failureResult(operation, "invalid-version", `${label} runtime inspection returned an invalid version response`, [], result.outcome.messages);
46
+ return successResult(operation, version, result.outcome.messages);
47
+ }
@@ -14,8 +14,7 @@ import { validateGeneratedPlugin } from "./generated-plugin.js";
14
14
  import { classifyHooks, materializeHooks, readManifest, } from "./hooks.js";
15
15
  import { applyManifestOverlay } from "./manifest-overlay.js";
16
16
  import { readCodexBuildSource } from "../../provenance.js";
17
- import { isAcceptedSplitValue } from "../../validate-generated-plugin-cli.js";
18
- import { codexControlInspection, codexOwnershipInspection, } from "./lifecycle.js";
17
+ import { codexOwnershipInspection } from "./lifecycle.js";
19
18
  import { codexInstallReceipt } from "./presentation.js";
20
19
  const PLUGIN_ID = CODEX_MANAGER_PLUGIN_ID;
21
20
  const MARKETPLACE_NAME = "superpowers-manager";
@@ -282,39 +281,6 @@ async function runBuild(input, env, log) {
282
281
  catch {
283
282
  fail("invalid-provenance", "candidate provenance is missing or invalid");
284
283
  }
285
- // The seven values the validator CLI would receive in split form:
286
- // --plugin-root, --requested-ref, --resolved-ref, --commit,
287
- // --manifest-version, --manifest-source, --upstream-manifest-version.
288
- // The eighth, --source, is passed attached, where argparse accepts any
289
- // dash-leading value, so it is deliberately absent here. Each value is
290
- // paired with the ADAPTER-facing flag name to report: --manager-version
291
- // (the CLI calls it --manifest-version) and --plugin-root /
292
- // --manifest-source (derived, not user-supplied) deliberately differ
293
- // from the validator CLI's own names, since the operator can only act
294
- // on the adapter's surface.
295
- const splitValues = [
296
- { value: candidateRoot, name: "--plugin-root" },
297
- { value: input.requestedRef, name: "--requested-ref" },
298
- { value: input.resolvedRef, name: "--resolved-ref" },
299
- { value: input.commit, name: "--commit" },
300
- { value: input.managerVersion, name: "--manager-version" },
301
- { value: manifestSource, name: "--manifest-source" },
302
- {
303
- value: input.upstreamManifestVersion,
304
- name: "--upstream-manifest-version",
305
- },
306
- ];
307
- const firstRejected = splitValues.find(({ value }) => !isAcceptedSplitValue(value));
308
- if (firstRejected !== undefined) {
309
- // Declared exception to message-record parity: argparse wrote usage
310
- // records here; this guard precedes the call and writes a
311
- // differently-worded record naming the rejected flag instead. The
312
- // failure code and message are unchanged.
313
- const text = "Generated plugin validation failed:\n" +
314
- `- validator argument \`${firstRejected.name}\` has a dash-leading value the argument parser rejects\n`;
315
- log.appendBytes("stderr", Buffer.from(text, "utf8"));
316
- fail("generated-plugin-validation-failed", "built-in generated plugin validation failed");
317
- }
318
284
  let errors;
319
285
  try {
320
286
  errors = await validateGeneratedPlugin({
@@ -447,16 +413,17 @@ async function runOwnership(context, env, log) {
447
413
  const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
448
414
  if (commandFailed(plugins)) {
449
415
  const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
450
- const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedListingJson);
416
+ const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
451
417
  return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
452
418
  }
453
419
  let managerPlugin;
454
420
  let legacyPlugin;
455
421
  let conflicts;
456
422
  try {
457
- managerPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", PLUGIN_ID);
458
- legacyPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", LEGACY_PLUGIN_ID);
459
- conflicts = await inspectCodexConflicts({ root: context.root, env }, plugins.stdout.toString("utf8"));
423
+ const installed = codexInstalledPluginsFromJson(plugins.stdout);
424
+ managerPlugin = installed.some((plugin) => plugin.pluginId === PLUGIN_ID);
425
+ legacyPlugin = installed.some((plugin) => plugin.pluginId === LEGACY_PLUGIN_ID);
426
+ conflicts = await inspectCodexConflicts({ root: context.root, env }, installed);
460
427
  }
461
428
  catch {
462
429
  fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
@@ -464,7 +431,7 @@ async function runOwnership(context, env, log) {
464
431
  const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
465
432
  if (commandFailed(marketplaces)) {
466
433
  const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
467
- const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedListingJson);
434
+ const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
468
435
  return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
469
436
  }
470
437
  let managerMarketplace;
@@ -505,7 +472,11 @@ export function codexInspectOwnership(context) {
505
472
  return runCodexOperation("inspect", context, (env, log) => runOwnership(context, env, log));
506
473
  }
507
474
  export function codexInspectControl(context) {
508
- return runCodexOperation("inspect", context, async () => codexControlInspection("managed"));
475
+ return runCodexOperation("inspect", context, async () => ({
476
+ probeEligibility: { kind: "allowed" },
477
+ mutationEligibility: { kind: "allowed" },
478
+ presentationValue: "managed",
479
+ }));
509
480
  }
510
481
  export function codexReadNativeState(context) {
511
482
  return runCodexOperation("native-state", context, async (env, log) => {
@@ -1,6 +1,5 @@
1
1
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { codexInstalledPluginsFromJson, } from "./json.js";
4
3
  export const CODEX_MANAGER_PLUGIN_ID = "superpowers@superpowers-manager";
5
4
  export const CODEX_LEGACY_PLUGIN_ID = "superpowers@superpowers-wrapper";
6
5
  const SUPERPOWERS_PLUGIN_PREFIX = "superpowers@";
@@ -85,9 +84,9 @@ async function nativeRouteConflict(ctx) {
85
84
  }
86
85
  return NATIVE_ROUTE_CONFLICT;
87
86
  }
88
- export async function inspectCodexConflicts(ctx, installedListing) {
87
+ export async function inspectCodexConflicts(ctx, installedPlugins) {
89
88
  const conflicts = new Set();
90
- for (const plugin of codexInstalledPluginsFromJson(installedListing)) {
89
+ for (const plugin of installedPlugins) {
91
90
  const conflict = unmanagedPluginConflict(plugin);
92
91
  if (conflict.length > 0)
93
92
  conflicts.add(conflict);
@@ -5,7 +5,7 @@ import { SafetyError } from "../../safety-error.js";
5
5
  import { parseStrictJson } from "../../strict-json.js";
6
6
  // Annotated, not `as const`, matching
7
7
  // `src/provenance.ts:20::const PROVENANCE_STRICT_PROFILE` and
8
- // `src/selection-store.ts:19::const SELECTION_JSON_PROFILE`. The annotation
8
+ // `src/selection-store.ts:15::const SELECTION_JSON_PROFILE`. The annotation
9
9
  // rejects a misspelled or unsupported profile field here rather than at the
10
10
  // parseStrictJson call.
11
11
  const MANIFEST_PROFILE = {
@@ -1,4 +1,5 @@
1
1
  import { SafetyError } from "../../safety-error.js";
2
+ import { hasTerminalControl } from "../../adapter-result.js";
2
3
  import { parseStrictJson, } from "../../strict-json.js";
3
4
  const ACCEPT_CONSTANTS = {
4
5
  duplicateKeys: "last-wins",
@@ -45,17 +46,6 @@ function checkedItems(raw, profile, arrayKey, field) {
45
46
  return candidate;
46
47
  });
47
48
  }
48
- function hasTerminalControl(value) {
49
- for (const character of value) {
50
- const code = character.codePointAt(0);
51
- if (code < 0x20 ||
52
- (code >= 0x7f && code <= 0x9f) ||
53
- (code >= 0xd800 && code <= 0xdfff)) {
54
- return true;
55
- }
56
- }
57
- return false;
58
- }
59
49
  export function installedListingHas(raw, arrayKey, field, value) {
60
50
  return checkedItems(raw, ACCEPT_CONSTANTS, arrayKey, field).some((item) => item[field] === value);
61
51
  }
@@ -52,24 +52,6 @@ export function reportLegacyState(identityState) {
52
52
  }
53
53
  return unknownState(identityState);
54
54
  }
55
- // Ported from
56
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:62-70::spw_require_managed_update_control`.
57
- // Both refusals reached spw_die in the shell, so both carry the `error: ` prefix
58
- // at the call site and neither is special.
59
- export function requireManagedUpdateControl(value) {
60
- if (value === "managed")
61
- return { ok: true };
62
- if (value === "unsupported") {
63
- return {
64
- ok: false,
65
- message: "adapter cannot guarantee manager-controlled updates",
66
- };
67
- }
68
- return {
69
- ok: false,
70
- message: `unknown adapter update-control capability: ${value}`,
71
- };
72
- }
73
55
  function installDecision(legacy) {
74
56
  switch (legacy.kind) {
75
57
  case "ok":
@@ -159,33 +141,3 @@ export function codexOwnershipInspection(identityState, removalInput, conflicts)
159
141
  presentationConflicts: conflicts,
160
142
  };
161
143
  }
162
- export function codexControlInspection(updateControl) {
163
- const probeEligibility = updateControl.length === 0
164
- ? {
165
- kind: "blocked",
166
- output: {
167
- stdout: [],
168
- stderr: [
169
- "error: probe did not report adapter update-control capability",
170
- ],
171
- },
172
- }
173
- : { kind: "allowed" };
174
- const managed = requireManagedUpdateControl(updateControl);
175
- const mutationEligibility = probeEligibility.kind === "blocked"
176
- ? probeEligibility
177
- : managed.ok
178
- ? { kind: "allowed" }
179
- : {
180
- kind: "blocked",
181
- output: {
182
- stdout: [],
183
- stderr: [`error: ${managed.message}`],
184
- },
185
- };
186
- return {
187
- probeEligibility,
188
- mutationEligibility,
189
- presentationValue: updateControl,
190
- };
191
- }
@@ -74,7 +74,7 @@ function setMember(entries, key, value) {
74
74
  }
75
75
  /**
76
76
  * Map strict-json's diagnostics onto the CPython wording the callers assert.
77
- * Repository policy at `AGENTS.md:92::most diagnostics are asserted as`
77
+ * Repository policy at `AGENTS.md:89::most diagnostics are asserted as`
78
78
  * freezes most of these diagnostics as complete-string contracts.
79
79
  */
80
80
  function translate(cause, source, path) {
@@ -1,5 +1,5 @@
1
- import { isAbsolute, join, relative, resolve, sep } from "node:path";
2
- import { assertNoFollowType, canonicalizeProspectivePath, } from "../../safe-path.js";
1
+ import { join, resolve } from "node:path";
2
+ import { assertNoFollowType, canonicalizeProspectivePath, isContained, } from "../../safe-path.js";
3
3
  import { SafetyError } from "../../safety-error.js";
4
4
  export function codexHome(env, cwd) {
5
5
  const configured = env.CODEX_HOME;
@@ -26,15 +26,18 @@ export function codexPaths(env, cwd) {
26
26
  };
27
27
  }
28
28
  function overlaps(a, b) {
29
- const inside = (root, leaf) => {
30
- const suffix = relative(root, leaf);
31
- return (suffix === "" ||
32
- (!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`)));
33
- };
34
- return inside(a, b) || inside(b, a);
29
+ return isContained(a, b) || isContained(b, a);
30
+ }
31
+ class CodexPathError extends SafetyError {
32
+ constructor(message, details, cause) {
33
+ super("codex-paths", message, {
34
+ details,
35
+ ...(cause === undefined ? {} : { cause }),
36
+ });
37
+ }
35
38
  }
36
39
  function separationError() {
37
- return new SafetyError("codex-paths", "preparation overlaps Codex published or recovery storage", { details: { kind: "overlap" } });
40
+ return new CodexPathError("preparation overlaps Codex published or recovery storage", { kind: "overlap" });
38
41
  }
39
42
  async function inspectRoot(path, root) {
40
43
  try {
@@ -42,29 +45,11 @@ async function inspectRoot(path, root) {
42
45
  return await canonicalizeProspectivePath(path);
43
46
  }
44
47
  catch (cause) {
45
- throw new SafetyError("codex-paths", `cannot inspect ${root} root: ${path}`, { cause, details: { kind: "inspection", root, path } });
48
+ throw new CodexPathError(`cannot inspect ${root} root: ${path}`, { kind: "inspection", root, path }, cause);
46
49
  }
47
50
  }
48
51
  export function codexPathFailureDetails(cause) {
49
- if (!(cause instanceof SafetyError) || cause.module !== "codex-paths") {
50
- return null;
51
- }
52
- const details = cause.details;
53
- if (details === null || typeof details !== "object" || !("kind" in details)) {
54
- return null;
55
- }
56
- if (details.kind === "overlap")
57
- return { kind: "overlap" };
58
- if (details.kind !== "inspection" ||
59
- !("root" in details) ||
60
- !("path" in details) ||
61
- (details.root !== "preparation" &&
62
- details.root !== "marketplace" &&
63
- details.root !== "recovery") ||
64
- typeof details.path !== "string") {
65
- return null;
66
- }
67
- return { kind: "inspection", root: details.root, path: details.path };
52
+ return cause instanceof CodexPathError ? (cause.details ?? null) : null;
68
53
  }
69
54
  export async function assertCodexPreparationSeparate(preparedRoot, paths) {
70
55
  const recovery = await inspectRoot(paths.recoveryRoot, "recovery");