superpowers-manager 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +36 -16
  2. package/dist/atomic.js +1 -3
  3. package/dist/cli-arguments.js +4 -1
  4. package/dist/cli.js +14 -43
  5. package/dist/commands/adapter-call.js +24 -0
  6. package/dist/commands/install.js +16 -63
  7. package/dist/commands/mutation.js +5 -5
  8. package/dist/commands/pin.js +10 -5
  9. package/dist/commands/prepare.js +42 -108
  10. package/dist/commands/probe.js +33 -19
  11. package/dist/commands/uninstall.js +18 -46
  12. package/dist/commands/unpin.js +2 -1
  13. package/dist/commands/update.js +8 -29
  14. package/dist/domain/refs.js +0 -3
  15. package/dist/effective-selection.js +11 -15
  16. package/dist/git-tree.js +4 -6
  17. package/dist/harness-command-result.js +36 -5
  18. package/dist/harnesses/claude-code/harness.js +39 -0
  19. package/dist/harnesses/claude-code/install.js +380 -0
  20. package/dist/harnesses/claude-code/native.js +79 -0
  21. package/dist/harnesses/claude-code/paths.js +35 -0
  22. package/dist/harnesses/claude-code/prepare.js +117 -0
  23. package/dist/harnesses/claude-code/presentation.js +25 -0
  24. package/dist/harnesses/claude-code/state.js +253 -0
  25. package/dist/harnesses/codex/adapter.js +27 -68
  26. package/dist/harnesses/codex/conflicts.js +3 -8
  27. package/dist/harnesses/codex/generated-plugin.js +4 -11
  28. package/dist/harnesses/codex/harness.js +4 -26
  29. package/dist/harnesses/codex/hooks.js +11 -27
  30. package/dist/harnesses/codex/lifecycle.js +22 -107
  31. package/dist/harnesses/codex/manifest-overlay.js +9 -27
  32. package/dist/harnesses/codex/paths.js +29 -0
  33. package/dist/harnesses/codex/prepare.js +9 -43
  34. package/dist/harnesses/codex/presentation.js +1 -1
  35. package/dist/harnesses/codex/publication.js +5 -40
  36. package/dist/harnesses/codex/state.js +1 -6
  37. package/dist/harnesses/codex/stored-state.js +1 -6
  38. package/dist/harnesses/opencode/config.js +105 -50
  39. package/dist/harnesses/opencode/discovery.js +11 -15
  40. package/dist/harnesses/opencode/install.js +57 -102
  41. package/dist/harnesses/opencode/native.js +28 -49
  42. package/dist/harnesses/opencode/package.js +45 -67
  43. package/dist/harnesses/opencode/prepare.js +2 -7
  44. package/dist/harnesses/opencode/presentation.js +3 -7
  45. package/dist/harnesses/opencode/state.js +1 -0
  46. package/dist/harnesses/pi/install.js +31 -120
  47. package/dist/harnesses/pi/native.js +9 -34
  48. package/dist/harnesses/pi/package.js +9 -50
  49. package/dist/harnesses/pi/prepare.js +3 -8
  50. package/dist/harnesses/pi/presentation.js +2 -7
  51. package/dist/harnesses/pi/settings.js +5 -9
  52. package/dist/harnesses/pi/shared-skills.js +23 -53
  53. package/dist/harnesses/pi/state.js +3 -10
  54. package/dist/provenance.js +3 -19
  55. package/dist/resource-lock.js +11 -16
  56. package/dist/safe-path.js +28 -1
  57. package/dist/selection.js +15 -19
  58. package/dist/snapshot-journal.js +101 -0
  59. package/dist/snapshot-package.js +62 -1
  60. package/dist/snapshot-prepare.js +9 -2
  61. package/dist/snapshot-presentation.js +9 -0
  62. package/dist/upstream.js +8 -12
  63. package/dist/validator.js +26 -23
  64. package/dist/workspace.js +29 -35
  65. package/package.json +10 -9
  66. package/dist/python-json.js +0 -4
  67. package/dist/upstream-workspace.js +0 -8
@@ -1,4 +1,4 @@
1
- import { failureResult, successResult, } from "../../adapter-result.js";
1
+ import { successResult, } from "../../adapter-result.js";
2
2
  import { codexInspectControl, codexInspectOwnership, codexInstall, } from "./adapter.js";
3
3
  import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./prepare.js";
4
4
  import { codexPresentation } from "./presentation.js";
@@ -6,17 +6,6 @@ import { codexHome, codexPaths } from "./paths.js";
6
6
  import { installCodexMarketplace, removeCodexMarketplace, } from "./publication.js";
7
7
  import { readCodexRecovery } from "./recovery.js";
8
8
  import { inspectCodexInstallation } from "./state.js";
