superpowers-manager 0.4.0 → 0.5.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.
package/dist/adapter.js CHANGED
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
6
6
  import { delimiter, isAbsolute, join } from "node:path";
7
7
  import { AdapterMessageLog, failureResult, successResult, } from "./adapter-result.js";
8
8
  import { activePluginVersionFromJson, installedListingHas, marketplaceRootFromJson, } from "./codex-json.js";
9
+ import { CODEX_LEGACY_PLUGIN_ID, CODEX_MANAGER_PLUGIN_ID, inspectCodexConflicts, } from "./codex-conflicts.js";
9
10
  import { oneLine } from "./cli-arguments.js";
10
11
  import { installedCommitFromRoot, installedRootForVersion, pathsEqual, } from "./codex-state.js";
11
12
  import { validateGeneratedPlugin } from "./generated-plugin.js";
@@ -14,9 +15,9 @@ import { applyManifestOverlay } from "./manifest-overlay.js";
14
15
  import { readCodexBuildSource } from "./provenance.js";
15
16
  import { isAcceptedSplitValue } from "./validate-generated-plugin-cli.js";
16
17
  import { withWorkspace, workspaceRemovalFailure } from "./workspace.js";
17
- const PLUGIN_ID = "superpowers@superpowers-manager";
18
+ const PLUGIN_ID = CODEX_MANAGER_PLUGIN_ID;
18
19
  const MARKETPLACE_NAME = "superpowers-manager";
19
- const LEGACY_PLUGIN_ID = "superpowers@superpowers-wrapper";
20
+ const LEGACY_PLUGIN_ID = CODEX_LEGACY_PLUGIN_ID;
20
21
  const LEGACY_MARKETPLACE_NAME = "superpowers-wrapper";
21
22
  class AdapterFailure extends Error {
22
23
  code;
@@ -486,7 +487,7 @@ async function runUninstall(input, env, log) {
486
487
  throw cause;
487
488
  }
488
489
  }
