superpowers-manager 0.5.0 → 0.5.2

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 (62) hide show
  1. package/README.md +39 -9
  2. package/dist/artifact-tree.js +2 -1
  3. package/dist/cli.js +3 -3
  4. package/dist/commands/install.js +18 -8
  5. package/dist/commands/prepare.js +5 -5
  6. package/dist/commands/probe.js +4 -4
  7. package/dist/commands/uninstall.js +3 -3
  8. package/dist/commands/update.js +1 -1
  9. package/dist/domain/refs.js +4 -0
  10. package/dist/{adapter.js → harnesses/codex/adapter.js} +156 -27
  11. package/dist/{codex-compatibility.js → harnesses/codex/compatibility.js} +7 -7
  12. package/dist/{codex-conflicts.js → harnesses/codex/conflicts.js} +1 -1
  13. package/dist/{generated-plugin.js → harnesses/codex/generated-plugin.js} +8 -74
  14. package/dist/{codex-harness.js → harnesses/codex/harness.js} +39 -21
  15. package/dist/{hooks.js → harnesses/codex/hooks.js} +7 -7
  16. package/dist/{codex-json.js → harnesses/codex/json.js} +8 -4
  17. package/dist/{lifecycle.js → harnesses/codex/lifecycle.js} +2 -2
  18. package/dist/{manifest-overlay.js → harnesses/codex/manifest-overlay.js} +4 -4
  19. package/dist/harnesses/codex/marketplace.js +148 -0
  20. package/dist/harnesses/codex/paths.js +97 -0
  21. package/dist/{codex-prepare.js → harnesses/codex/prepare.js} +41 -9
  22. package/dist/{codex-presentation.js → harnesses/codex/presentation.js} +7 -6
  23. package/dist/harnesses/codex/publication.js +538 -0
  24. package/dist/harnesses/codex/recovery.js +338 -0
  25. package/dist/harnesses/codex/state.js +209 -0
  26. package/dist/harnesses/codex/stored-state.js +249 -0
  27. package/dist/{pi-compatibility.js → harnesses/pi/compatibility.js} +3 -3
  28. package/dist/{pi-harness.js → harnesses/pi/harness.js} +7 -7
  29. package/dist/{pi-install.js → harnesses/pi/install.js} +8 -8
  30. package/dist/{pi-native.js → harnesses/pi/native.js} +4 -4
  31. package/dist/{pi-package.js → harnesses/pi/package.js} +6 -6
  32. package/dist/{pi-prepare.js → harnesses/pi/prepare.js} +6 -6
  33. package/dist/{pi-settings.js → harnesses/pi/settings.js} +3 -3
  34. package/dist/{pi-shared-skills.js → harnesses/pi/shared-skills.js} +1 -1
  35. package/dist/{pi-state.js → harnesses/pi/state.js} +8 -8
  36. package/dist/provenance.js +3 -3
  37. package/dist/python-text.js +16 -0
  38. package/dist/skill-validation.js +52 -0
  39. package/dist/validate-generated-plugin-cli.js +1 -1
  40. package/node_modules/smol-toml/LICENSE +24 -0
  41. package/node_modules/smol-toml/README.md +418 -0
  42. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  43. package/node_modules/smol-toml/dist/date.js +127 -0
  44. package/node_modules/smol-toml/dist/error.d.ts +38 -0
  45. package/node_modules/smol-toml/dist/error.js +63 -0
  46. package/node_modules/smol-toml/dist/extract.js +69 -0
  47. package/node_modules/smol-toml/dist/index.cjs +734 -0
  48. package/node_modules/smol-toml/dist/index.d.ts +43 -0
  49. package/node_modules/smol-toml/dist/index.js +33 -0
  50. package/node_modules/smol-toml/dist/parse.d.ts +36 -0
  51. package/node_modules/smol-toml/dist/parse.js +149 -0
  52. package/node_modules/smol-toml/dist/primitive.js +238 -0
  53. package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
  54. package/node_modules/smol-toml/dist/stringify.js +181 -0
  55. package/node_modules/smol-toml/dist/struct.js +179 -0
  56. package/node_modules/smol-toml/dist/util.d.ts +38 -0
  57. package/node_modules/smol-toml/dist/util.js +89 -0
  58. package/node_modules/smol-toml/package.json +68 -0
  59. package/package.json +13 -4
  60. package/dist/codex-state.js +0 -78
  61. /package/dist/{pi-paths.js → harnesses/pi/paths.js} +0 -0
  62. /package/dist/{pi-presentation.js → harnesses/pi/presentation.js} +0 -0
@@ -1,20 +1,9 @@
1
- import { lstat, readdir, readFile, readlink, stat } from "node:fs/promises";
2
1
  import { homedir } from "node:os";