9
- function preserveFailure(result) {
10
- if (result.outcome.ok) {
11
- throw new Error("cannot preserve a successful adapter result as failure");
12
- }
13
- return { status: result.status, outcome: result.outcome };
14
- }
15
- export function rejectSuccessfulNonzeroStatus(result, message) {
16
- if (!result.outcome.ok || result.status === 0)
17
- return result;
18
- return failureResult(result.outcome.operation, "invalid-status", message, [], result.outcome.messages);
19
- }
20
9
  function requirements(command, env) {
21
10
  if (command !== "probe" &&
22
11
  command !== "install" &&
@@ -38,7 +27,7 @@ async function mutationRoots(ctx) {
38
27
  return [codexHome(ctx.env ?? {}, process.cwd())];
39
28
  }
40
29
  async function inspectCodexUpdateControl(ctx) {
41
- const result = rejectSuccessfulNonzeroStatus(await codexInspectControl(ctx), "adapter reported a failure status for inspect --view update-control");
30
+ const result = await codexInspectControl(ctx);
42
31
  if (!result.outcome.ok)
43
32
  return result;
44
33
  const paths = codexPaths(ctx.env ?? {}, process.cwd());
@@ -74,19 +63,8 @@ export const codexHarness = {
74
63
  inspectOwnership: codexInspectOwnership,
75
64
  inspectUpdateControl: inspectCodexUpdateControl,
76
65
  inspectInstalled: inspectCodexInstallation,
77
- install: async (artifact, ctx) => installCodexMarketplace(artifact, ctx, async (root, context) => {
78
- const result = await codexInstall(root, context);
79
- return rejectSuccessfulNonzeroStatus(result, codexPresentation.callFailure("install", context).invalidStatus);
80
- }),
81
- remove: async (input, ctx) => {
82
- const result = await removeCodexMarketplace(input, ctx);
83
- if (!result.outcome.ok)
84
- return preserveFailure(result);
85
- if (result.status !== 0) {
86
- return rejectSuccessfulNonzeroStatus(result, codexPresentation.callFailure("remove", ctx, input).invalidStatus);
87
- }
88
- return successResult(result.outcome.operation, null, result.outcome.messages);
89
- },
66
+ install: (artifact, ctx) => installCodexMarketplace(artifact, ctx, codexInstall),
67
+ remove: removeCodexMarketplace,
90
68
  requirements,
91
69
  get presentation() {
92
70
  return codexPresentation;
@@ -1,10 +1,10 @@
1
- import { copyFile, cp, mkdir, readdir, readFile, readlink, stat, symlink, } from "node:fs/promises";
1
+ import { copyFile, cp, mkdir, readdir, readFile, readlink, symlink, } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, join, resolve } from "node:path";
3
- import { assertExistingContained, assertProspectiveContained, classifyPathNoFollow, } from "../../safe-path.js";
3
+ import { assertExistingContained, assertProspectiveContained, classifyPathNoFollow, isDirectory, isFile, } from "../../safe-path.js";
4
4
  import { SafetyError } from "../../safety-error.js";
5
5
  import { parseStrictJson } from "../../strict-json.js";
6
6
  // Annotated, not `as const`, matching
7
- // `src/provenance.ts:20::const PROVENANCE_STRICT_PROFILE` and
7
+ // `src/provenance.ts:19::const PROVENANCE_STRICT_PROFILE` and
8
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.
@@ -14,8 +14,8 @@ const MANIFEST_PROFILE = {
14
14
  maxDepth: 256,
15
15
  };
16
16
  // Private helper, matching
17
- // `src/selection.ts:42::return new SafetyError("selection"` and
18
- // `src/upstream.ts:35::function upstreamError`. No new exported error class:
17
+ // `src/selection.ts:35::return new SafetyError("selection"` and
18
+ // `src/upstream.ts:36::function upstreamError`. No new exported error class:
19
19
  // SafetyError already carries module and cause.
20
20
  function hookError(message, cause) {
21
21
  return new SafetyError("hooks", message, { cause });
@@ -37,22 +37,6 @@ async function classifyOwned(path, label) {
37
37
  throw ownedPathError(label, path, cause);
38
38
  }
39
39
  }
40
- async function isRegularFileFollowing(path) {
41
- try {
42
- return (await stat(path)).isFile();
43
- }
44
- catch {
45
- return false;
46
- }
47
- }
48
- async function isDirectoryFollowing(path) {
49
- try {
50
- return (await stat(path)).isDirectory();
51
- }
52
- catch {
53
- return false;
54
- }
55
- }
56
40
  async function requireExistingContained(root, path, label) {
57
41
  try {
58
42
  await assertExistingContained(root, path);
@@ -108,7 +92,7 @@ function isEmptyObject(value) {
108
92
  async function checkedFile(root, relativePath) {
109
93
  const source = resolve(root, relativePath);
110
94
  await requireExistingContained(root, source, "declared hook source");
111
- if (!(await isRegularFileFollowing(source))) {
95
+ if (!(await isFile(source))) {
112
96
  throw hookError(`declared hook source is not a regular file: ${source}`);
113
97
  }
114
98
  return source;
@@ -132,14 +116,14 @@ export async function classifyHooks(manifest, source, upstreamRoot) {
132
116
  const hooksRootPresent = (await classifyOwned(hooksRoot, "hook subtree")) !== "missing";
133
117
  if (!declared) {
134
118
  return {
135
- copyHooksSubtree: await isRegularFileFollowing(defaultConfig),
119
+ copyHooksSubtree: await isFile(defaultConfig),
136
120
  declaredPaths: [],
137
121
  };
138
122
  }
139
123
  const hooks = manifest.hooks;
140
124
  if (isEmptyArray(hooks)) {
141
125
  return {
142
- copyHooksSubtree: await isRegularFileFollowing(defaultConfig),
126
+ copyHooksSubtree: await isFile(defaultConfig),
143
127
  declaredPaths: [],
144
128
  };
145
129
  }
@@ -179,7 +163,7 @@ async function checkedDestination(root, relativePath) {
179
163
  return destination;
180
164
  }
181
165
  await requireExistingContained(root, destination, "declared hook destination");
182
- if (!(await isRegularFileFollowing(destination))) {
166
+ if (!(await isFile(destination))) {
183
167
  throw hookError(`declared hook destination is not a regular file: ${destination}`);
184
168
  }
185
169
  return destination;
@@ -218,7 +202,7 @@ async function validateSubtreeSymlinks(tree, containmentRoot) {
218
202
  catch (cause) {
219
203
  throw hookError(`hook subtree escapes or is broken: ${tree}`, cause);
220
204
  }
221
- if (!(await isDirectoryFollowing(tree))) {
205
+ if (!(await isDirectory(tree))) {
222
206
  throw hookError(`hook subtree is not a directory: ${tree}`);
223
207
  }
224
208
  for (const path of await collectEntries(tree)) {
@@ -252,7 +236,7 @@ async function validateMaterializedDestination(destination, candidateRoot) {
252
236
  // re-owned. Discarding it would erase why containment failed.
253
237
  throw hookError(message, cause);
254
238
  }
255
- if (!(await isRegularFileFollowing(destination))) {
239
+ if (!(await isFile(destination))) {
256
240
  throw hookError(message);
257
241
  }
258
242
  }
@@ -23,120 +23,35 @@ const REPORT_LINES = [
23
23
  "Legacy superpowers-wrapper Codex state remains installed.",
24
24
  "Run: npx superpowers-wrapper@0.1.1 uninstall",
25
25
  ];
26
- function unknownState(identityState) {
27
- return {
28
- kind: "unknown",
29
- message: `unknown adapter identity state: ${identityState}`,
30
- };
31
- }
32
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:43-60::spw_require_no_legacy_state`
33
- export function requireNoLegacyState(identityState) {
34
- if (identityState === "neither" || identityState === "manager") {
35
- return { kind: "ok" };
36
- }
37
- if (identityState === "legacy" || identityState === "both") {
38
- return { kind: "blocked", lines: BLOCKED_LINES };
39
- }
40
- return unknownState(identityState);
41
- }
42
- // Ported from
43
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:72-85::spw_report_legacy_state`.
44
- // Same enumeration, different disposition: this one reports and continues
45
- // rather than blocking, so its clean arm and its legacy arm are both non-fatal.
46
- export function reportLegacyState(identityState) {
47
- if (identityState === "neither" || identityState === "manager") {
48
- return { kind: "ok" };
49
- }
50
- if (identityState === "legacy" || identityState === "both") {
51
- return { kind: "report", lines: REPORT_LINES };
52
- }
53
- return unknownState(identityState);
54
- }
55
- function installDecision(legacy) {
56
- switch (legacy.kind) {
57
- case "ok":
58
- return { kind: "allowed" };
59
- case "blocked":
60
- return {
61
- kind: "blocked",
62
- output: { stdout: [], stderr: legacy.lines },
63
- };
64
- case "unknown":
65
- return {
66
- kind: "blocked",
67
- output: { stdout: [], stderr: [`error: ${legacy.message}`] },
68
- };
69
- case "report":
70
- return {
71
- kind: "blocked",
72
- output: {
73
- stdout: [],
74
- stderr: ["error: unexpected legacy report during installation"],
75
- },
76
- };
77
- }
26
+ function blocked(stderr) {
27
+ return { kind: "blocked", output: { stdout: [], stderr } };
78
28
  }
79
29
  export function codexOwnershipInspection(identityState, removalInput, conflicts) {
30
+ const legacy = identityState === "legacy" || identityState === "both";
80
31
  const { pluginPresent, marketplacePresent } = removalInput;
81
- const legacyEligibility = identityState.length === 0
82
- ? {
83
- kind: "blocked",
84
- output: {
85
- stdout: [],
86
- stderr: ["error: probe did not report adapter identity state"],
87
- },
88
- }
89
- : installDecision(requireNoLegacyState(identityState));
90
- const installEligibility = legacyEligibility.kind === "blocked" || conflicts.length === 0
91
- ? legacyEligibility
92
- : {
93
- kind: "blocked",
94
- output: {
95
- stdout: [],
96
- stderr: [
97
- "Conflicting unmanaged Superpowers Codex resources require manual resolution:",
98
- ...conflicts.map((conflict) => `- ${conflict}`),
99
- "Remove or disable each resource manually, then retry.",
100
- ],
101
- },
102
- };
103
- const legacyReport = reportLegacyState(identityState);
104
- let removalVerification;
105
- if (pluginPresent || marketplacePresent) {
106
- const message = pluginPresent
107
- ? "owned plugin resource is still installed after removal"
108
- : "owned marketplace resource is still registered after removal";
109
- removalVerification = {
110
- kind: "blocked",
111
- output: { stdout: [], stderr: ["error: " + message] },
112
- };
113
- }
114
- else if (legacyReport.kind === "unknown") {
115
- removalVerification = {
116
- kind: "blocked",
117
- output: { stdout: [], stderr: [`error: ${legacyReport.message}`] },
118
- };
119
- }
120
- else if (legacyReport.kind === "blocked") {
121
- removalVerification = {
122
- kind: "blocked",
123
- output: {
124
- stdout: [],
125
- stderr: ["error: unexpected legacy block after removal"],
126
- },
127
- };
128
- }
129
- else {
130
- removalVerification = { kind: "allowed" };
131
- }
132
- const postRemovalOutput = legacyReport.kind === "report"
133
- ? { stdout: legacyReport.lines, stderr: [] }
134
- : { stdout: [], stderr: [] };
32
+ const installEligibility = legacy
33
+ ? blocked(BLOCKED_LINES)
34
+ : conflicts.length > 0
35
+ ? blocked([
36
+ "Conflicting unmanaged Superpowers Codex resources require manual resolution:",
37
+ ...conflicts.map((conflict) => `- ${conflict}`),
38
+ "Remove or disable each resource manually, then retry.",
39
+ ])
40
+ : { kind: "allowed" };
41
+ const removalVerification = pluginPresent || marketplacePresent
42
+ ? blocked([
43
+ pluginPresent
44
+ ? "error: owned plugin resource is still installed after removal"
45
+ : "error: owned marketplace resource is still registered after removal",
46
+ ])
47
+ : { kind: "allowed" };
135
48
  return {
136
49
  installEligibility,
137
50
  removalInput,
138
51
  removalVerification,
139
- postRemovalOutput,
52
+ postRemovalOutput: legacy
53
+ ? { stdout: REPORT_LINES, stderr: [] }
54
+ : { stdout: [], stderr: [] },
140
55
  presentationValue: identityState,
141
56
  presentationConflicts: conflicts,
142
57
  };
@@ -39,29 +39,11 @@ export function applyManifestOverlay(source, version, path) {
39
39
  const entries = parsed.entries.map(([key, value]) => [key, value]);
40
40
  setMember(entries, "version", version);
41
41
  setMember(entries, "skills", "./skills/");
42
- try {
43
- return `${emitObject(entries, 0)}\n`;
44
- }
45
- catch (cause) {
46
- throw rewrapNumberOutOfRange(cause, path);
47
- }
48
- }
49
- // emitNumber has no manifest path, so it throws a bare
50
- // `JSON number out of range: ${raw}`. This rewrap adds the path without
51
- // touching the local emitter's own contract, and is scoped to exactly this
52
- // message so it cannot swallow or reword any other overlay throw.
53
- const NUMBER_OUT_OF_RANGE_PREFIX = "JSON number out of range: ";
54
- function rewrapNumberOutOfRange(cause, path) {
55
- if (cause instanceof SafetyError &&
56
- cause.message.startsWith(NUMBER_OUT_OF_RANGE_PREFIX)) {
57
- const raw = cause.message.slice(NUMBER_OUT_OF_RANGE_PREFIX.length);
58
- return new SafetyError("manifest-overlay", `JSON number out of range in ${path}: ${raw}`, { cause });
59
- }
60
- return cause;
42
+ return `${emitObject(entries, 0, path)}\n`;
61
43
  }
62
- function emitNumber(raw) {
44
+ function emitNumber(raw, path) {
63
45
  if (/[.eE]/.test(raw) && !Number.isFinite(Number(raw))) {
64
- throw new SafetyError("manifest-overlay", `JSON number out of range: ${raw}`);
46
+ throw new SafetyError("manifest-overlay", `JSON number out of range in ${path}: ${raw}`);
65
47
  }
66
48
  return raw;
67
49
  }
@@ -100,7 +82,7 @@ function translate(cause, source, path) {
100
82
  const column = (newlines[newlines.length - 1]?.length ?? 0) + 1;
101
83
  return new SafetyError("manifest-overlay", `invalid manifest JSON in ${path}: line ${line} column ${column}: ${reason}`, { cause });
102
84
  }
103
- function emit(value, depth) {
85
+ function emit(value, depth, path) {
104
86
  if (value === null)
105
87
  return "null";
106
88
  if (typeof value === "boolean")
@@ -110,21 +92,21 @@ function emit(value, depth) {
110
92
  // Order matters: isRawNumber before isRawObject, and both before Array —
111
93
  // the branded records are objects too.
112
94
  if (isRawNumber(value))
113
- return emitNumber(value.source);
95
+ return emitNumber(value.source, path);
114
96
  if (isRawObject(value))
115
- return emitObject(value.entries, depth);
97
+ return emitObject(value.entries, depth, path);
116
98
  const inner = " ".repeat(depth + 1);
117
99
  const outer = " ".repeat(depth);
118
100
  if (value.length === 0)
119
101
  return "[]";
120
- const items = value.map((item) => `${inner}${emit(item, depth + 1)}`);
102
+ const items = value.map((item) => `${inner}${emit(item, depth + 1, path)}`);
121
103
  return `[\n${items.join(",\n")}\n${outer}]`;
122
104
  }
123
- function emitObject(entries, depth) {
105
+ function emitObject(entries, depth, path) {
124
106
  if (entries.length === 0)
125
107
  return "{}";
126
108
  const inner = " ".repeat(depth + 1);
127
109
  const outer = " ".repeat(depth);
128
- const members = entries.map(([key, item]) => `${inner}${escapeNonAscii(JSON.stringify(key))}: ${emit(item, depth + 1)}`);
110
+ const members = entries.map(([key, item]) => `${inner}${escapeNonAscii(JSON.stringify(key))}: ${emit(item, depth + 1, path)}`);
129
111
  return `{\n${members.join(",\n")}\n${outer}}`;
130
112
  }
@@ -51,6 +51,35 @@ async function inspectRoot(path, root) {
51
51
  export function codexPathFailureDetails(cause) {
52
52
  return cause instanceof CodexPathError ? (cause.details ?? null) : null;
53
53
  }
54
+ // One mapping from a preparation-separation failure to the refusal an
55
+ // operation reports. Callers differ only in the code for a non-recovery
56
+ // refusal and in the suffix on the recovery message.
57
+ export function codexPathRefusal(cause, paths, preparedRoot, refusedCode, recoverySuffix) {
58
+ const failure = codexPathFailureDetails(cause);
59
+ if (failure?.kind === "inspection") {
60
+ if (failure.root === "recovery") {
61
+ return {
62
+ code: "recovery-required",
63
+ message: `cannot inspect Codex recovery state at ${paths.recoveryRoot}${recoverySuffix}`,
64
+ };
65
+ }
66
+ return {
67
+ code: refusedCode,
68
+ message: failure.root === "marketplace"
69
+ ? `cannot inspect Codex marketplace storage at ${paths.marketplaceRoot}`
70
+ : `cannot inspect Codex preparation root at ${preparedRoot}`,
71
+ };
72
+ }
73
+ return failure?.kind === "overlap"
74
+ ? {
75
+ code: "preparation-overlap",
76
+ message: `Codex preparation overlaps Codex published or recovery storage: ${preparedRoot}`,
77
+ }
78
+ : {
79
+ code: refusedCode,
80
+ message: `cannot validate Codex preparation storage separation at ${preparedRoot}`,
81
+ };
82
+ }
54
83
  export async function assertCodexPreparationSeparate(preparedRoot, paths) {
55
84
  const recovery = await inspectRoot(paths.recoveryRoot, "recovery");
56
85
  const marketplace = await inspectRoot(paths.marketplaceRoot, "marketplace");
@@ -6,10 +6,10 @@ import { ARTIFACT_RECEIPT } from "../../artifact-tree.js";
6
6
  import { assessCodexCompatibility, readCodexAssessment, writeCodexAssessment, } from "./compatibility.js";
7
7
  import { readManifest } from "./hooks.js";
8
8
  import { readGeneratedCommitLenient, readStrictProvenanceField, writeProvenance, } from "../../provenance.js";
9
- import { classifyPathNoFollow } from "../../safe-path.js";
9
+ import { classifyPathNoFollow, isFile } from "../../safe-path.js";
10
10
  import { SafetyError } from "../../safety-error.js";
11
11
  import { manifestVersionForRef } from "../../upstream-version.js";
12
- import { assertCodexPreparationSeparate, codexPathFailureDetails, codexPaths, } from "./paths.js";
12
+ import { assertCodexPreparationSeparate, codexPathRefusal, codexPaths, } from "./paths.js";
13
13
  import { readCodexRecovery } from "./recovery.js";
14
14
  // Order is inherited from the original prepare command; the first miss wins.
15
15
  const REQUIRED_UPSTREAM = [
@@ -25,12 +25,6 @@ const COPY_PATHS = [
25
25
  "README.md",
26
26
  "CODE_OF_CONDUCT.md",
27
27
  ];
28
- const RESOLUTION_KINDS = [
29
- "latest-release",
30
- "tag",
31
- "ref",
32
- "raw-commit",
33
- ];
34
28
  function unassessedCompatibility() {
35
29
  return {
36
30
  kind: "unknown",
@@ -55,14 +49,6 @@ async function pathExists(path) {
55
49
  return false;
56
50
  }
57
51
  }
58
- async function regularFileExists(path) {
59
- try {
60
- return (await stat(path)).isFile();
61
- }
62
- catch {
63
- return false;
64
- }
65
- }
66
52
  async function owned(message, fn) {
67
53
  try {
68
54
  return await fn();
@@ -102,18 +88,8 @@ export async function validateCodexPreparationBeforeFetch(ctx) {
102
88
  await assertCodexPreparationSeparate(preparedRoot, paths);
103
89
  }
104
90
  catch (cause) {
105
- const failure = codexPathFailureDetails(cause);
106
- if (failure?.kind === "inspection") {
107
- if (failure.root === "recovery") {
108
- return failureResult("prepare", "recovery-required", `cannot inspect Codex recovery state at ${paths.recoveryRoot}`, [], []);
109
- }
110
- return failureResult("prepare", "prepare-failed", failure.root === "marketplace"
111
- ? `cannot inspect Codex marketplace storage at ${paths.marketplaceRoot}`
112
- : `cannot inspect Codex preparation root at ${preparedRoot}`, [], []);
113
- }
114
- return failureResult("prepare", failure?.kind === "overlap" ? "preparation-overlap" : "prepare-failed", failure?.kind === "overlap"
115
- ? `Codex preparation overlaps Codex published or recovery storage: ${preparedRoot}`
116
- : `cannot validate Codex preparation storage separation at ${preparedRoot}`, [], []);
91
+ const refusal = codexPathRefusal(cause, paths, preparedRoot, "prepare-failed", "");
92
+ return failureResult("prepare", refusal.code, refusal.message, [], []);
117
93
  }
118
94
  try {
119
95
  if ((await readCodexRecovery(paths)) !== null) {
@@ -124,7 +100,7 @@ export async function validateCodexPreparationBeforeFetch(ctx) {
124
100
  return failureResult("prepare", "recovery-required", `cannot inspect Codex recovery state at ${paths.recoveryRoot}`, [], []);
125
101
  }
126
102
  const template = manifestTemplate(ctx);
127
- if (!(await regularFileExists(template))) {
103
+ if (!(await isFile(template))) {
128
104
  return failureResult("prepare", "missing-template", `missing fallback manifest template: ${template}`, [], []);
129
105
  }
130
106
  return successResult("prepare", null, []);
@@ -139,13 +115,6 @@ export async function readUpstreamManifestVersion(path) {
139
115
  }
140
116
  return value;
141
117
  }
142
- function asResolutionKind(value) {
143
- for (const kind of RESOLUTION_KINDS) {
144
- if (kind === value)
145
- return kind;
146
- }
147
- throw prepareError(`unknown upstream resolution kind: ${value}`);
148
- }
149
118
  export async function prepareCodexCandidate(input, ctx) {
150
119
  let upstreamManifestVersion = "";
151
120
  let managerVersion = "";
@@ -161,7 +130,7 @@ export async function prepareCodexCandidate(input, ctx) {
161
130
  await copyPathIfPresent(join(input.upstreamRoot, name), join(input.candidateRoot, name));
162
131
  }
163
132
  const upstreamManifest = join(input.upstreamRoot, ".codex-plugin", "plugin.json");
164
- if (await regularFileExists(upstreamManifest)) {
133
+ if (await isFile(upstreamManifest)) {
165
134
  upstreamManifestVersion =
166
135
  await readUpstreamManifestVersion(upstreamManifest);
167
136
  }
@@ -174,13 +143,13 @@ export async function prepareCodexCandidate(input, ctx) {
174
143
  });
175
144
  managerVersion = manifestVersionForRef({
176
145
  requestedRef: input.selection.requestedRef,
177
- resolutionKind: asResolutionKind(input.selection.resolutionKind),
146
+ resolutionKind: input.selection.resolutionKind,
178
147
  resolvedRef: input.selection.resolvedRef,
179
148
  commit: input.selection.desiredCommit,
180
149
  });
181
150
  }
182
151
  catch (cause) {
183
- // owned()/asResolutionKind(), readManifest(), and writeProvenance replace
152
+ // owned(), readManifest(), and writeProvenance replace
184
153
  // every subordinate failure with controlled preparation, hook, or
185
154
  // provenance text. Re-emitting those owned diagnostics is therefore safe.
186
155
  // codexBuild stays below this catch because it deliberately rethrows
@@ -203,13 +172,10 @@ export async function prepareCodexCandidate(input, ctx) {
203
172
  if (!built.outcome.ok) {
204
173
  return { status: built.status, outcome: built.outcome };
205
174
  }
206
- if (built.status !== 0) {
207
- return failureResult("build", "invalid-status", "adapter reported failure without an error outcome", [], built.outcome.messages);
208
- }
209
175
  const compatibility = await assessCodexCompatibility(input.upstreamRoot, input.selection);
210
176
  if (compatibility.kind === "supported") {
211
177
  try {
212
- await writeCodexAssessment(input.candidateRoot, input.selection, (await regularFileExists(join(input.upstreamRoot, ".codex-plugin/plugin.json")))
178
+ await writeCodexAssessment(input.candidateRoot, input.selection, (await isFile(join(input.upstreamRoot, ".codex-plugin/plugin.json")))
213
179
  ? "upstream"
214
180
  : "fallback");
215
181
  }
@@ -90,7 +90,7 @@ function verificationOutput(kind, hint) {
90
90
  return { stdout: [], stderr };
91
91
  }
92
92
  function safeHint(value) {
93
- return typeof value === "string" && !hasTerminalControl(value) ? value : "";
93
+ return hasTerminalControl(value) ? "" : value;
94
94
  }
95
95
  export function codexInstallReceipt(missingHint, mismatchHint) {
96
96
  return {
@@ -3,17 +3,16 @@ import { failureResult, successResult, } from "../../adapter-result.js";
3
3
  import { beginDirectoryPublication, } from "../../atomic.js";
4
4
  import { digestArtifactTree } from "../../artifact-tree.js";
5
5
  import { assertNoFollowType, classifyPathNoFollow } from "../../safe-path.js";
6
- import { codexInspectControl, codexInspectOwnership, codexInstallRefreshMode, codexRemove, codexReadNativeState, } from "./adapter.js";
6
+ import { codexInspectOwnership, codexInstallRefreshMode, codexRemove, codexReadNativeState, } from "./adapter.js";
7
7
  import { readCodexAssessment } from "./compatibility.js";
8
8
  import { readCodexMarketplace, stageCodexMarketplace, } from "./marketplace.js";
9
- import { assertCodexPreparationSeparate, codexPathFailureDetails, codexPaths, } from "./paths.js";
9
+ import { assertCodexPreparationSeparate, codexPathRefusal, codexPaths, } from "./paths.js";
10
10
  import { codexPreparationLocation } from "./prepare.js";
11
11
  import { beginCodexRecovery, finishCodexRecovery, readCodexRecovery, verifyCodexRecovery, } from "./recovery.js";
12
12
  import { hasFilesystemAccessFailure, pathsEqual } from "./state.js";
13
13
  const DEFAULTS = {
14
14
  readNative: codexReadNativeState,
15
15
  inspectOwnership: codexInspectOwnership,
16
- inspectControl: codexInspectControl,
17
16
  beginPublication: beginDirectoryPublication,
18
17
  };
19
18
  const REMOVAL_DEFAULTS = {
@@ -30,18 +29,7 @@ function acceptedNative(result) {
30
29
  if (result.status !== 0 || !result.outcome.ok) {
31
30
  throw new Error("Codex native observation failed");
32
31
  }
33
- const value = result.outcome.result;
34
- if (typeof value.marketplaceRoot !== "string" &&
35
- value.marketplaceRoot !== null) {
36
- throw new Error("Codex native observation is malformed");
37
- }
38
- if (typeof value.pluginPresent !== "boolean" ||
39
- typeof value.pluginEnabled !== "boolean" ||
40
- (typeof value.activeVersion !== "string" && value.activeVersion !== null) ||
41
- (typeof value.activeRoot !== "string" && value.activeRoot !== null)) {
42
- throw new Error("Codex native observation is malformed");
43
- }
44
- return value;
32
+ return result.outcome.result;
45
33
  }
46
34
  function requireOwnership(result) {
47
35
  if (result.status !== 0 || !result.outcome.ok) {
@@ -51,14 +39,6 @@ function requireOwnership(result) {
51
39
  throw new Error("Codex ownership blocks publication");
52
40
  }
53
41
  }
54
- function requireControl(result) {
55
- if (result.status !== 0 || !result.outcome.ok) {
56
- throw new Error("Codex inspection failed");
57
- }
58
- if (result.outcome.result?.mutationEligibility?.kind !== "allowed") {
59
- throw new Error("Codex update control blocks publication");
60
- }
61
- }
62
42
  function sameNative(left, right) {
63
43
  return (left.marketplaceRoot === right.marketplaceRoot &&
64
44
  left.pluginPresent === right.pluginPresent &&
@@ -247,9 +227,6 @@ async function requireEligibility(ctx, dependencies, messages) {
247
227
  const ownership = await dependencies.inspectOwnership(ctx);
248
228
  messages.push(...ownership.outcome.messages);
249
229
  requireOwnership(ownership);
250
- const control = await dependencies.inspectControl(ctx);
251
- messages.push(...control.outcome.messages);
252
- requireControl(control);
253
230
  }
254
231
  async function verifyPrevious(pending, previous) {
255
232
  const current = await readCodexMarketplace(pending.paths.marketplaceRoot);
@@ -388,20 +365,8 @@ export async function installCodexMarketplace(artifact, ctx, activate, dependenc
388
365
  await assertCodexPreparationSeparate(preparedRoot, paths);
389
366
  }
390
367
  catch (cause) {
391
- const failure = codexPathFailureDetails(cause);
392
- if (failure?.kind === "inspection") {
393
- if (failure.root === "recovery") {
394
- return fail("recovery-required", `cannot inspect Codex recovery state at ${paths.recoveryRoot}; recovery is required before mutation`, messages);
395
- }
396
- return fail("activation-refused", failure.root === "marketplace"
397
- ? `cannot inspect Codex marketplace storage at ${paths.marketplaceRoot}`
398
- : `cannot inspect Codex preparation root at ${preparedRoot}`, messages);
399
- }
400
- return fail(failure?.kind === "overlap"
401
- ? "preparation-overlap"
402
- : "activation-refused", failure?.kind === "overlap"
403
- ? `Codex preparation overlaps Codex published or recovery storage: ${preparedRoot}`
404
- : `cannot validate Codex preparation storage separation at ${preparedRoot}`, messages);
368
+ const refusal = codexPathRefusal(cause, paths, preparedRoot, "activation-refused", "; recovery is required before mutation");
369
+ return fail(refusal.code, refusal.message, messages);
405
370
  }
406
371
  let pending;
407
372
  let publication;
@@ -83,11 +83,6 @@ async function comparablePath(path) {
83
83
  export async function pathsEqual(a, b) {
84
84
  return (await comparablePath(a)) === (await comparablePath(b));
85
85
  }
86
- function preserveFailure(result) {
87
- if (result.outcome.ok)
88
- throw new Error("expected adapter failure");
89
- return failureResult(result.outcome.operation, result.outcome.error.code, result.outcome.error.message, result.outcome.error.hints, result.outcome.messages);
90
- }
91
86
  function mismatch(reason, messages = [], observedIdentity = "") {
92
87
  return successResult("inspect-codex-installed", { kind: "mismatch", observedIdentity }, [
93
88
  ...messages,
@@ -116,7 +111,7 @@ export async function inspectCodexInstallation(selection, ctx, readNative = code
116
111
  const operation = "inspect-codex-installed";
117
112
  const native = await readNative(ctx);
118
113
  if (!native.outcome.ok)
119
- return preserveFailure(native);
114
+ return { status: native.status, outcome: native.outcome };
120
115
  const messages = native.outcome.messages;
121
116
  const paths = codexPaths(ctx.env ?? {}, process.cwd());
122
117
  let marketplace;
@@ -3,6 +3,7 @@ import { lstat, open, readdir } from "node:fs/promises";
3
3
  import { isAbsolute, join } from "node:path";
4
4
  import { parse, TomlDate } from "smol-toml";
5
5
  import { SafetyError } from "../../safety-error.js";
6
+ import { isErrno } from "../../safe-path.js";
6
7
  import { codexHome } from "./paths.js";
7
8
  const CONFIG_LIMIT = 1024 * 1024;
8
9
  const CONFIG_DEPTH_LIMIT = 64;
@@ -15,12 +16,6 @@ const MARKETPLACE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
15
16
  function storedError(message, cause) {
16
17
  return new SafetyError("codex-stored-state", message, { cause });
17
18
  }
18
- function isErrno(cause, code) {
19
- return (cause !== null &&
20
- typeof cause === "object" &&
21
- "code" in cause &&
22
- cause.code === code);
23
- }
24
19
  function table(value, name) {
25
20
  if (value === undefined)
26
21
  return {};