489
- async function runInspect(view, env, log) {
490
+ async function runInspect(view, context, env, log) {
490
491
  if (view === "update-control") {
491
492
  return { view: "update-control", update_control: "managed" };
492
493
  }
@@ -552,9 +553,11 @@ async function runInspect(view, env, log) {
552
553
  let legacyPlugin;
553
554
  let managerMarketplace;
554
555
  let legacyMarketplace;
556
+ let conflicts;
555
557
  try {
556
558
  managerPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", PLUGIN_ID);
557
559
  legacyPlugin = installedListingHas(plugins.stdout, "installed", "pluginId", LEGACY_PLUGIN_ID);
560
+ conflicts = await inspectCodexConflicts({ root: context.root, env }, plugins.stdout.toString("utf8"));
558
561
  }
559
562
  catch {
560
563
  fail("inspect-failed", `cannot parse output of '${codexBin} plugin list --json'`);
@@ -585,6 +588,7 @@ async function runInspect(view, env, log) {
585
588
  : legacyPresent
586
589
  ? "legacy"
587
590
  : "neither",
591
+ conflicts: [...conflicts],
588
592
  };
589
593
  }, { onCleanupFailure: reportOrphanedWorkspace(log) });
590
594
  }
@@ -622,7 +626,7 @@ export function codexRemove(input, context) {
622
626
  return runCodexOperation("uninstall", context, (env, log) => runUninstall(input, env, log));
623
627
  }
624
628
  export function codexInspect(view, context) {
625
- return runCodexOperation("inspect", context, (env, log) => runInspect(view, env, log));
629
+ return runCodexOperation("inspect", context, (env, log) => runInspect(view, context, env, log));
626
630
  }
627
631
  export async function runAdapter(argv, context) {
628
632
  const rawOperation = argv[0];
@@ -0,0 +1,110 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readdir, readFile, readlink } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { assertExistingContained, assertNoFollowType, assertSymlinkTargetContained, } from "./safe-path.js";
5
+ import { SafetyError } from "./safety-error.js";
6
+ import { parseStrictJson } from "./strict-json.js";
7
+ import { compareByCodePoint, DEFAULT_FS_DEPS, validateSkillFrontmatter, } from "./generated-plugin.js";
8
+ export const ARTIFACT_RECEIPT = ".superpowers-manager.json";
9
+ export const ARTIFACT_DIGEST_RE = /^[a-f0-9]{64}$/;
10
+ const DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
11
+ export function addArtifactHashField(hash, bytes) {
12
+ const length = Buffer.alloc(8);
13
+ length.writeBigUInt64BE(BigInt(bytes.byteLength));
14
+ hash.update(length);
15
+ hash.update(bytes);
16
+ }
17
+ export async function readArtifactFile(root, path, limit = 1024 * 1024) {
18
+ try {
19
+ await assertExistingContained(root, path);
20
+ await assertNoFollowType(path, ["regular-file"]);
21
+ if ((await lstat(path)).size > limit)
22
+ throw new Error("size");
23
+ const bytes = await readFile(path);
24
+ if (bytes.length > limit)
25
+ throw new Error("size");
26
+ return bytes;
27
+ }
28
+ catch (cause) {
29
+ throw new SafetyError("artifact", `cannot read bounded artifact file: ${path}`, { cause });
30
+ }
31
+ }
32
+ export async function readArtifactObject(root, path) {
33
+ try {
34
+ const value = parseStrictJson(await readArtifactFile(root, path), {
35
+ duplicateKeys: "reject",
36
+ nonStandardConstants: "reject",
37
+ maxDepth: 64,
38
+ });
39
+ if (value === null || typeof value !== "object" || Array.isArray(value))
40
+ throw new Error("object");
41
+ return value;
42
+ }
43
+ catch (cause) {
44
+ throw new SafetyError("artifact", `invalid artifact JSON object: ${path}`, {
45
+ cause,
46
+ });
47
+ }
48
+ }
49
+ export async function validateNativeSkill(root) {
50
+ const path = join(root, "skills", "using-superpowers", "SKILL.md");
51
+ await readArtifactFile(root, path);
52
+ const errors = [];
53
+ await validateSkillFrontmatter(path, "using-superpowers", errors, DEFAULT_FS_DEPS);
54
+ if (errors.length)
55
+ throw new SafetyError("artifact", `invalid native bootstrap skill: ${path}`);
56
+ }
57
+ export async function digestArtifactTree(root) {
58
+ try {
59
+ await assertNoFollowType(root, ["directory"]);
60
+ const paths = [];
61
+ async function visit(relative) {
62
+ for (const bytes of await readdir(join(root, relative), {
63
+ encoding: "buffer",
64
+ })) {
65
+ const name = DECODER.decode(bytes);
66
+ const path = relative ? `${relative}/${name}` : name;
67
+ if (path === ARTIFACT_RECEIPT)
68
+ continue;
69
+ paths.push(path);
70
+ if ((await lstat(join(root, path))).isDirectory())
71
+ await visit(path);
72
+ }
73
+ }
74
+ await visit("");
75
+ const hash = createHash("sha256");
76
+ for (const path of paths.sort(compareByCodePoint)) {
77
+ const absolute = join(root, path), info = await lstat(absolute);
78
+ let kind, contents;
79
+ if (info.isDirectory()) {
80
+ kind = "directory";
81
+ contents = Buffer.alloc(0);
82
+ }
83
+ else if (info.isFile()) {
84
+ kind = "file";
85
+ contents = await readFile(absolute);
86
+ }
87
+ else if (info.isSymbolicLink()) {
88
+ await assertSymlinkTargetContained(root, absolute);
89
+ kind = "link";
90
+ contents = await readlink(absolute, { encoding: "buffer" });
91
+ DECODER.decode(contents);
92
+ }
93
+ else
94
+ throw new Error("entry type");
95
+ for (const bytes of [
96
+ Buffer.from(kind),
97
+ Buffer.from(path),
98
+ Buffer.from(String(info.isFile() ? info.mode & 0o111 : 0)),
99
+ contents,
100
+ ])
101
+ addArtifactHashField(hash, bytes);
102
+ }
103
+ return hash.digest("hex");
104
+ }
105
+ catch (cause) {
106
+ throw new SafetyError("artifact", `cannot digest artifact tree: ${root}`, {
107
+ cause,
108
+ });
109
+ }
110
+ }
package/dist/atomic.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { constants, lstat, open, readFile, rename, rm, unlink, } from "node:fs/promises";
3
- import { basename, dirname, join } from "node:path";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
4
  import { SafetyError } from "./safety-error.js";
5
5
  export async function atomicWriteFile(path, bytes, options) {
6
6
  const renamePath = options.hooks?.rename ?? rename;
@@ -92,18 +92,52 @@ async function chooseBackup(live) {
92
92
  }
93
93
  throw new SafetyError("atomic", "cannot choose unique backup path");
94
94
  }
95
- export async function atomicReplaceDir(candidate, live, options = {}) {
95
+ async function validateRequestedBackup(backup, candidate, live) {
96
+ const resolvedBackup = resolve(backup);
97
+ const resolvedCandidate = resolve(candidate);
98
+ const resolvedLive = resolve(live);
99
+ const prefix = `.${basename(resolvedLive)}.bak.`;
100
+ if (dirname(resolvedBackup) !== dirname(resolvedLive) ||
101
+ !basename(resolvedBackup).startsWith(prefix) ||
102
+ basename(resolvedBackup).length === prefix.length ||
103
+ resolvedBackup === resolvedCandidate ||
104
+ resolvedBackup === resolvedLive ||
105
+ (await exists(backup))) {
106
+ throw new Error("invalid directory backup path");
107
+ }
108
+ }
109
+ async function directoryIdentity(path) {
110
+ const details = await lstat(path);
111
+ return { dev: details.dev, ino: details.ino };
112
+ }
113
+ function sameIdentity(left, right) {
114
+ return left.dev === right.dev && left.ino === right.ino;
115
+ }
116
+ function publicationFailure(message, cause) {
117
+ return new SafetyError("atomic", message, {
118
+ cause,
119
+ details: { phase: "post-replacement" },
120
+ });
121
+ }
122
+ export async function beginDirectoryPublication(candidate, live, options = {}) {
96
123
  const renamePath = options.hooks?.rename ?? rename;
97
124
  const removePath = options.hooks?.rm ?? rm;
98
125
  let backupCreated = false;
126
+ let backupIdentity = null;
99
127
  let phase = "pre-replacement";
100
128
  try {
101
- const backup = await chooseBackup(live);
129
+ const backup = options.backupPath ?? (await chooseBackup(live));
130
+ if (options.backupPath !== undefined) {
131
+ await validateRequestedBackup(backup, candidate, live);
132
+ }
102
133
  if (await exists(live)) {
134
+ backupIdentity = await directoryIdentity(live);
103
135
  await renamePath(live, backup);
104
136
  backupCreated = true;
105
137
  }
138
+ let publishedIdentity;
106
139
  try {
140
+ publishedIdentity = await directoryIdentity(candidate);
107
141
  await renamePath(candidate, live);
108
142
  phase = "post-replacement";
109
143
  }
@@ -125,14 +159,72 @@ export async function atomicReplaceDir(candidate, live, options = {}) {
125
159
  await removePath(candidate, { recursive: true, force: true });
126
160
  throw new SafetyError("atomic", "directory activation failed; previous tree restored", { cause, details: { phase: "pre-replacement" } });
127
161
  }
128
- if (backupCreated) {
162
+ const retainedBackup = backupCreated ? backup : null;
163
+ const retainedBackupIdentity = backupCreated ? backupIdentity : null;
164
+ let settled = false;
165
+ const claimSettlement = () => {
166
+ if (settled) {
167
+ throw publicationFailure("directory publication already settled", new Error("publication already settled"));
168
+ }
169
+ settled = true;
170
+ };
171
+ const requireRetainedBackup = async (action) => {
172
+ if (retainedBackup === null)
173
+ return;
174
+ let currentIdentity;
129
175
  try {
130
- await removePath(backup, { recursive: true, force: true });
176
+ currentIdentity = await directoryIdentity(retainedBackup);
131
177
  }
132
178
  catch (cause) {
133
- throw new SafetyError("atomic", `directory replacement succeeded but backup cleanup failed at ${backup}`, { cause, details: { phase: "post-replacement" } });
179
+ throw publicationFailure(`directory ${action} refused because backup changed unexpectedly at ${retainedBackup}`, cause);
134
180
  }
135
- }
181
+ if (retainedBackupIdentity === null ||
182
+ !sameIdentity(currentIdentity, retainedBackupIdentity)) {
183
+ throw publicationFailure(`directory ${action} refused because backup changed unexpectedly at ${retainedBackup}`, new Error("retained backup directory identity changed"));
184
+ }
185
+ };
186
+ return {
187
+ live,
188
+ backup: retainedBackup,
189
+ async finalize() {
190
+ claimSettlement();
191
+ if (retainedBackup === null)
192
+ return;
193
+ await requireRetainedBackup("finalization");
194
+ try {
195
+ await removePath(retainedBackup, { recursive: true, force: true });
196
+ }
197
+ catch (cause) {
198
+ throw publicationFailure(`directory replacement succeeded but backup cleanup failed at ${retainedBackup}`, cause);
199
+ }
200
+ },
201
+ async rollback() {
202
+ claimSettlement();
203
+ await requireRetainedBackup("rollback");
204
+ let currentIdentity;
205
+ try {
206
+ currentIdentity = await directoryIdentity(live);
207
+ }
208
+ catch (cause) {
209
+ throw publicationFailure(`directory rollback refused because live tree changed unexpectedly at ${live}`, cause);
210
+ }
211
+ if (!sameIdentity(currentIdentity, publishedIdentity)) {
212
+ throw publicationFailure(`directory rollback refused because live tree changed unexpectedly at ${live}`, new Error("published directory identity changed"));
213
+ }
214
+ try {
215
+ await removePath(live, { recursive: true, force: true });
216
+ if (retainedBackup !== null) {
217
+ await renamePath(retainedBackup, live);
218
+ }
219
+ }
220
+ catch (cause) {
221
+ const message = retainedBackup === null
222
+ ? "directory rollback failed with no prior tree"
223
+ : `directory rollback failed; backup preserved at ${retainedBackup}`;
224
+ throw publicationFailure(message, cause);
225
+ }
226
+ },
227
+ };
136
228
  }
137
229
  catch (cause) {
138
230
  if (cause instanceof SafetyError)
@@ -140,3 +232,7 @@ export async function atomicReplaceDir(candidate, live, options = {}) {
140
232
  throw new SafetyError("atomic", "directory replacement failed", { cause, details: { phase } });
141
233
  }
142
234
  }
235
+ export async function atomicReplaceDir(candidate, live, options = {}) {
236
+ const pending = await beginDirectoryPublication(candidate, live, options);
237
+ await pending.finalize();
238
+ }
@@ -1,5 +1,42 @@
1
1
  export class UsageError extends Error {
2
2
  }
3
+ export function extractHarnessOptions(command, argv) {
4
+ let harness = "codex";
5
+ let targeted = false;
6
+ let allowExperimental = false;
7
+ const args = [];
8
+ for (let index = 0; index < argv.length; index += 1) {
9
+ const token = argv[index];
10
+ if (token === "--harness" || token.startsWith("--harness=")) {
11
+ if (command === "pin" ||
12
+ command === "unpin" ||
13
+ command === "track-latest")
14
+ throw new UsageError("harness targeting is not available for shared selection commands");
15
+ if (targeted)
16
+ throw new UsageError("duplicate option: --harness");
17
+ targeted = true;
18
+ const value = token === "--harness"
19
+ ? argv[++index]
20
+ : token.slice("--harness=".length);
21
+ if (!value || value.startsWith("--"))
22
+ throw new UsageError("option --harness requires a value");
23
+ if (value !== "codex" && value !== "pi")
24
+ throw new UsageError(`unknown harness: ${value}`);
25
+ harness = value;
26
+ }
27
+ else if (token === "--allow-experimental") {
28
+ if (command !== "install" && command !== "update")
29
+ throw new UsageError("--allow-experimental is only available for install or update");
30
+ if (allowExperimental)
31
+ throw new UsageError("duplicate option: --allow-experimental");
32
+ allowExperimental = true;
33
+ }
34
+ else {
35
+ args.push(token);
36
+ }
37
+ }
38
+ return { options: { harness, allowExperimental }, args };
39
+ }
3
40
  export function parseFlags(argv, names) {
4
41
  const allowed = new Set(names);
5
42
  const result = {};
package/dist/cli.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { oneLine } from "./cli-arguments.js";
4
+ import { extractHarnessOptions, oneLine, UsageError } from "./cli-arguments.js";
5
5
  import { codexHarness } from "./codex-harness.js";
6
+ import { piHarness } from "./pi-harness.js";
7
+ import { createResourceCoordinator } from "./resource-lock.js";
6
8
  import { runInstall } from "./commands/install.js";
7
9
  import { runPin } from "./commands/pin.js";
8
10
  import { runPrepare } from "./commands/prepare.js";
@@ -85,14 +87,28 @@ function isMain(moduleFilename, argvPath) {
85
87
  function parseArgs(argv) {
86
88
  const first = argv[0];
87
89
  if (argv.length === 0)
88
- return { kind: "run", cmd: "update", args: [] };
90
+ return {
91
+ kind: "run",
92
+ cmd: "update",
93
+ args: [],
94
+ options: { harness: "codex", allowExperimental: false },
95
+ };
89
96
  if (first === "--help" || first === "-h")
90
97
  return { kind: "help" };
91
98
  if (first === "--version")
92
99
  return { kind: "version" };
93
100
  if (first && SUBCOMMANDS.includes(first)) {
94
101
  const command = first;
95
- const args = argv.slice(1);
102
+ let extracted;
103
+ try {
104
+ extracted = extractHarnessOptions(command, argv.slice(1));
105
+ }
106
+ catch (cause) {
107
+ if (cause instanceof UsageError)
108
+ return { kind: "usage-error", message: cause.message };
109
+ throw cause;
110
+ }
111
+ const { args, options } = extracted;
96
112
  if (command === "pin" && args.length !== 1) {
97
113
  return {
98
114
  kind: "usage-error",
@@ -128,7 +144,7 @@ function parseArgs(argv) {
128
144
  message: "usage: superpowers-manager probe [--porcelain]",
129
145
  };
130
146
  }
131
- return { kind: "run", cmd: command, args };
147
+ return { kind: "run", cmd: command, args, options };
132
148
  }
133
149
  return { kind: "usage-error", message: `unknown subcommand: ${first}` };
134
150
  }
@@ -232,6 +248,14 @@ function usage() {
232
248
  " update probe, then prepare/install only if needed (default when no subcommand)",
233
249
  " uninstall remove the manager plugin and marketplace from Codex",
234
250
  "",
251
+ "Target one invocation (default: codex; selection commands are shared):",
252
+ " probe --harness pi [--porcelain]",
253
+ " prepare --harness pi",
254
+ " install --harness pi [--allow-experimental]",
255
+ " update --harness pi [--allow-experimental]",
256
+ " uninstall --harness pi",
257
+ " --harness=codex and --harness=pi are also accepted after the command.",
258
+ "",
235
259
  "Environment overrides (used by in-process commands): SUPERPOWERS_REF,",
236
260
  "SUPERPOWERS_UPSTREAM_URL, SUPERPOWERS_CODEX, SUPERPOWERS_CACHE_DIR,",
237
261
  "SUPERPOWERS_CONFIG_DIR, XDG_CONFIG_HOME,",
@@ -264,11 +288,22 @@ async function main() {
264
288
  console.error(usage());
265
289
  process.exit(2);
266
290
  }
267
- const pf = preflight(parsed.cmd, process.env, process.platform);
291
+ const status = parsed.options.harness === "pi"
292
+ ? await dispatch(piHarness, parsed)
293
+ : await dispatch(codexHarness, parsed);
294
+ process.exit(status);
295
+ }
296
+ async function dispatch(adapter, parsed) {
297
+ const root = resolvePackageRoot(import.meta.filename);
298
+ if (!root) {
299
+ console.error("error: cannot resolve the superpowers-manager package root");
300
+ return 1;
301
+ }
302
+ const pf = preflightFor(parsed.cmd, process.env, process.platform, adapter);
268
303
  if (!pf.ok) {
269
304
  for (const e of pf.errors)
270
305
  console.error(`error: ${e}`);
271
- process.exit(1);
306
+ return 1;
272
307
  }
273
308
  // Dispatch is no longer a branch: slice 4b flipped the last spawned command,
274
309
  // so every subcommand runs here, and slice 6 deleted the DISPATCH table whose
@@ -281,15 +316,17 @@ async function main() {
281
316
  const handler = IN_PROCESS_HANDLERS[parsed.cmd];
282
317
  if (!handler) {
283
318
  console.error(`error: no in-process handler registered for: ${parsed.cmd}`);
284
- process.exit(1);
319
+ return 1;
285
320
  }
286
321
  const ctx = {
287
322
  root,
288
323
  env: process.env,
289
324
  stdout: process.stdout,
290
325
  stderr: process.stderr,
326
+ options: parsed.options,
327
+ coordination: createResourceCoordinator(),
291
328
  // The ONLY production binding of a concrete harness implementation.
292
- adapter: codexHarness,
329
+ adapter,
293
330
  };
294
331
  let status;
295
332
  try {
@@ -300,11 +337,11 @@ async function main() {
300
337
  // failures and returns a status code (see src/commands/unpin.ts). This
301
338
  // re-emits a subordinate module's own diagnostic if one somehow escapes.
302
339
  console.error(`error: ${oneLine(cause)}`);
303
- process.exit(1);
340
+ return 1;
304
341
  }
305
- process.exit(status);
342
+ return status;
306
343
  }
307
- export { resolvePackageRoot, isMain, parseArgs, findTool, commandRequirements, commandRequirementsFor, preflight, preflightFor, usage, main, };
344
+ export { resolvePackageRoot, isMain, parseArgs, findTool, commandRequirements, commandRequirementsFor, preflight, preflightFor, usage, dispatch, main, };
308
345
  if (isMain(import.meta.filename, process.argv[1])) {
309
346
  main().catch((cause) => {
310
347
  console.error(`error: ${oneLine(cause)}`);
@@ -0,0 +1,126 @@
1
+ import { join } from "node:path";
2
+ import { validateNativeSkill } from "./artifact-tree.js";
3
+ import { classifyPathNoFollow, assertExistingContained } from "./safe-path.js";
4
+ import { classifyHooks, readManifest } from "./hooks.js";
5
+ import { SEMVER_RE } from "./generated-plugin.js";
6
+ import { validateSource } from "./selection.js";
7
+ import { writeFile } from "node:fs/promises";
8
+ import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, } from "./artifact-tree.js";
9
+ import { COMMIT_RE } from "./domain/refs.js";
10
+ import { validateGeneratedPlugin } from "./generated-plugin.js";
11
+ import { SafetyError } from "./safety-error.js";
12
+ export async function writeCodexAssessment(root, selection, manifestSource) {
13
+ const receipt = {
14
+ schema: 1,
15
+ manager: "superpowers-manager",
16
+ harness: "codex",
17
+ source: selection.effectiveSource,
18
+ commit: selection.desiredCommit,
19
+ generation: "codex-native-skills-v1",
20
+ manifestSource,
21
+ digest: await digestArtifactTree(root),
22
+ };
23
+ await writeFile(join(root, ARTIFACT_RECEIPT), JSON.stringify(receipt) + "\n", { flag: "wx" });
24
+ }
25
+ export async function readCodexAssessment(root) {
26
+ try {
27
+ const receipt = await readArtifactObject(root, join(root, ARTIFACT_RECEIPT));
28
+ const provenance = await readArtifactObject(root, join(root, ".superpowers-upstream.json"));
29
+ if (receipt.schema !== 1 ||
30
+ receipt.manager !== "superpowers-manager" ||
31
+ receipt.harness !== "codex" ||
32
+ receipt.generation !== "codex-native-skills-v1" ||
33
+ typeof receipt.source !== "string" ||
34
+ typeof receipt.commit !== "string" ||
35
+ !COMMIT_RE.test(receipt.commit) ||
36
+ typeof receipt.digest !== "string" ||
37
+ !ARTIFACT_DIGEST_RE.test(receipt.digest) ||
38
+ (receipt.manifestSource !== "upstream" &&
39
+ receipt.manifestSource !== "fallback") ||
40
+ receipt.source !== provenance.source ||
41
+ receipt.commit !== provenance.commit)
42
+ throw new Error("assessment fields");
43
+ validateSource(receipt.source);
44
+ if ((await digestArtifactTree(root)) !== receipt.digest)
45
+ throw new Error("assessment digest");
46
+ const manifest = await readManifest(join(root, ".codex-plugin/plugin.json"));
47
+ if (typeof manifest.version !== "string" ||
48
+ typeof provenance.requested_ref !== "string" ||
49
+ typeof provenance.resolved_ref !== "string" ||
50
+ typeof provenance.upstream_manifest_version !== "string")
51
+ throw new Error("provenance fields");
52
+ const errors = await validateGeneratedPlugin({
53
+ pluginRoot: root,
54
+ source: receipt.source,
55
+ commit: receipt.commit,
56
+ requestedRef: provenance.requested_ref,
57
+ resolvedRef: provenance.resolved_ref,
58
+ upstreamManifestVersion: provenance.upstream_manifest_version,
59
+ manifestVersion: manifest.version,
60
+ manifestSource: receipt.manifestSource,
61
+ });
62
+ if (errors.length)
63
+ throw new Error("generated resources");
64
+ const selection = {
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);
82
+ return {
83
+ root,
84
+ commit: receipt.commit,
85
+ identity: receipt.commit,
86
+ compatibility,
87
+ };
88
+ }
89
+ catch (cause) {
90
+ throw new SafetyError("codex-compatibility", `invalid Codex prepared assessment: ${root}`, { cause });
91
+ }
92
+ }
93
+ export async function assessCodexCompatibility(root, selection) {
94
+ try {
95
+ validateSource(selection.effectiveSource);
96
+ await validateNativeSkill(root);
97
+ for (const path of [
98
+ ".codex/superpowers-codex",
99
+ ".codex/superpowers-bootstrap.md",
100
+ ]) {
101
+ if ((await classifyPathNoFollow(join(root, path))) !== "missing")
102
+ throw new Error("legacy bootstrap");
103
+ }
104
+ const path = join(root, ".codex-plugin/plugin.json");
105
+ if ((await classifyPathNoFollow(path)) !== "missing") {
106
+ await assertExistingContained(root, path);
107
+ const manifest = await readManifest(path);
108
+ if (manifest.name !== "superpowers" ||
109
+ typeof manifest.version !== "string" ||
110
+ !SEMVER_RE.test(manifest.version))
111
+ throw new Error("manifest");
112
+ await classifyHooks(manifest, "upstream", root);
113
+ }
114
+ return {
115
+ kind: "supported",
116
+ generation: "codex-native-skills-v1",
117
+ reason: "native Codex skill discovery",
118
+ };
119
+ }
120
+ catch {
121
+ return {
122
+ kind: "unsupported",
123
+ reason: `Codex package does not provide compatible native skill discovery: ${root}`,
124
+ };
125
+ }
126
+ }