3
2
  import { posix } from "node:path";
4
- import { COMMIT_RE } from "./domain/refs.js";
5
- import { pythonSplitlines, pythonStrip } from "./python-text.js";
6
- import { parseStrictJson } from "./strict-json.js";
7
- export const DEFAULT_FS_DEPS = {
8
- lstat,
9
- stat,
10
- readdir,
11
- readlink,
12
- readFile,
13
- };
14
- // The Python's own SEMVER_RE (:15-22). Deliberately not SEMVER_BASE_SOURCE
15
- // from src/domain/refs.ts, which omits the `+build` component; widening the
16
- // shared constant would change TAG_RE and is out of scope.
17
- export const SEMVER_RE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
3
+ import { COMMIT_RE, SEMVER_RE } from "../../domain/refs.js";
4
+ import { compareByCodePoint, pythonStrip } from "../../python-text.js";
5
+ import { DEFAULT_FS_DEPS, validateSkillFrontmatter, } from "../../skill-validation.js";
6
+ import { parseStrictJson } from "../../strict-json.js";
18
7
  const MANIFEST_JSON_PROFILE = {
19
8
  duplicateKeys: "last-wins",
20
9
  nonStandardConstants: "reject",
@@ -50,7 +39,7 @@ function decodePathBytes(bytes) {
50
39
  * syscall, so a string carrying one would open a different file than the
51
40
  * caller named. CPython raises `UnicodeEncodeError` instead.
52
41
  */
53
- export function hasUnpairedSurrogate(value) {
42
+ function hasUnpairedSurrogate(value) {
54
43
  for (let index = 0; index < value.length; index += 1) {
55
44
  const unit = value.charCodeAt(index);
56
45
  if (unit >= 0xd800 && unit <= 0xdbff) {
@@ -443,61 +432,6 @@ const REQUIRED_FILES = [
443
432
  "README.md",
444
433
  "CODE_OF_CONDUCT.md",
445
434
  ];
446
- /** Python `sorted()` orders by code point; JavaScript's default sort does not. */
447
- export function compareByCodePoint(left, right) {
448
- // `Array.from` splits by code point, exactly as spreading would; oxlint's
449
- // `no-misused-spread` rejects the spread form, and grapheme segmentation is
450
- // the wrong unit here — Python compares code points.
451
- const leftPoints = Array.from(left);
452
- const rightPoints = Array.from(right);
453
- const shared = Math.min(leftPoints.length, rightPoints.length);
454
- for (let index = 0; index < shared; index += 1) {
455
- const a = leftPoints[index].codePointAt(0);
456
- const b = rightPoints[index].codePointAt(0);
457
- if (a !== b)
458
- return a - b;
459
- }
460
- return leftPoints.length - rightPoints.length;
461
- }
462
- export async function validateSkillFrontmatter(skillMd, skillName, errors, deps) {
463
- let contents;
464
- try {
465
- contents = STRICT_DECODER.decode(await deps.readFile(skillMd));
466
- }
467
- catch {
468
- errors.push(`skill \`${skillName}\` has unreadable UTF-8 \`SKILL.md\``);
469
- return;
470
- }
471
- if (contents === "") {
472
- errors.push(`skill \`${skillName}\` has empty \`SKILL.md\``);
473
- return;
474
- }
475
- const lines = pythonSplitlines(contents);
476
- if (lines.length === 0 || lines[0] !== "---") {
477
- errors.push(`skill \`${skillName}\` must start with \`---\``);
478
- return;
479
- }
480
- const closingIndex = lines.indexOf("---", 1);
481
- if (closingIndex === -1) {
482
- errors.push(`skill \`${skillName}\` frontmatter is not closed`);
483
- return;
484
- }
485
- const frontmatter = lines.slice(1, closingIndex);
486
- for (const key of ["name", "description"]) {
487
- const matches = frontmatter.filter((line) => line.startsWith(`${key}:`));
488
- if (matches.length !== 1) {
489
- errors.push(`skill \`${skillName}\` frontmatter must contain exactly one top-level \`${key}:\``);
490
- continue;
491
- }
492
- const value = pythonStrip(matches[0].slice(matches[0].indexOf(":") + 1));
493
- if (value === "" ||
494
- value === "''" ||
495
- value === '""' ||
496
- value.startsWith("#")) {
497
- errors.push(`skill \`${skillName}\` frontmatter field \`${key}\` must be non-empty`);
498
- }
499
- }
500
- }
501
435
  async function validateHookSubtree(pluginRoot, hooksRoot, errors, deps) {
502
436
  let resolvedRoot;
503
437
  try {
@@ -513,9 +447,9 @@ async function validateHookSubtree(pluginRoot, hooksRoot, errors, deps) {
513
447
  isLink = (await inspectLink(path, deps)) === "symlink";
514
448
  }
515
449
  catch {
516
- // The `src/generated-plugin.ts:667::isLink =` probe reaches the first catch bounded by `src/generated-plugin.ts:672-676::let rawTarget`; the `src/generated-plugin.ts:678::rawTarget = decodePathBytes` readlink reaches the same site text.
517
- // The three probes sharing the *subtree* string are `src/generated-plugin.ts:711::resolvedDirectory =`, `src/generated-plugin.ts:721::children =`, and
518
- // `src/generated-plugin.ts:730::deps, true)) === "directory"`, not this one.
450
+ // The `src/harnesses/codex/generated-plugin.ts:583::isLink =` probe reaches the first catch bounded by `src/harnesses/codex/generated-plugin.ts:592::let rawTarget`; the `src/harnesses/codex/generated-plugin.ts:594::rawTarget = decodePathBytes` readlink reaches the same site text.
451
+ // The three probes sharing the *subtree* string are `src/harnesses/codex/generated-plugin.ts:627::resolvedDirectory =`, `src/harnesses/codex/generated-plugin.ts:637::children =`, and
452
+ // `src/harnesses/codex/generated-plugin.ts:646::deps, true)) === "directory"`, not this one.
519
453
  errors.push(`generated hook symlink could not be inspected: ${path}`);
520
454
  return false;
521
455
  }
@@ -1,11 +1,13 @@
1
- import { join, resolve } from "node:path";
2
- import { failureResult, hasTerminalControl, successResult, } from "./adapter-result.js";
3
- import { codexInspect, codexInstall, codexRemove, } from "./adapter.js";
4
- import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./codex-prepare.js";
5
- import { codexInstallReceipt, codexPresentation, } from "./codex-presentation.js";
1
+ import { failureResult, hasTerminalControl, successResult, } from "../../adapter-result.js";
2
+ import { codexInspect, codexInstall, } from "./adapter.js";
3
+ import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./prepare.js";
4
+ import { codexInstallReceipt, codexPresentation } from "./presentation.js";
6
5
  import { reportLegacyState, requireManagedUpdateControl, requireNoLegacyState, verifyUninstalledResources, } from "./lifecycle.js";
7
- import { SafetyError } from "./safety-error.js";
8
- import { commitMatches } from "./status.js";
6
+ import { commitMatches } from "../../status.js";
7
+ import { codexHome, codexPaths } from "./paths.js";
8
+ import { installCodexMarketplace, removeCodexMarketplace, } from "./publication.js";
9
+ import { readCodexRecovery } from "./recovery.js";
10
+ import { inspectCodexInstallation } from "./state.js";
9
11
  function preserveFailure(result) {
10
12
  if (result.outcome.ok) {
11
13
  throw new Error("cannot preserve a successful adapter result as failure");
@@ -281,16 +283,34 @@ function requirements(command, env) {
281
283
  ];
282
284
  }
283
285
  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)];
286
+ return [codexHome(ctx.env ?? {}, process.cwd())];
287
+ }
288
+ async function inspectCodexUpdateControl(ctx) {
289
+ const normalized = normalizeCodexControl(await codexInspect("update-control", ctx));
290
+ if (!normalized.outcome.ok)
291
+ return normalized;
292
+ const paths = codexPaths(ctx.env ?? {}, process.cwd());
293
+ let diagnostic = null;
294
+ try {
295
+ if ((await readCodexRecovery(paths)) !== null) {
296
+ diagnostic = `Codex recovery required; preserve material at ${paths.recoveryRoot} for manual resolution`;
297
+ }
288
298
  }
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");
299
+ catch {
300
+ diagnostic = `cannot inspect Codex recovery state at ${paths.recoveryRoot}`;
292
301
  }
293
- return [join(resolve(home), ".codex")];
302
+ if (diagnostic === null)
303
+ return normalized;
304
+ const decision = {
305
+ kind: "blocked",
306
+ output: { stdout: [], stderr: [`error: ${diagnostic}`] },
307
+ };
308
+ return successResult(normalized.outcome.operation, {
309
+ probeEligibility: decision,
310
+ mutationEligibility: decision,
311
+ presentationValue: diagnostic,
312
+ recoveryState: "required",
313
+ }, normalized.outcome.messages);
294
314
  }
295
315
  export const codexHarness = {
296
316
  preparationLocation: codexPreparationLocation,
@@ -300,13 +320,11 @@ export const codexHarness = {
300
320
  inspectPrepared: inspectCodexPrepared,
301
321
  readPrepared: readCodexPrepared,
302
322
  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
- },
323
+ inspectUpdateControl: inspectCodexUpdateControl,
324
+ inspectInstalled: inspectCodexInstallation,
325
+ install: async (artifact, ctx) => installCodexMarketplace(artifact, ctx, async (root, context) => normalizeCodexInstallForContext(await codexInstall(root, context), context)),
308
326
  remove: async (input, ctx) => {
309
- const result = await codexRemove(input, ctx);
327
+ const result = await removeCodexMarketplace(input, ctx);
310
328
  if (!result.outcome.ok)
311
329
  return preserveFailure(result);
312
330
  if (result.status !== 0) {
@@ -1,10 +1,10 @@
1
1
  import { copyFile, cp, mkdir, readdir, readFile, readlink, stat, symlink, } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, join, resolve } from "node:path";
3
- import { assertExistingContained, assertProspectiveContained, classifyPathNoFollow, } from "./safe-path.js";
4
- import { SafetyError } from "./safety-error.js";
5
- import { parseStrictJson } from "./strict-json.js";
3
+ import { assertExistingContained, assertProspectiveContained, classifyPathNoFollow, } from "../../safe-path.js";
4
+ import { SafetyError } from "../../safety-error.js";
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.
@@ -77,11 +77,11 @@ export async function readManifest(path) {
77
77
  catch {
78
78
  // Deliberately drops the cause on both branches. `detail(cause)` on the
79
79
  // read branch surfaced a raw errno (`ENOENT: … open '<path>'`), which the
80
- // `hook classification failed:` site in src/adapter.ts re-emits onto the
80
+ // `hook classification failed:` site in src/harnesses/codex/adapter.ts re-emits onto the
81
81
  // terminal these commands write to; the parse branch surfaced
82
- // strict-json's own wording under a prefix src/manifest-overlay.ts also
82
+ // strict-json's own wording under a prefix src/harnesses/codex/manifest-overlay.ts also
83
83
  // uses with CPython wording.
84
- // Same text as the `cannot read manifest JSON in` site in src/adapter.ts.
84
+ // Same text as the `cannot read manifest JSON in` site in src/harnesses/codex/adapter.ts.
85
85
  throw hookError(`cannot read manifest JSON in ${path}`);
86
86
  }
87
87
  let parsed;
@@ -1,5 +1,5 @@
1
- import { SafetyError } from "./safety-error.js";
2
- import { parseStrictJson, } from "./strict-json.js";
1
+ import { SafetyError } from "../../safety-error.js";
2
+ import { parseStrictJson, } from "../../strict-json.js";
3
3
  const ACCEPT_CONSTANTS = {
4
4
  duplicateKeys: "last-wins",
5
5
  nonStandardConstants: "accept",
@@ -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,7 @@ export function reportLegacyState(identityState) {
52
52
  }
53
53
  return unknownState(identityState);
54
54
  }
55
- export { verifyInstalledFingerprint, } from "./codex-presentation.js";
55
+ export { verifyInstalledFingerprint } from "./presentation.js";
56
56
  // Ported from
57
57
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:62-70::spw_require_managed_update_control`.
58
58
  // Both refusals reached spw_die in the shell, so both carry the `error: ` prefix
@@ -101,7 +101,7 @@ export function verifyUninstalledResources(inspectResult) {
101
101
  // rather than getting its own message.
102
102
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/adapter.sh:70::expected`
103
103
  // emits "expected Boolean adapter result at resources.plugin" for input {} —
104
- // the input tests/unit/lifecycle.test.js's "a non-object resources falls
104
+ // the input tests/unit/harnesses/codex/lifecycle.test.ts's "a non-object resources falls
105
105
  // through to the Boolean message" exercises — so a distinct "not an object"
106
106
  // message here would be a port-only divergence. Parity, not divergence. Spec
107
107
  // §6.2.3 item 3.
@@ -1,6 +1,6 @@
1
- import { escapeNonAscii, formatPythonNumber } from "./python-json-format.js";
2
- import { SafetyError } from "./safety-error.js";
3
- import { isRawNumber, isRawObject, parseStrictJsonPreservingNumbers, } from "./strict-json.js";
1
+ import { escapeNonAscii, formatPythonNumber, } from "../../python-json-format.js";
2
+ import { SafetyError } from "../../safety-error.js";
3
+ import { isRawNumber, isRawObject, parseStrictJsonPreservingNumbers, } from "../../strict-json.js";
4
4
  const OVERLAY_PROFILE = {
5
5
  duplicateKeys: "last-wins",
6
6
  nonStandardConstants: "reject",
@@ -12,7 +12,7 @@ const OVERLAY_PROFILE = {
12
12
  * Byte-for-byte equivalent to the CPython implementation this replaces:
13
13
  * `json.dump(data, f, indent=2, allow_nan=False)` plus a trailing newline.
14
14
  * The byte-parity evidence lives in
15
- * `tests/baseline/manifest-overlay-parity.test.js` (tracked as
15
+ * `tests/baseline/harnesses/codex/manifest-overlay-parity.test.ts` (tracked as
16
16
  * `MANIFEST-READER-OVERLAY-01` in `docs/baseline/traceability.md`) against
17
17
  * the fixtures under `tests/fixtures/baseline/overlay-parity/`. Known,
18
18
  * deliberate divergences from strict byte parity are recorded in
@@ -0,0 +1,148 @@
1
+ import { copyFile, cp, lstat, mkdir, readdir, rm, writeFile, } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, } from "../../artifact-tree.js";
4
+ import { assertNoFollowType, classifyPathNoFollow } from "../../safe-path.js";
5
+ import { SafetyError } from "../../safety-error.js";
6
+ import { readCodexAssessment } from "./compatibility.js";
7
+ const MARKETPLACE_PATH = ".agents/plugins/marketplace.json";
8
+ const PLUGIN_PATH = "plugins/superpowers";
9
+ function inspectionError(root, cause) {
10
+ return new SafetyError("codex-marketplace", `cannot inspect owned Codex marketplace: ${root}`, { cause });
11
+ }
12
+ function validateMarketplace(value) {
13
+ const plugins = value.plugins;
14
+ if (!Array.isArray(plugins) || plugins.length !== 1) {
15
+ throw new Error("marketplace plugins");
16
+ }
17
+ const plugin = plugins[0];
18
+ if (plugin === null || typeof plugin !== "object" || Array.isArray(plugin)) {
19
+ throw new Error("marketplace plugin");
20
+ }
21
+ const source = plugin.source;
22
+ if (plugin.name !== "superpowers" ||
23
+ source === null ||
24
+ typeof source !== "object" ||
25
+ Array.isArray(source) ||
26
+ source.source !== "local" ||
27
+ source.path !== "./plugins/superpowers") {
28
+ throw new Error("marketplace source");
29
+ }
30
+ }
31
+ async function requireEntries(root, expected) {
32
+ const actual = await readdir(root);
33
+ if (actual.length !== expected.length ||
34
+ actual.some((entry) => !expected.includes(entry))) {
35
+ throw new Error("tree entries");
36
+ }
37
+ }
38
+ async function readMarketplace(root) {
39
+ try {
40
+ await assertNoFollowType(root, ["directory"]);
41
+ const before = await lstat(root);
42
+ await requireEntries(root, [".agents", "plugins", ARTIFACT_RECEIPT]);
43
+ await assertNoFollowType(join(root, ".agents"), ["directory"]);
44
+ await assertNoFollowType(join(root, "plugins"), ["directory"]);
45
+ await assertNoFollowType(join(root, ARTIFACT_RECEIPT), ["regular-file"]);
46
+ await requireEntries(join(root, ".agents"), ["plugins"]);
47
+ await requireEntries(join(root, ".agents", "plugins"), [
48
+ "marketplace.json",
49
+ ]);
50
+ await requireEntries(join(root, "plugins"), ["superpowers"]);
51
+ const pluginRoot = join(root, PLUGIN_PATH);
52
+ await assertNoFollowType(pluginRoot, ["directory"]);
53
+ validateMarketplace(await readArtifactObject(root, join(root, MARKETPLACE_PATH)));
54
+ const receipt = await readArtifactObject(root, join(root, ARTIFACT_RECEIPT));
55
+ if (receipt.schema !== 1 ||
56
+ receipt.manager !== "superpowers-manager" ||
57
+ receipt.harness !== "codex" ||
58
+ receipt.generation !== "codex-marketplace-v1" ||
59
+ typeof receipt.digest !== "string" ||
60
+ !ARTIFACT_DIGEST_RE.test(receipt.digest)) {
61
+ throw new Error("receipt");
62
+ }
63
+ const digest = await digestArtifactTree(root);
64
+ if (digest !== receipt.digest)
65
+ throw new Error("digest");
66
+ const artifact = await readCodexAssessment(pluginRoot);
67
+ const after = await lstat(root);
68
+ if (before.dev !== after.dev || before.ino !== after.ino) {
69
+ throw new Error("root changed");
70
+ }
71
+ return {
72
+ root,
73
+ dev: before.dev,
74
+ ino: before.ino,
75
+ digest,
76
+ artifact,
77
+ };
78
+ }
79
+ catch (cause) {
80
+ throw inspectionError(root, cause);
81
+ }
82
+ }
83
+ async function removeOwnedCandidate(root, identity) {
84
+ try {
85
+ await assertNoFollowType(root, ["directory"]);
86
+ const current = await lstat(root);
87
+ if (current.dev !== identity.dev || current.ino !== identity.ino)
88
+ return;
89
+ await rm(root, { recursive: true });
90
+ }
91
+ catch {
92
+ // A candidate that cannot still be proven as ours must remain untouched.
93
+ }
94
+ }
95
+ export async function stageCodexMarketplace(artifact, packageRoot, candidateRoot) {
96
+ let candidateIdentity = null;
97
+ try {
98
+ const assessed = await readCodexAssessment(artifact.root);
99
+ if (assessed.commit !== artifact.commit)
100
+ throw new Error("artifact commit");
101
+ const sourceDigest = await digestArtifactTree(artifact.root);
102
+ await assertNoFollowType(candidateRoot, ["missing"]);
103
+ await mkdir(candidateRoot, { mode: 0o700 });
104
+ const candidate = await lstat(candidateRoot);
105
+ candidateIdentity = { dev: candidate.dev, ino: candidate.ino };
106
+ await mkdir(join(candidateRoot, ".agents", "plugins"), { recursive: true });
107
+ await mkdir(join(candidateRoot, "plugins"), { recursive: true });
108
+ await copyFile(join(packageRoot, MARKETPLACE_PATH), join(candidateRoot, MARKETPLACE_PATH), 0);
109
+ await cp(artifact.root, join(candidateRoot, PLUGIN_PATH), {
110
+ dereference: false,
111
+ errorOnExist: true,
112
+ force: false,
113
+ recursive: true,
114
+ verbatimSymlinks: true,
115
+ });
116
+ const copied = await readCodexAssessment(join(candidateRoot, PLUGIN_PATH));
117
+ if (copied.commit !== artifact.commit ||
118
+ (await digestArtifactTree(copied.root)) !== sourceDigest) {
119
+ throw new Error("copied artifact");
120
+ }
121
+ validateMarketplace(await readArtifactObject(candidateRoot, join(candidateRoot, MARKETPLACE_PATH)));
122
+ const receipt = {
123
+ schema: 1,
124
+ manager: "superpowers-manager",
125
+ harness: "codex",
126
+ generation: "codex-marketplace-v1",
127
+ digest: await digestArtifactTree(candidateRoot),
128
+ };
129
+ await writeFile(join(candidateRoot, ARTIFACT_RECEIPT), JSON.stringify(receipt) + "\n", { flag: "wx" });
130
+ return await readMarketplace(candidateRoot);
131
+ }
132
+ catch (cause) {
133
+ if (candidateIdentity !== null) {
134
+ await removeOwnedCandidate(candidateRoot, candidateIdentity);
135
+ }
136
+ throw new SafetyError("codex-marketplace", `cannot stage owned Codex marketplace: ${candidateRoot}`, { cause });
137
+ }
138
+ }
139
+ export async function readCodexMarketplace(root) {
140
+ try {
141
+ if ((await classifyPathNoFollow(root)) === "missing")
142
+ return null;
143
+ }
144
+ catch (cause) {
145
+ throw inspectionError(root, cause);
146
+ }
147
+ return await readMarketplace(root);
148
+ }
@@ -0,0 +1,97 @@
1
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
2
+ import { assertNoFollowType, canonicalizeProspectivePath, } from "../../safe-path.js";
3
+ import { SafetyError } from "../../safety-error.js";
4
+ export function codexHome(env, cwd) {
5
+ const configured = env.CODEX_HOME;
6
+ if (configured !== undefined && configured.length > 0) {
7
+ return resolve(cwd, configured);
8
+ }
9
+ const home = env.HOME;
10
+ if (home === undefined || home.length === 0) {
11
+ throw new SafetyError("codex-paths", "cannot determine Codex state root without HOME");
12
+ }
13
+ return join(resolve(cwd, home), ".codex");
14
+ }
15
+ export function codexPaths(env, cwd) {
16
+ const root = codexHome(env, cwd);
17
+ const managerRoot = join(root, "superpowers-manager");
18
+ const marketplaceRoot = join(managerRoot, "marketplace");
19
+ return {
20
+ codexHome: root,
21
+ managerRoot,
22
+ preparedRoot: join(managerRoot, "prepared"),
23
+ marketplaceRoot,
24
+ publishedPluginRoot: join(marketplaceRoot, "plugins", "superpowers"),
25
+ recoveryRoot: join(managerRoot, "recovery"),
26
+ };
27
+ }
28
+ function overlaps(a, b) {
29
+ const inside = (root, leaf) => {
30
+ const suffix = relative(root, leaf);
31
+ return (suffix === "" ||
32
+ (!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`)));
33
+ };
34
+ return inside(a, b) || inside(b, a);
35
+ }
36
+ function separationError() {
37
+ return new SafetyError("codex-paths", "preparation overlaps Codex published or recovery storage", { details: { kind: "overlap" } });
38
+ }
39
+ async function inspectRoot(path, root) {
40
+ try {
41
+ await assertNoFollowType(path, ["directory", "missing"]);
42
+ return await canonicalizeProspectivePath(path);
43
+ }
44
+ catch (cause) {
45
+ throw new SafetyError("codex-paths", `cannot inspect ${root} root: ${path}`, { cause, details: { kind: "inspection", root, path } });
46
+ }
47
+ }
48
+ export function codexPathFailureDetails(cause) {
49
+ if (!(cause instanceof SafetyError) || cause.module !== "codex-paths") {
50
+ return null;
51
+ }
52
+ const details = cause.details;
53
+ if (details === null || typeof details !== "object" || !("kind" in details)) {
54
+ return null;
55
+ }
56
+ if (details.kind === "overlap")
57
+ return { kind: "overlap" };
58
+ if (details.kind !== "inspection" ||
59
+ !("root" in details) ||
60
+ !("path" in details) ||
61
+ (details.root !== "preparation" &&
62
+ details.root !== "marketplace" &&
63
+ details.root !== "recovery") ||
64
+ typeof details.path !== "string") {
65
+ return null;
66
+ }
67
+ return { kind: "inspection", root: details.root, path: details.path };
68
+ }
69
+ export async function assertCodexPreparationSeparate(preparedRoot, paths) {
70
+ const recovery = await inspectRoot(paths.recoveryRoot, "recovery");
71
+ const marketplace = await inspectRoot(paths.marketplaceRoot, "marketplace");
72
+ let prepared;
73
+ try {
74
+ prepared = await inspectRoot(preparedRoot, "preparation");
75
+ }
76
+ catch (cause) {
77
+ const failure = codexPathFailureDetails(cause);
78
+ if (failure?.kind !== "inspection" || failure.root !== "preparation") {
79
+ throw cause;
80
+ }
81
+ try {
82
+ const canonical = await canonicalizeProspectivePath(preparedRoot);
83
+ if (overlaps(canonical, marketplace) || overlaps(canonical, recovery)) {
84
+ throw separationError();
85
+ }
86
+ }
87
+ catch (resolutionCause) {
88
+ if (codexPathFailureDetails(resolutionCause)?.kind === "overlap") {
89
+ throw resolutionCause;
90
+ }
91
+ }
92
+ throw cause;
93
+ }
94
+ if (overlaps(prepared, marketplace) || overlaps(prepared, recovery)) {
95
+ throw separationError();
96
+ }
97
+ }
@@ -1,14 +1,16 @@
1
1
  import { cp, mkdir, rm, stat } from "node:fs/promises";
2
2
  import { isAbsolute, join, resolve } from "node:path";
3
- import { failureResult, successResult, } from "./adapter-result.js";
3
+ import { failureResult, successResult, } from "../../adapter-result.js";
4
4
  import { codexBuild } from "./adapter.js";
5
- import { ARTIFACT_RECEIPT } from "./artifact-tree.js";
6
- import { assessCodexCompatibility, readCodexAssessment, writeCodexAssessment, } from "./codex-compatibility.js";
5
+ import { ARTIFACT_RECEIPT } from "../../artifact-tree.js";
6
+ import { assessCodexCompatibility, readCodexAssessment, writeCodexAssessment, } from "./compatibility.js";
7
7
  import { readManifest } from "./hooks.js";
8
- import { readGeneratedCommitLenient, readStrictProvenanceField, writeProvenance, } from "./provenance.js";
9
- import { classifyPathNoFollow } from "./safe-path.js";
10
- import { SafetyError } from "./safety-error.js";
11
- import { manifestVersionForRef } from "./upstream-version.js";
8
+ import { readGeneratedCommitLenient, readStrictProvenanceField, writeProvenance, } from "../../provenance.js";
9
+ import { classifyPathNoFollow } from "../../safe-path.js";
10
+ import { SafetyError } from "../../safety-error.js";
11
+ import { manifestVersionForRef } from "../../upstream-version.js";
12
+ import { assertCodexPreparationSeparate, codexPathFailureDetails, codexPaths, } from "./paths.js";
13
+ import { readCodexRecovery } from "./recovery.js";
12
14
  // Order is inherited from the original prepare command; the first miss wins.
13
15
  const REQUIRED_UPSTREAM = [
14
16
  { path: "skills", label: "skills/" },
@@ -75,7 +77,7 @@ async function copyPathIfPresent(source, destination) {
75
77
  await owned(`cannot clear candidate path: ${destination}`, () => rm(destination, { recursive: true, force: true }));
76
78
  await owned(`cannot copy upstream path into candidate: ${source}`, () => cp(source, destination, { recursive: true, verbatimSymlinks: true }));
77
79
  }
78
- export function resolveFromCwd(value, cwd) {
80
+ function resolveFromCwd(value, cwd) {
79
81
  return isAbsolute(value) ? value : resolve(cwd, value);
80
82
  }
81
83
  function manifestTemplate(ctx) {
@@ -86,11 +88,41 @@ function manifestTemplate(ctx) {
86
88
  export function codexPreparationLocation(ctx) {
87
89
  const env = ctx.env ?? {};
88
90
  return {
89
- destinationRoot: resolveFromCwd(env.SUPERPOWERS_PLUGIN_ROOT || join(ctx.root, "plugins", "superpowers"), process.cwd()),
91
+ destinationRoot: env.SUPERPOWERS_PLUGIN_ROOT !== undefined &&
92
+ env.SUPERPOWERS_PLUGIN_ROOT.length > 0
93
+ ? resolveFromCwd(env.SUPERPOWERS_PLUGIN_ROOT, process.cwd())
94
+ : codexPaths(env, process.cwd()).preparedRoot,
90
95
  stagingLeaf: "superpowers",
91
96
  };
92
97
  }
93
98
  export async function validateCodexPreparationBeforeFetch(ctx) {
99
+ const paths = codexPaths(ctx.env ?? {}, process.cwd());
100
+ const preparedRoot = codexPreparationLocation(ctx).destinationRoot;
101
+ try {
102
+ await assertCodexPreparationSeparate(preparedRoot, paths);
103
+ }
104
+ 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}`, [], []);
117
+ }
118
+ try {
119
+ if ((await readCodexRecovery(paths)) !== null) {
120
+ return failureResult("prepare", "recovery-required", `Codex recovery is required before preparation; preserve material at ${paths.recoveryRoot}`, [], []);
121
+ }
122
+ }
123
+ catch {
124
+ return failureResult("prepare", "recovery-required", `cannot inspect Codex recovery state at ${paths.recoveryRoot}`, [], []);
125
+ }
94
126
  const template = manifestTemplate(ctx);
95
127
  if (!(await regularFileExists(template))) {
96
128
  return failureResult("prepare", "missing-template", `missing fallback manifest template: ${template}`, [], []);
@@ -1,8 +1,8 @@
1
- import { normalizeCodexInstall, normalizeCodexInstalled, } from "./codex-harness.js";
2
- import { displaySource } from "./selection.js";
1
+ import { normalizeCodexInstall, normalizeCodexInstalled } from "./harness.js";
2
+ import { displaySource } from "../../selection.js";
3
3
  // One ordered table drives both formats, preserving the public human and
4
4
  // porcelain field order exposed through the harness presentation.
5
- export function fields(f) {
5
+ function fields(f) {
6
6
  return [
7
7
  { key: "harness", label: "harness", value: f.harness },
8
8
  { key: "requested_ref", label: "requested ref", value: f.requestedRef },
@@ -116,7 +116,7 @@ export function codexInstallReceipt(missingHint, mismatchHint) {
116
116
  mismatchVerificationOutput: verificationOutput("mismatch", mismatchHint),
117
117
  };
118
118
  }
119
- // Compatibility export target for src/lifecycle.ts. Delegate to the production
119
+ // Compatibility export target for src/harnesses/codex/lifecycle.ts. Delegate to the production
120
120
  // normalizers and renderer so the retained verdict cannot drift from commands.
121
121
  export function verifyInstalledFingerprint(desiredCommit, installResult, inspectResult) {
122
122
  const receipt = normalizeCodexInstall(installResult);
@@ -251,17 +251,18 @@ export const codexPresentation = {
251
251
  if (inspection.outcome.result.kind === "current") {
252
252
  return { stdout: [...stdout, "manager updated"], stderr: [] };
253
253
  }
254
+ const missing = inspection.outcome.result.kind === "absent" || observed.length === 0;
254
255
  if (receipt.status !== 0 || !receipt.outcome.ok) {
255
256
  return {
256
257
  stdout,
257
258
  stderr: [
258
- inspection.outcome.result.kind === "absent"
259
+ missing
259
260
  ? "error: installed manager fingerprint is not detectable after install."
260
261
  : "error: installed manager fingerprint does not match the prepared plugin after install.",
261
262
  ],
262
263
  };
263
264
  }
264
- const output = inspection.outcome.result.kind === "absent"
265
+ const output = missing
265
266
  ? receipt.outcome.result.missingVerificationOutput
266
267
  : receipt.outcome.result.mismatchVerificationOutput;
267
268
  return { stdout, stderr: output.stderr };