superpowers-manager 0.5.2 → 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 (83) hide show
  1. package/README.md +44 -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 +32 -52
  6. package/dist/commands/adapter-call.js +28 -0
  7. package/dist/commands/install.js +26 -107
  8. package/dist/commands/mutation.js +7 -0
  9. package/dist/commands/pin.js +1 -1
  10. package/dist/commands/prepare.js +39 -129
  11. package/dist/commands/probe.js +35 -264
  12. package/dist/commands/track-latest.js +1 -1
  13. package/dist/commands/uninstall.js +16 -92
  14. package/dist/commands/update.js +5 -25
  15. package/dist/effective-selection.js +7 -9
  16. package/dist/git-tree.js +85 -0
  17. package/dist/harness-command-result.js +47 -0
  18. package/dist/harnesses/codex/adapter.js +231 -510
  19. package/dist/harnesses/codex/compatibility.js +2 -17
  20. package/dist/harnesses/codex/conflicts.js +2 -3
  21. package/dist/harnesses/codex/harness.js +18 -263
  22. package/dist/harnesses/codex/hooks.js +1 -1
  23. package/dist/harnesses/codex/json.js +1 -11
  24. package/dist/harnesses/codex/lifecycle.js +80 -72
  25. package/dist/harnesses/codex/manifest-overlay.js +23 -18
  26. package/dist/harnesses/codex/paths.js +14 -29
  27. package/dist/harnesses/codex/presentation.js +8 -37
  28. package/dist/harnesses/codex/publication.js +13 -37
  29. package/dist/harnesses/codex/recovery.js +5 -5
  30. package/dist/harnesses/codex/stored-state.js +1 -1
  31. package/dist/harnesses/opencode/config.js +256 -0
  32. package/dist/harnesses/opencode/discovery.js +587 -0
  33. package/dist/harnesses/opencode/harness.js +37 -0
  34. package/dist/harnesses/opencode/install.js +601 -0
  35. package/dist/harnesses/opencode/native.js +57 -0
  36. package/dist/harnesses/opencode/package.js +89 -0
  37. package/dist/harnesses/opencode/paths.js +38 -0
  38. package/dist/harnesses/opencode/prepare.js +30 -0
  39. package/dist/harnesses/opencode/presentation.js +28 -0
  40. package/dist/harnesses/opencode/state.js +174 -0
  41. package/dist/harnesses/pi/compatibility.js +5 -37
  42. package/dist/harnesses/pi/install.js +7 -5
  43. package/dist/harnesses/pi/native.js +3 -47
  44. package/dist/harnesses/pi/package.js +5 -107
  45. package/dist/harnesses/pi/prepare.js +13 -104
  46. package/dist/harnesses/pi/presentation.js +23 -66
  47. package/dist/harnesses/pi/shared-skills.js +28 -39
  48. package/dist/harnesses/pi/state.js +7 -27
  49. package/dist/provenance.js +2 -2
  50. package/dist/python-json-format.js +0 -69
  51. package/dist/python-json.js +2 -27
  52. package/dist/python-text.js +9 -69
  53. package/dist/safe-path.js +2 -2
  54. package/dist/selection-store.js +2 -22
  55. package/dist/snapshot-package.js +78 -0
  56. package/dist/snapshot-prepare.js +99 -0
  57. package/dist/snapshot-presentation.js +44 -0
  58. package/dist/validator.js +39 -73
  59. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  60. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  61. package/node_modules/jsonc-parser/README.md +364 -0
  62. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  63. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  64. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  65. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  66. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  67. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  68. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  69. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  70. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  71. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  72. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  73. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  74. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  75. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  76. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  77. package/node_modules/jsonc-parser/package.json +37 -0
  78. package/package.json +8 -4
  79. package/dist/domain/fingerprint.js +0 -4
  80. package/dist/selection-state-cli.js +0 -85
  81. package/dist/status.js +0 -24
  82. package/dist/upstream-cli.js +0 -110
  83. package/dist/validate-generated-plugin-cli.js +0 -118
@@ -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,
@@ -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);
@@ -1,9 +1,7 @@
1
- import { failureResult, hasTerminalControl, successResult, } from "../../adapter-result.js";
2
- import { codexInspect, codexInstall, } from "./adapter.js";
1
+ import { failureResult, successResult, } from "../../adapter-result.js";
2
+ import { codexInspectControl, codexInspectOwnership, codexInstall, } from "./adapter.js";
3
3
  import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./prepare.js";
