superpowers-manager 0.5.1 → 0.6.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 (51) hide show
  1. package/README.md +13 -11
  2. package/dist/cli.js +9 -14
  3. package/dist/commands/install.js +18 -9
  4. package/dist/commands/mutation.js +7 -0
  5. package/dist/commands/prepare.js +5 -51
  6. package/dist/commands/probe.js +8 -10
  7. package/dist/commands/uninstall.js +2 -2
  8. package/dist/effective-selection.js +3 -3
  9. package/dist/harnesses/codex/adapter.js +336 -457
  10. package/dist/harnesses/codex/compatibility.js +2 -17
  11. package/dist/harnesses/codex/generated-plugin.js +1 -1
  12. package/dist/harnesses/codex/harness.js +45 -272
  13. package/dist/harnesses/codex/hooks.js +1 -1
  14. package/dist/harnesses/codex/json.js +6 -2
  15. package/dist/harnesses/codex/lifecycle.js +110 -54
  16. package/dist/harnesses/codex/manifest-overlay.js +22 -17
  17. package/dist/harnesses/codex/marketplace.js +148 -0
  18. package/dist/harnesses/codex/paths.js +97 -0
  19. package/dist/harnesses/codex/prepare.js +34 -2
  20. package/dist/harnesses/codex/presentation.js +10 -17
  21. package/dist/harnesses/codex/publication.js +520 -0
  22. package/dist/harnesses/codex/recovery.js +338 -0
  23. package/dist/harnesses/codex/state.js +131 -0
  24. package/dist/harnesses/codex/stored-state.js +249 -0
  25. package/dist/provenance.js +3 -3
  26. package/dist/python-json-format.js +0 -69
  27. package/dist/python-json.js +2 -27
  28. package/dist/python-text.js +9 -69
  29. package/dist/validator.js +35 -72
  30. package/node_modules/smol-toml/LICENSE +24 -0
  31. package/node_modules/smol-toml/README.md +418 -0
  32. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  33. package/node_modules/smol-toml/dist/date.js +127 -0
  34. package/node_modules/smol-toml/dist/error.d.ts +38 -0
  35. package/node_modules/smol-toml/dist/error.js +63 -0
  36. package/node_modules/smol-toml/dist/extract.js +69 -0
  37. package/node_modules/smol-toml/dist/index.cjs +734 -0
  38. package/node_modules/smol-toml/dist/index.d.ts +43 -0
  39. package/node_modules/smol-toml/dist/index.js +33 -0
  40. package/node_modules/smol-toml/dist/parse.d.ts +36 -0
  41. package/node_modules/smol-toml/dist/parse.js +149 -0
  42. package/node_modules/smol-toml/dist/primitive.js +238 -0
  43. package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
  44. package/node_modules/smol-toml/dist/stringify.js +181 -0
  45. package/node_modules/smol-toml/dist/struct.js +179 -0
  46. package/node_modules/smol-toml/dist/util.d.ts +38 -0
  47. package/node_modules/smol-toml/dist/util.js +89 -0
  48. package/node_modules/smol-toml/package.json +68 -0
  49. package/package.json +10 -2
  50. package/dist/selection-state-cli.js +0 -85
  51. package/dist/upstream-cli.js +0 -110
@@ -61,24 +61,9 @@ export async function readCodexAssessment(root) {
61
61
  });
62
62
  if (errors.length)
63
63
  throw new Error("generated resources");