4
- import { codexInstallReceipt, codexPresentation } from "./presentation.js";
5
- import { reportLegacyState, requireManagedUpdateControl, requireNoLegacyState, verifyUninstalledResources, } from "./lifecycle.js";
6
- import { commitMatches } from "../../status.js";
4
+ import { codexPresentation } from "./presentation.js";
7
5
  import { codexHome, codexPaths } from "./paths.js";
8
6
  import { installCodexMarketplace, removeCodexMarketplace, } from "./publication.js";
9
7
  import { readCodexRecovery } from "./recovery.js";
@@ -14,257 +12,11 @@ function preserveFailure(result) {
14
12
  }
15
13
  return { status: result.status, outcome: result.outcome };
16
14
  }
17
- function malformed(result, message) {
18
- return failureResult(result.outcome.operation, "malformed-result", message, [], result.outcome.messages);
19
- }
20
- function invalidStatus(result, message) {
15
+ export function rejectSuccessfulNonzeroStatus(result, message) {
16
+ if (!result.outcome.ok || result.status === 0)
17
+ return result;
21
18
  return failureResult(result.outcome.operation, "invalid-status", message, [], result.outcome.messages);
22
19
  }
23
- function resultRecord(result) {
24
- const value = result.outcome.ok ? result.outcome.result : null;
25
- return typeof value === "object" && value !== null && !Array.isArray(value)
26
- ? value
27
- : null;
28
- }
29
- function stringField(result, key) {
30
- const raw = result[key];
31
- if (raw === null || raw === undefined)
32
- return { ok: true, value: "" };
33
- if (typeof raw === "string")
34
- return { ok: true, value: raw };
35
- return { ok: false };
36
- }
37
- function resourceFlag(result, key) {
38
- const resources = result.resources;
39
- const bag = typeof resources === "object" &&
40
- resources !== null &&
41
- !Array.isArray(resources)
42
- ? resources
43
- : {};
44
- const value = bag[key];
45
- return typeof value === "boolean" ? value : null;
46
- }
47
- function conflictDescriptions(result) {
48
- const raw = result.conflicts;
49
- if (raw === null || raw === undefined)
50
- return [];
51
- if (!Array.isArray(raw))
52
- return null;
53
- const descriptions = [];
54
- for (const item of raw) {
55
- if (typeof item !== "string" ||
56
- item.length === 0 ||
57
- hasTerminalControl(item)) {
58
- return null;
59
- }
60
- descriptions.push(item);
61
- }
62
- return descriptions;
63
- }
64
- function installDecision(legacy) {
65
- switch (legacy.kind) {
66
- case "ok":
67
- return { kind: "allowed" };
68
- case "blocked":
69
- return {
70
- kind: "blocked",
71
- output: { stdout: [], stderr: legacy.lines },
72
- };
73
- case "unknown":
74
- return {
75
- kind: "blocked",
76
- output: { stdout: [], stderr: [`error: ${legacy.message}`] },
77
- };
78
- case "report":
79
- return {
80
- kind: "blocked",
81
- output: {
82
- stdout: [],
83
- stderr: ["error: unexpected legacy report during installation"],
84
- },
85
- };
86
- }
87
- }
88
- export function normalizeCodexOwnership(result) {
89
- if (!result.outcome.ok)
90
- return preserveFailure(result);
91
- if (result.status !== 0) {
92
- return invalidStatus(result, "adapter reported a failure status for inspect --view ownership");
93
- }
94
- const record = resultRecord(result);
95
- if (record === null) {
96
- return malformed(result, "adapter returned a non-object result for inspect --view ownership");
97
- }
98
- const pluginPresent = resourceFlag(record, "plugin");
99
- if (pluginPresent === null) {
100
- return malformed(result, "expected a Boolean adapter result at resources.plugin");
101
- }
102
- const marketplacePresent = resourceFlag(record, "marketplace");
103
- if (marketplacePresent === null) {
104
- return malformed(result, "expected a Boolean adapter result at resources.marketplace");
105
- }
106
- const identity = stringField(record, "identity_state");
107
- if (!identity.ok) {
108
- return malformed(result, "adapter returned a non-string identity_state for inspect --view ownership");
109
- }
110
- const identityState = identity.value;
111
- const conflicts = conflictDescriptions(record);
112
- if (conflicts === null) {
113
- return malformed(result, "expected an array of strings at conflicts");
114
- }
115
- const legacyEligibility = identityState.length === 0
116
- ? {
117
- kind: "blocked",
118
- output: {
119
- stdout: [],
120
- stderr: ["error: probe did not report adapter identity state"],
121
- },
122
- }
123
- : installDecision(requireNoLegacyState(identityState));
124
- const installEligibility = legacyEligibility.kind === "blocked" || conflicts.length === 0
125
- ? legacyEligibility
126
- : {
127
- kind: "blocked",
128
- output: {
129
- stdout: [],
130
- stderr: [
131
- "Conflicting unmanaged Superpowers Codex resources require manual resolution:",
132
- ...conflicts.map((conflict) => `- ${conflict}`),
133
- "Remove or disable each resource manually, then retry.",
134
- ],
135
- },
136
- };
137
- const verification = verifyUninstalledResources(result);
138
- const legacyReport = reportLegacyState(identityState);
139
- let removalVerification;
140
- if (!verification.ok) {
141
- removalVerification = {
142
- kind: "blocked",
143
- output: { stdout: [], stderr: [`error: ${verification.message}`] },
144
- };
145
- }
146
- else if (legacyReport.kind === "unknown") {
147
- removalVerification = {
148
- kind: "blocked",
149
- output: { stdout: [], stderr: [`error: ${legacyReport.message}`] },
150
- };
151
- }
152
- else if (legacyReport.kind === "blocked") {
153
- removalVerification = {
154
- kind: "blocked",
155
- output: {
156
- stdout: [],
157
- stderr: ["error: unexpected legacy block after removal"],
158
- },
159
- };
160
- }
161
- else {
162
- removalVerification = { kind: "allowed" };
163
- }
164
- const postRemovalOutput = legacyReport.kind === "report"
165
- ? { stdout: legacyReport.lines, stderr: [] }
166
- : { stdout: [], stderr: [] };
167
- return successResult(result.outcome.operation, {
168
- installEligibility,
169
- removalInput: { pluginPresent, marketplacePresent },
170
- removalVerification,
171
- postRemovalOutput,
172
- presentationValue: identityState,
173
- presentationConflicts: conflicts,
174
- }, result.outcome.messages);
175
- }
176
- export function normalizeCodexControl(result) {
177
- if (!result.outcome.ok)
178
- return preserveFailure(result);
179
- if (result.status !== 0) {
180
- return invalidStatus(result, "adapter reported a failure status for inspect --view update-control");
181
- }
182
- const record = resultRecord(result);
183
- if (record === null) {
184
- return malformed(result, "adapter returned a non-object result for inspect --view update-control");
185
- }
186
- const control = stringField(record, "update_control");
187
- if (!control.ok) {
188
- return malformed(result, "adapter returned a non-string update_control for inspect --view update-control");
189
- }
190
- const updateControl = control.value;
191
- const probeEligibility = updateControl.length === 0
192
- ? {
193
- kind: "blocked",
194
- output: {
195
- stdout: [],
196
- stderr: [
197
- "error: probe did not report adapter update-control capability",
198
- ],
199
- },
200
- }
201
- : { kind: "allowed" };
202
- const managed = requireManagedUpdateControl(updateControl);
203
- const mutationEligibility = probeEligibility.kind === "blocked"
204
- ? probeEligibility
205
- : managed.ok
206
- ? { kind: "allowed" }
207
- : {
208
- kind: "blocked",
209
- output: {
210
- stdout: [],
211
- stderr: [`error: ${managed.message}`],
212
- },
213
- };
214
- return successResult(result.outcome.operation, { probeEligibility, mutationEligibility, presentationValue: updateControl }, result.outcome.messages);
215
- }
216
- export function normalizeCodexInstalled(result, desiredCommit) {
217
- if (!result.outcome.ok)
218
- return preserveFailure(result);
219
- if (result.status !== 0) {
220
- return invalidStatus(result, "adapter reported a failure status for inspect --view fingerprint");
221
- }
222
- const record = resultRecord(result);
223
- if (record === null) {
224
- return malformed(result, "adapter returned a non-object result for inspect --view fingerprint");
225
- }
226
- const fingerprint = stringField(record, "fingerprint");
227
- if (!fingerprint.ok) {
228
- return malformed(result, "adapter returned a non-string fingerprint for inspect --view fingerprint");
229
- }
230
- const observedIdentity = fingerprint.value;
231
- const installed = observedIdentity.length === 0
232
- ? { kind: "absent", observedIdentity: "" }
233
- : commitMatches(desiredCommit, observedIdentity)
234
- ? { kind: "current", observedIdentity }
235
- : { kind: "mismatch", observedIdentity };
236
- return successResult(result.outcome.operation, installed, result.outcome.messages);
237
- }
238
- function safeHint(value) {
239
- return typeof value === "string" && !hasTerminalControl(value) ? value : "";
240
- }
241
- export function normalizeCodexInstall(result) {
242
- if (!result.outcome.ok)
243
- return preserveFailure(result);
244
- if (result.status !== 0) {
245
- return invalidStatus(result, "adapter reported a failure status for install");
246
- }
247
- const record = resultRecord(result);
248
- if (record === null) {
249
- return malformed(result, "adapter returned a non-object result for install");
250
- }
251
- const hints = record.verification_hints;
252
- const bag = typeof hints === "object" && hints !== null && !Array.isArray(hints)
253
- ? hints
254
- : {};
255
- return successResult(result.outcome.operation, codexInstallReceipt(safeHint(bag.missing), safeHint(bag.mismatch)), result.outcome.messages);
256
- }
257
- // The canonical payload normalizer has no context parameter. Keep the native
258
- // call's defensive success/nonzero-status guard beside the concrete adapter,
259
- // where the exact legacy diagnostic can still name ctx.root.
260
- export function normalizeCodexInstallForContext(result, ctx) {
261
- if (!result.outcome.ok)
262
- return preserveFailure(result);
263
- if (result.status !== 0) {
264
- return invalidStatus(result, codexPresentation.callFailure("install", ctx).invalidStatus);
265
- }
266
- return normalizeCodexInstall(result);
267
- }
268
20
  function requirements(command, env) {
269
21
  if (command !== "probe" &&
270
22
  command !== "install" &&
@@ -286,9 +38,9 @@ async function mutationRoots(ctx) {
286
38
  return [codexHome(ctx.env ?? {}, process.cwd())];
287
39
  }
288
40
  async function inspectCodexUpdateControl(ctx) {
289
- const normalized = normalizeCodexControl(await codexInspect("update-control", ctx));
290
- if (!normalized.outcome.ok)
291
- return normalized;
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;
292
44
  const paths = codexPaths(ctx.env ?? {}, process.cwd());
293
45
  let diagnostic = null;
294
46
  try {
@@ -300,17 +52,17 @@ async function inspectCodexUpdateControl(ctx) {
300
52
  diagnostic = `cannot inspect Codex recovery state at ${paths.recoveryRoot}`;
301
53
  }
302
54
  if (diagnostic === null)
303
- return normalized;
55
+ return result;
304
56
  const decision = {
305
57
  kind: "blocked",
306
58
  output: { stdout: [], stderr: [`error: ${diagnostic}`] },
307
59
  };
308
- return successResult(normalized.outcome.operation, {
60
+ return successResult(result.outcome.operation, {
309
61
  probeEligibility: decision,
310
62
  mutationEligibility: decision,
311
63
  presentationValue: diagnostic,
312
64
  recoveryState: "required",
313
- }, normalized.outcome.messages);
65
+ }, result.outcome.messages);
314
66
  }
315
67
  export const codexHarness = {
316
68
  preparationLocation: codexPreparationLocation,
@@ -319,16 +71,19 @@ export const codexHarness = {
319
71
  prepareCandidate: prepareCodexCandidate,
320
72
  inspectPrepared: inspectCodexPrepared,
321
73
  readPrepared: readCodexPrepared,
322
- inspectOwnership: async (ctx) => normalizeCodexOwnership(await codexInspect("ownership", ctx)),
74
+ inspectOwnership: codexInspectOwnership,
323
75
  inspectUpdateControl: inspectCodexUpdateControl,
324
76
  inspectInstalled: inspectCodexInstallation,
325
- install: async (artifact, ctx) => installCodexMarketplace(artifact, ctx, async (root, context) => normalizeCodexInstallForContext(await codexInstall(root, context), context)),
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
+ }),
326
81
  remove: async (input, ctx) => {
327
82
  const result = await removeCodexMarketplace(input, ctx);
328
83
  if (!result.outcome.ok)
329
84
  return preserveFailure(result);
330
85
  if (result.status !== 0) {
331
- return invalidStatus(result, codexPresentation.callFailure("remove", ctx, input).invalidStatus);
86
+ return rejectSuccessfulNonzeroStatus(result, codexPresentation.callFailure("remove", ctx, input).invalidStatus);
332
87
  }
333
88
  return successResult(result.outcome.operation, null, result.outcome.messages);
334
89
  },
@@ -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,84 +52,92 @@ export function reportLegacyState(identityState) {
52
52
  }
53
53
  return unknownState(identityState);
54
54
  }
55
- export { verifyInstalledFingerprint } from "./presentation.js";
56
- // Ported from
57
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:62-70::spw_require_managed_update_control`.
58
- // Both refusals reached spw_die in the shell, so both carry the `error: ` prefix
59
- // at the call site and neither is special.
60
- export function requireManagedUpdateControl(value) {
61
- if (value === "managed")
62
- return { ok: true };
63
- if (value === "unsupported") {
64
- return {
65
- ok: false,
66
- message: "adapter cannot guarantee manager-controlled updates",
67
- };
68
- }
69
- return {
70
- ok: false,
71
- message: `unknown adapter update-control capability: ${value}`,
72
- };
73
- }
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") {
55
+ function installDecision(legacy) {
56
+ switch (legacy.kind) {
57
+ case "ok":
58
+ return { kind: "allowed" };
59
+ case "blocked":
116
60
  return {
117
- ok: false,
118
- message: `expected a Boolean adapter result at resources.${key}`,
61
+ kind: "blocked",
62
+ output: { stdout: [], stderr: legacy.lines },
119
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
+ }
78
+ }
79
+ export function codexOwnershipInspection(identityState, removalInput, conflicts) {
80
+ 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
+ },
120
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
+ };
121
113
  }
122
- if (bag.plugin === true) {
123
- return {
124
- ok: false,
125
- message: "owned plugin resource is still installed after removal",
114
+ else if (legacyReport.kind === "unknown") {
115
+ removalVerification = {
116
+ kind: "blocked",
117
+ output: { stdout: [], stderr: [`error: ${legacyReport.message}`] },
126
118
  };
127
119
  }
128
- if (bag.marketplace === true) {
129
- return {
130
- ok: false,
131
- message: "owned marketplace resource is still registered after removal",
120
+ else if (legacyReport.kind === "blocked") {
121
+ removalVerification = {
122
+ kind: "blocked",
123
+ output: {
124
+ stdout: [],
125
+ stderr: ["error: unexpected legacy block after removal"],
126
+ },
132
127
  };
133
128
  }
134
- return { ok: true };
129
+ else {
130
+ removalVerification = { kind: "allowed" };
131
+ }
132
+ const postRemovalOutput = legacyReport.kind === "report"
133
+ ? { stdout: legacyReport.lines, stderr: [] }
134
+ : { stdout: [], stderr: [] };
135
+ return {
136
+ installEligibility,
137
+ removalInput,
138
+ removalVerification,
139
+ postRemovalOutput,
140
+ presentationValue: identityState,
141
+ presentationConflicts: conflicts,
142
+ };
135
143
  }
@@ -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)
@@ -69,7 +74,7 @@ function setMember(entries, key, value) {
69
74
  }
70
75
  /**
71
76
  * Map strict-json's diagnostics onto the CPython wording the callers assert.
72
- * Repository policy at `AGENTS.md:92::most diagnostics are asserted as`
77
+ * Repository policy at `AGENTS.md:89::most diagnostics are asserted as`
73
78
  * freezes most of these diagnostics as complete-string contracts.
74
79
  */
75
80
  function translate(cause, source, path) {
@@ -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);