64
- const selection = {
64
+ const compatibility = await assessCodexCompatibility(root, {
65
65
  effectiveSource: receipt.source,
66
- desiredCommit: receipt.commit,
67
- requestedRef: provenance.requested_ref,
68
- resolvedRef: provenance.resolved_ref,
69
- resolutionKind: "raw-commit",
70
- selectionOrigin: "package-default",
71
- selectionMode: "default",
72
- upstreamSourceOrigin: "package-default",
73
- saved: {
74
- saved_mode: "none",
75
- saved_source: "",
76
- saved_requested_ref: "",
77
- saved_resolved_ref: "",
78
- saved_commit: "",
79
- },
80
- };
81
- const compatibility = await assessCodexCompatibility(root, selection);
66
+ });
82
67
  return {
83
68
  root,
84
69
  commit: receipt.commit,
@@ -39,7 +39,7 @@ function decodePathBytes(bytes) {
39
39
  * syscall, so a string carrying one would open a different file than the
40
40
  * caller named. CPython raises `UnicodeEncodeError` instead.
41
41
  */
42
- export function hasUnpairedSurrogate(value) {
42
+ function hasUnpairedSurrogate(value) {
43
43
  for (let index = 0; index < value.length; index += 1) {
44
44
  const unit = value.charCodeAt(index);
45
45
  if (unit >= 0xd800 && unit <= 0xdbff) {
@@ -1,268 +1,22 @@
1
- import { join, resolve } from "node:path";
2
- import { failureResult, hasTerminalControl, successResult, } from "../../adapter-result.js";
3
- import { codexInspect, codexInstall, codexRemove, } from "./adapter.js";
1
+ import { failureResult, successResult, } from "../../adapter-result.js";
2
+ import { codexInspectControl, codexInspectOwnership, codexInstall, } from "./adapter.js";
4
3
  import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./prepare.js";
5
- import { codexInstallReceipt, codexPresentation } from "./presentation.js";
6
- import { reportLegacyState, requireManagedUpdateControl, requireNoLegacyState, verifyUninstalledResources, } from "./lifecycle.js";
7
- import { SafetyError } from "../../safety-error.js";
8
- import { commitMatches } from "../../status.js";
4
+ import { codexPresentation } from "./presentation.js";
5
+ import { codexHome, codexPaths } from "./paths.js";
6
+ import { installCodexMarketplace, removeCodexMarketplace, } from "./publication.js";
7
+ import { readCodexRecovery } from "./recovery.js";
8
+ import { inspectCodexInstallation } from "./state.js";
9
9
  function preserveFailure(result) {
10
10
  if (result.outcome.ok) {
11
11
  throw new Error("cannot preserve a successful adapter result as failure");
12
12
  }
13
13
  return { status: result.status, outcome: result.outcome };
14
14
  }
15
- function malformed(result, message) {
16
- return failureResult(result.outcome.operation, "malformed-result", message, [], result.outcome.messages);
17
- }
18
- function invalidStatus(result, message) {
15
+ export function rejectSuccessfulNonzeroStatus(result, message) {
16
+ if (!result.outcome.ok || result.status === 0)
17
+ return result;
19
18
  return failureResult(result.outcome.operation, "invalid-status", message, [], result.outcome.messages);
20
19
  }
21
- function resultRecord(result) {
22
- const value = result.outcome.ok ? result.outcome.result : null;
23
- return typeof value === "object" && value !== null && !Array.isArray(value)
24
- ? value
25
- : null;
26
- }
27
- function stringField(result, key) {
28
- const raw = result[key];
29
- if (raw === null || raw === undefined)
30
- return { ok: true, value: "" };
31
- if (typeof raw === "string")
32
- return { ok: true, value: raw };
33
- return { ok: false };
34
- }
35
- function resourceFlag(result, key) {
36
- const resources = result.resources;
37
- const bag = typeof resources === "object" &&
38
- resources !== null &&
39
- !Array.isArray(resources)
40
- ? resources
41
- : {};
42
- const value = bag[key];
43
- return typeof value === "boolean" ? value : null;
44
- }
45
- function conflictDescriptions(result) {
46
- const raw = result.conflicts;
47
- if (raw === null || raw === undefined)
48
- return [];
49
- if (!Array.isArray(raw))
50
- return null;
51
- const descriptions = [];
52
- for (const item of raw) {
53
- if (typeof item !== "string" ||
54
- item.length === 0 ||
55
- hasTerminalControl(item)) {
56
- return null;
57
- }
58
- descriptions.push(item);
59
- }
60
- return descriptions;
61
- }
62
- function installDecision(legacy) {
63
- switch (legacy.kind) {
64
- case "ok":
65
- return { kind: "allowed" };
66
- case "blocked":
67
- return {
68
- kind: "blocked",
69
- output: { stdout: [], stderr: legacy.lines },
70
- };
71
- case "unknown":
72
- return {
73
- kind: "blocked",
74
- output: { stdout: [], stderr: [`error: ${legacy.message}`] },
75
- };
76
- case "report":
77
- return {
78
- kind: "blocked",
79
- output: {
80
- stdout: [],
81
- stderr: ["error: unexpected legacy report during installation"],
82
- },
83
- };
84
- }
85
- }
86
- export function normalizeCodexOwnership(result) {
87
- if (!result.outcome.ok)
88
- return preserveFailure(result);
89
- if (result.status !== 0) {
90
- return invalidStatus(result, "adapter reported a failure status for inspect --view ownership");
91
- }
92
- const record = resultRecord(result);
93
- if (record === null) {
94
- return malformed(result, "adapter returned a non-object result for inspect --view ownership");
95
- }
96
- const pluginPresent = resourceFlag(record, "plugin");
97
- if (pluginPresent === null) {
98
- return malformed(result, "expected a Boolean adapter result at resources.plugin");
99
- }
100
- const marketplacePresent = resourceFlag(record, "marketplace");
101
- if (marketplacePresent === null) {
102
- return malformed(result, "expected a Boolean adapter result at resources.marketplace");
103
- }
104
- const identity = stringField(record, "identity_state");
105
- if (!identity.ok) {
106
- return malformed(result, "adapter returned a non-string identity_state for inspect --view ownership");
107
- }
108
- const identityState = identity.value;
109
- const conflicts = conflictDescriptions(record);
110
- if (conflicts === null) {
111
- return malformed(result, "expected an array of strings at conflicts");
112
- }
113
- const legacyEligibility = identityState.length === 0
114
- ? {
115
- kind: "blocked",
116
- output: {
117
- stdout: [],
118
- stderr: ["error: probe did not report adapter identity state"],
119
- },
120
- }
121
- : installDecision(requireNoLegacyState(identityState));
122
- const installEligibility = legacyEligibility.kind === "blocked" || conflicts.length === 0
123
- ? legacyEligibility
124
- : {
125
- kind: "blocked",
126
- output: {
127
- stdout: [],
128
- stderr: [
129
- "Conflicting unmanaged Superpowers Codex resources require manual resolution:",
130
- ...conflicts.map((conflict) => `- ${conflict}`),
131
- "Remove or disable each resource manually, then retry.",
132
- ],
133
- },
134
- };
135
- const verification = verifyUninstalledResources(result);
136
- const legacyReport = reportLegacyState(identityState);
137
- let removalVerification;
138
- if (!verification.ok) {
139
- removalVerification = {
140
- kind: "blocked",
141
- output: { stdout: [], stderr: [`error: ${verification.message}`] },
142
- };
143
- }
144
- else if (legacyReport.kind === "unknown") {
145
- removalVerification = {
146
- kind: "blocked",
147
- output: { stdout: [], stderr: [`error: ${legacyReport.message}`] },
148
- };
149
- }
150
- else if (legacyReport.kind === "blocked") {
151
- removalVerification = {
152
- kind: "blocked",
153
- output: {
154
- stdout: [],
155
- stderr: ["error: unexpected legacy block after removal"],
156
- },
157
- };
158
- }
159
- else {
160
- removalVerification = { kind: "allowed" };
161
- }
162
- const postRemovalOutput = legacyReport.kind === "report"
163
- ? { stdout: legacyReport.lines, stderr: [] }
164
- : { stdout: [], stderr: [] };
165
- return successResult(result.outcome.operation, {
166
- installEligibility,
167
- removalInput: { pluginPresent, marketplacePresent },
168
- removalVerification,
169
- postRemovalOutput,
170
- presentationValue: identityState,
171
- presentationConflicts: conflicts,
172
- }, result.outcome.messages);
173
- }
174
- export function normalizeCodexControl(result) {
175
- if (!result.outcome.ok)
176
- return preserveFailure(result);
177
- if (result.status !== 0) {
178
- return invalidStatus(result, "adapter reported a failure status for inspect --view update-control");
179
- }
180
- const record = resultRecord(result);
181
- if (record === null) {
182
- return malformed(result, "adapter returned a non-object result for inspect --view update-control");
183
- }
184
- const control = stringField(record, "update_control");
185
- if (!control.ok) {
186
- return malformed(result, "adapter returned a non-string update_control for inspect --view update-control");
187
- }
188
- const updateControl = control.value;
189
- const probeEligibility = updateControl.length === 0
190
- ? {
191
- kind: "blocked",
192
- output: {
193
- stdout: [],
194
- stderr: [
195
- "error: probe did not report adapter update-control capability",
196
- ],
197
- },
198
- }
199
- : { kind: "allowed" };
200
- const managed = requireManagedUpdateControl(updateControl);
201
- const mutationEligibility = probeEligibility.kind === "blocked"
202
- ? probeEligibility
203
- : managed.ok
204
- ? { kind: "allowed" }
205
- : {
206
- kind: "blocked",
207
- output: {
208
- stdout: [],
209
- stderr: [`error: ${managed.message}`],
210
- },
211
- };
212
- return successResult(result.outcome.operation, { probeEligibility, mutationEligibility, presentationValue: updateControl }, result.outcome.messages);
213
- }
214
- export function normalizeCodexInstalled(result, desiredCommit) {
215
- if (!result.outcome.ok)
216
- return preserveFailure(result);
217
- if (result.status !== 0) {
218
- return invalidStatus(result, "adapter reported a failure status for inspect --view fingerprint");
219
- }
220
- const record = resultRecord(result);
221
- if (record === null) {
222
- return malformed(result, "adapter returned a non-object result for inspect --view fingerprint");
223
- }
224
- const fingerprint = stringField(record, "fingerprint");
225
- if (!fingerprint.ok) {
226
- return malformed(result, "adapter returned a non-string fingerprint for inspect --view fingerprint");
227
- }
228
- const observedIdentity = fingerprint.value;
229
- const installed = observedIdentity.length === 0
230
- ? { kind: "absent", observedIdentity: "" }
231
- : commitMatches(desiredCommit, observedIdentity)
232
- ? { kind: "current", observedIdentity }
233
- : { kind: "mismatch", observedIdentity };
234
- return successResult(result.outcome.operation, installed, result.outcome.messages);
235
- }
236
- function safeHint(value) {
237
- return typeof value === "string" && !hasTerminalControl(value) ? value : "";
238
- }
239
- export function normalizeCodexInstall(result) {
240
- if (!result.outcome.ok)
241
- return preserveFailure(result);
242
- if (result.status !== 0) {
243
- return invalidStatus(result, "adapter reported a failure status for install");
244
- }
245
- const record = resultRecord(result);
246
- if (record === null) {
247
- return malformed(result, "adapter returned a non-object result for install");
248
- }
249
- const hints = record.verification_hints;
250
- const bag = typeof hints === "object" && hints !== null && !Array.isArray(hints)
251
- ? hints
252
- : {};
253
- return successResult(result.outcome.operation, codexInstallReceipt(safeHint(bag.missing), safeHint(bag.mismatch)), result.outcome.messages);
254
- }
255
- // The canonical payload normalizer has no context parameter. Keep the native
256
- // call's defensive success/nonzero-status guard beside the concrete adapter,
257
- // where the exact legacy diagnostic can still name ctx.root.
258
- export function normalizeCodexInstallForContext(result, ctx) {
259
- if (!result.outcome.ok)
260
- return preserveFailure(result);
261
- if (result.status !== 0) {
262
- return invalidStatus(result, codexPresentation.callFailure("install", ctx).invalidStatus);
263
- }
264
- return normalizeCodexInstall(result);
265
- }
266
20
  function requirements(command, env) {
267
21
  if (command !== "probe" &&
268
22
  command !== "install" &&
@@ -281,16 +35,34 @@ function requirements(command, env) {
281
35
  ];
282
36
  }
283
37
  async function mutationRoots(ctx) {
284
- const env = ctx.env ?? {};
285
- const configured = env.CODEX_HOME;
286
- if (configured !== undefined && configured.length > 0) {
287
- return [resolve(configured)];
38
+ return [codexHome(ctx.env ?? {}, process.cwd())];
39
+ }
40
+ async function inspectCodexUpdateControl(ctx) {
41
+ const result = rejectSuccessfulNonzeroStatus(await codexInspectControl(ctx), "adapter reported a failure status for inspect --view update-control");
42
+ if (!result.outcome.ok)
43
+ return result;
44
+ const paths = codexPaths(ctx.env ?? {}, process.cwd());
45
+ let diagnostic = null;
46
+ try {
47
+ if ((await readCodexRecovery(paths)) !== null) {
48
+ diagnostic = `Codex recovery required; preserve material at ${paths.recoveryRoot} for manual resolution`;
49
+ }
288
50
  }
289
- const home = env.HOME;
290
- if (home === undefined || home.length === 0) {
291
- throw new SafetyError("codex-harness", "cannot determine Codex state root without HOME");
51
+ catch {
52
+ diagnostic = `cannot inspect Codex recovery state at ${paths.recoveryRoot}`;
292
53
  }
293
- return [join(resolve(home), ".codex")];
54
+ if (diagnostic === null)
55
+ return result;
56
+ const decision = {
57
+ kind: "blocked",
58
+ output: { stdout: [], stderr: [`error: ${diagnostic}`] },
59
+ };
60
+ return successResult(result.outcome.operation, {
61
+ probeEligibility: decision,
62
+ mutationEligibility: decision,
63
+ presentationValue: diagnostic,
64
+ recoveryState: "required",
65
+ }, result.outcome.messages);
294
66
  }
295
67
  export const codexHarness = {
296
68
  preparationLocation: codexPreparationLocation,
@@ -299,18 +71,19 @@ export const codexHarness = {
299
71
  prepareCandidate: prepareCodexCandidate,
300
72
  inspectPrepared: inspectCodexPrepared,
301
73
  readPrepared: readCodexPrepared,
302
- inspectOwnership: async (ctx) => normalizeCodexOwnership(await codexInspect("ownership", ctx)),
303
- inspectUpdateControl: async (ctx) => normalizeCodexControl(await codexInspect("update-control", ctx)),
304
- inspectInstalled: async (selection, ctx) => normalizeCodexInstalled(await codexInspect("fingerprint", ctx), selection.desiredCommit),
305
- install: async (_artifact, ctx) => {
306
- return normalizeCodexInstallForContext(await codexInstall(ctx.root, ctx), ctx);
307
- },
74
+ inspectOwnership: codexInspectOwnership,
75
+ inspectUpdateControl: inspectCodexUpdateControl,
76
+ 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
+ }),
308
81
  remove: async (input, ctx) => {
309
- const result = await codexRemove(input, ctx);
82
+ const result = await removeCodexMarketplace(input, ctx);
310
83
  if (!result.outcome.ok)
311
84
  return preserveFailure(result);
312
85
  if (result.status !== 0) {
313
- return invalidStatus(result, codexPresentation.callFailure("remove", ctx, input).invalidStatus);
86
+ return rejectSuccessfulNonzeroStatus(result, codexPresentation.callFailure("remove", ctx, input).invalidStatus);
314
87
  }
315
88
  return successResult(result.outcome.operation, null, result.outcome.messages);
316
89
  },
@@ -4,7 +4,7 @@ import { assertExistingContained, assertProspectiveContained, classifyPathNoFoll
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::export const PROVENANCE_STRICT_PROFILE` and
7
+ // `src/provenance.ts:20::const PROVENANCE_STRICT_PROFILE` and
8
8
  // `src/selection-store.ts:19::const SELECTION_JSON_PROFILE`. The annotation
9
9
  // rejects a misspelled or unsupported profile field here rather than at the
10
10
  // parseStrictJson call.
@@ -66,9 +66,13 @@ export function codexInstalledPluginsFromJson(raw) {
66
66
  enabled: typeof item.enabled === "boolean" ? item.enabled : null,
67
67
  }));
68
68
  }
69
- export function marketplaceRootFromJson(raw, marketplaceName) {
69
+ export function marketplaceRootFromJson(raw, marketplaceName, requireUnique = false) {
70
70
  const items = checkedItems(raw, ACCEPT_CONSTANTS, "marketplaces", "name");
71
- const match = items.find((item) => item.name === marketplaceName);
71
+ const matches = items.filter((item) => item.name === marketplaceName);
72
+ if (requireUnique && matches.length > 1) {
73
+ fail("marketplace appears more than once");
74
+ }
75
+ const match = matches[0];
72
76
  if (match === undefined)
73
77
  return "";
74
78
  if (typeof match.root !== "string" || match.root.length === 0) {
@@ -52,7 +52,6 @@ export function reportLegacyState(identityState) {
52
52
  }
53
53
  return unknownState(identityState);
54
54
  }
55
- export { verifyInstalledFingerprint, } from "./presentation.js";
56
55
  // Ported from
57
56
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:62-70::spw_require_managed_update_control`.
58
57
  // Both refusals reached spw_die in the shell, so both carry the `error: ` prefix
@@ -71,65 +70,122 @@ export function requireManagedUpdateControl(value) {
71
70
  message: `unknown adapter update-control capability: ${value}`,
72
71
  };
73
72
  }
74
- function readResult(adapterResult) {
75
- if (adapterResult.status !== 0)
76
- return { kind: "call-failed" };
77
- const outcome = adapterResult.outcome;
78
- if (!outcome.ok)
79
- return { kind: "call-failed" };
80
- const value = outcome.result;
81
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
82
- return { kind: "unusable" };
83
- }
84
- return { kind: "object", value: value };
85
- }
86
- // Ported from
87
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:126-141::spw_verify_uninstalled_resources`,
88
- // with the Boolean coercion of
89
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/adapter.sh:58-73::spw_adapter_result_boolean`
90
- // folded in: a non-Boolean is a hard failure, never a falsy "absent".
91
- export function verifyUninstalledResources(inspectResult) {
92
- const read = readResult(inspectResult);
93
- if (read.kind !== "object") {
94
- return {
95
- ok: false,
96
- message: "cannot read the adapter ownership inspection after removal",
97
- };
98
- }
99
- const inspected = read.value;
100
- // A missing or non-object `resources` falls THROUGH to the Boolean check
101
- // rather than getting its own message.
102
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/adapter.sh:70::expected`
103
- // emits "expected Boolean adapter result at resources.plugin" for input {} —
104
- // the input tests/unit/harnesses/codex/lifecycle.test.ts's "a non-object resources falls
105
- // through to the Boolean message" exercises — so a distinct "not an object"
106
- // message here would be a port-only divergence. Parity, not divergence. Spec
107
- // §6.2.3 item 3.
108
- const resources = inspected.resources;
109
- const bag = typeof resources === "object" &&
110
- resources !== null &&
111
- !Array.isArray(resources)
112
- ? resources
113
- : {};
114
- for (const key of ["plugin", "marketplace"]) {
115
- if (typeof bag[key] !== "boolean") {
73
+ function installDecision(legacy) {
74
+ switch (legacy.kind) {
75
+ case "ok":
76
+ return { kind: "allowed" };
77
+ case "blocked":
116
78
  return {
117
- ok: false,
118
- message: `expected a Boolean adapter result at resources.${key}`,
79
+ kind: "blocked",
80
+ output: { stdout: [], stderr: legacy.lines },
119
81
  };
82
+ case "unknown":
83
+ return {
84
+ kind: "blocked",
85
+ output: { stdout: [], stderr: [`error: ${legacy.message}`] },
86
+ };
87
+ case "report":
88
+ return {
89
+ kind: "blocked",
90
+ output: {
91
+ stdout: [],
92
+ stderr: ["error: unexpected legacy report during installation"],
93
+ },
94
+ };
95
+ }
96
+ }
97
+ export function codexOwnershipInspection(identityState, removalInput, conflicts) {
98
+ const { pluginPresent, marketplacePresent } = removalInput;
99
+ const legacyEligibility = identityState.length === 0
100
+ ? {
101
+ kind: "blocked",
102
+ output: {
103
+ stdout: [],
104
+ stderr: ["error: probe did not report adapter identity state"],
105
+ },
120
106
  }
107
+ : installDecision(requireNoLegacyState(identityState));
108
+ const installEligibility = legacyEligibility.kind === "blocked" || conflicts.length === 0
109
+ ? legacyEligibility
110
+ : {
111
+ kind: "blocked",
112
+ output: {
113
+ stdout: [],
114
+ stderr: [
115
+ "Conflicting unmanaged Superpowers Codex resources require manual resolution:",
116
+ ...conflicts.map((conflict) => `- ${conflict}`),
117
+ "Remove or disable each resource manually, then retry.",
118
+ ],
119
+ },
120
+ };
121
+ const legacyReport = reportLegacyState(identityState);
122
+ let removalVerification;
123
+ if (pluginPresent || marketplacePresent) {
124
+ const message = pluginPresent
125
+ ? "owned plugin resource is still installed after removal"
126
+ : "owned marketplace resource is still registered after removal";
127
+ removalVerification = {
128
+ kind: "blocked",
129
+ output: { stdout: [], stderr: ["error: " + message] },
130
+ };
121
131
  }
122
- if (bag.plugin === true) {
123
- return {
124
- ok: false,
125
- message: "owned plugin resource is still installed after removal",
132
+ else if (legacyReport.kind === "unknown") {
133
+ removalVerification = {
134
+ kind: "blocked",
135
+ output: { stdout: [], stderr: [`error: ${legacyReport.message}`] },
126
136
  };
127
137
  }
128
- if (bag.marketplace === true) {
129
- return {
130
- ok: false,
131
- message: "owned marketplace resource is still registered after removal",
138
+ else if (legacyReport.kind === "blocked") {
139
+ removalVerification = {
140
+ kind: "blocked",
141
+ output: {
142
+ stdout: [],
143
+ stderr: ["error: unexpected legacy block after removal"],
144
+ },
132
145
  };
133
146
  }
134
- return { ok: true };
147
+ else {
148
+ removalVerification = { kind: "allowed" };
149
+ }
150
+ const postRemovalOutput = legacyReport.kind === "report"
151
+ ? { stdout: legacyReport.lines, stderr: [] }
152
+ : { stdout: [], stderr: [] };
153
+ return {
154
+ installEligibility,
155
+ removalInput,
156
+ removalVerification,
157
+ postRemovalOutput,
158
+ presentationValue: identityState,
159
+ presentationConflicts: conflicts,
160
+ };
161
+ }
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
+ };
135
191
  }
@@ -1,4 +1,4 @@
1
- import { escapeNonAscii, formatPythonNumber, } from "../../python-json-format.js";
1
+ import { escapeNonAscii } from "../../python-json-format.js";
2
2
  import { SafetyError } from "../../safety-error.js";
3
3
  import { isRawNumber, isRawObject, parseStrictJsonPreservingNumbers, } from "../../strict-json.js";
4
4
  const OVERLAY_PROFILE = {
@@ -9,14 +9,17 @@ const OVERLAY_PROFILE = {
9
9
  /**
10
10
  * Apply the manager's manifest overlay to a Codex plugin manifest.
11
11
  *
12
- * Byte-for-byte equivalent to the CPython implementation this replaces:
13
- * `json.dump(data, f, indent=2, allow_nan=False)` plus a trailing newline.
14
- * The byte-parity evidence lives in
15
- * `tests/baseline/harnesses/codex/manifest-overlay-parity.test.ts` (tracked as
16
- * `MANIFEST-READER-OVERLAY-01` in `docs/baseline/traceability.md`) against
17
- * the fixtures under `tests/fixtures/baseline/overlay-parity/`. Known,
18
- * deliberate divergences from strict byte parity are recorded in
12
+ * Preserves validated JSON numeric source tokens while applying the manager's
13
+ * manifest fields, with a trailing newline. The baseline contract lives in
14
+ * `tests/baseline/harnesses/codex/manifest-overlay-parity.test.ts` against the
15
+ * fixtures under `tests/fixtures/baseline/overlay-parity/`. The 5,000-digit
16
+ * integer widening is recorded in
19
17
  * `tests/fixtures/baseline/overlay-parity/divergent/README.md`.
18
+ *
19
+ * Decimal and exponent tokens are range-checked before emission. The strict
20
+ * parser rejects non-standard constants before they reach the emitter. Object
21
+ * member order, string escaping, and unknown fields remain part of this
22
+ * overlay contract.
20
23
  */
21
24
  export function applyManifestOverlay(source, version, path) {
22
25
  let parsed;
@@ -43,14 +46,10 @@ export function applyManifestOverlay(source, version, path) {
43
46
  throw rewrapNumberOutOfRange(cause, path);
44
47
  }
45
48
  }
46
- // formatPythonNumber (src/python-json-format.ts) has no notion of a manifest
47
- // path — it is a general CPython-`json.dump`-equivalent number formatter,
48
- // exercised on its own in tests/unit/python-json-format.test.js — so it
49
- // throws a bare `JSON number out of range: ${raw}`. That left the operator
50
- // with no indication of which manifest produced the diagnostic, unlike every
51
- // other overlay failure. This rewrap adds the path without touching
52
- // formatPythonNumber's own contract, and is scoped to exactly this message so
53
- // it cannot swallow or reword any other overlay throw.
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.
54
53
  const NUMBER_OUT_OF_RANGE_PREFIX = "JSON number out of range: ";
55
54
  function rewrapNumberOutOfRange(cause, path) {
56
55
  if (cause instanceof SafetyError &&
@@ -60,6 +59,12 @@ function rewrapNumberOutOfRange(cause, path) {
60
59
  }
61
60
  return cause;
62
61
  }
62
+ function emitNumber(raw) {
63
+ if (/[.eE]/.test(raw) && !Number.isFinite(Number(raw))) {
64
+ throw new SafetyError("manifest-overlay", `JSON number out of range: ${raw}`);
65
+ }
66
+ return raw;
67
+ }
63
68
  function setMember(entries, key, value) {
64
69
  const index = entries.findIndex(([existing]) => existing === key);
65
70
  if (index === -1)
@@ -105,7 +110,7 @@ function emit(value, depth) {
105
110
  // Order matters: isRawNumber before isRawObject, and both before Array —
106
111
  // the branded records are objects too.
107
112
  if (isRawNumber(value))
108
- return formatPythonNumber(value.source);
113
+ return emitNumber(value.source);
109
114
  if (isRawObject(value))
110
115
  return emitObject(value.entries, depth);
111
116
  const inner = " ".repeat(depth + 1);