superpowers-manager 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +36 -16
  2. package/dist/atomic.js +1 -3
  3. package/dist/cli-arguments.js +4 -1
  4. package/dist/cli.js +14 -43
  5. package/dist/commands/adapter-call.js +24 -0
  6. package/dist/commands/install.js +16 -63
  7. package/dist/commands/mutation.js +5 -5
  8. package/dist/commands/pin.js +10 -5
  9. package/dist/commands/prepare.js +42 -108
  10. package/dist/commands/probe.js +33 -19
  11. package/dist/commands/uninstall.js +18 -46
  12. package/dist/commands/unpin.js +2 -1
  13. package/dist/commands/update.js +8 -29
  14. package/dist/domain/refs.js +0 -3
  15. package/dist/effective-selection.js +11 -15
  16. package/dist/git-tree.js +4 -6
  17. package/dist/harness-command-result.js +36 -5
  18. package/dist/harnesses/claude-code/harness.js +39 -0
  19. package/dist/harnesses/claude-code/install.js +380 -0
  20. package/dist/harnesses/claude-code/native.js +79 -0
  21. package/dist/harnesses/claude-code/paths.js +35 -0
  22. package/dist/harnesses/claude-code/prepare.js +117 -0
  23. package/dist/harnesses/claude-code/presentation.js +25 -0
  24. package/dist/harnesses/claude-code/state.js +253 -0
  25. package/dist/harnesses/codex/adapter.js +27 -68
  26. package/dist/harnesses/codex/conflicts.js +3 -8
  27. package/dist/harnesses/codex/generated-plugin.js +4 -11
  28. package/dist/harnesses/codex/harness.js +4 -26
  29. package/dist/harnesses/codex/hooks.js +11 -27
  30. package/dist/harnesses/codex/lifecycle.js +22 -107
  31. package/dist/harnesses/codex/manifest-overlay.js +9 -27
  32. package/dist/harnesses/codex/paths.js +29 -0
  33. package/dist/harnesses/codex/prepare.js +9 -43
  34. package/dist/harnesses/codex/presentation.js +1 -1
  35. package/dist/harnesses/codex/publication.js +5 -40
  36. package/dist/harnesses/codex/state.js +1 -6
  37. package/dist/harnesses/codex/stored-state.js +1 -6
  38. package/dist/harnesses/opencode/config.js +105 -50
  39. package/dist/harnesses/opencode/discovery.js +11 -15
  40. package/dist/harnesses/opencode/install.js +57 -102
  41. package/dist/harnesses/opencode/native.js +28 -49
  42. package/dist/harnesses/opencode/package.js +45 -67
  43. package/dist/harnesses/opencode/prepare.js +2 -7
  44. package/dist/harnesses/opencode/presentation.js +3 -7
  45. package/dist/harnesses/opencode/state.js +1 -0
  46. package/dist/harnesses/pi/install.js +31 -120
  47. package/dist/harnesses/pi/native.js +9 -34
  48. package/dist/harnesses/pi/package.js +9 -50
  49. package/dist/harnesses/pi/prepare.js +3 -8
  50. package/dist/harnesses/pi/presentation.js +2 -7
  51. package/dist/harnesses/pi/settings.js +5 -9
  52. package/dist/harnesses/pi/shared-skills.js +23 -53
  53. package/dist/harnesses/pi/state.js +3 -10
  54. package/dist/provenance.js +3 -19
  55. package/dist/resource-lock.js +11 -16
  56. package/dist/safe-path.js +28 -1
  57. package/dist/selection.js +15 -19
  58. package/dist/snapshot-journal.js +101 -0
  59. package/dist/snapshot-package.js +62 -1
  60. package/dist/snapshot-prepare.js +9 -2
  61. package/dist/snapshot-presentation.js +9 -0
  62. package/dist/upstream.js +8 -12
  63. package/dist/validator.js +26 -23
  64. package/dist/workspace.js +29 -35
  65. package/package.json +10 -9
  66. package/dist/python-json.js +0 -4
  67. package/dist/upstream-workspace.js +0 -8
@@ -0,0 +1,117 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { applyEdits, modify } from "jsonc-parser";
4
+ import { failureResult, successResult, } from "../../adapter-result.js";
5
+ import { readArtifactFile, readArtifactObject, validateNativeSkill, } from "../../artifact-tree.js";
6
+ import { SEMVER_RE } from "../../domain/refs.js";
7
+ import { classifyPathNoFollow } from "../../safe-path.js";
8
+ import { SafetyError } from "../../safety-error.js";
9
+ import { validateSource } from "../../selection.js";
10
+ import { createSnapshotReceipts, } from "../../snapshot-package.js";
11
+ import { createSnapshotPreparation } from "../../snapshot-prepare.js";
12
+ import { manifestVersionForRef } from "../../upstream-version.js";
13
+ import { assertClaudeCodeStorageSafe, claudeCodePaths } from "./paths.js";
14
+ const CLAUDE_CODE_MANIFEST = ".claude-plugin/plugin.json";
15
+ const GENERATION = "claude-code-native-plugin-v1";
16
+ async function assessClaudeCodeCompatibility(root, selection) {
17
+ try {
18
+ validateSource(selection.effectiveSource);
19
+ const manifest = await readArtifactObject(root, join(root, CLAUDE_CODE_MANIFEST));
20
+ if (manifest.name !== "superpowers" ||
21
+ typeof manifest.version !== "string" ||
22
+ !SEMVER_RE.test(manifest.version))
23
+ throw new Error("manifest");
24
+ await validateNativeSkill(root);
25
+ const hooks = join(root, "hooks", "hooks.json");
26
+ if ((await classifyPathNoFollow(hooks)) !== "missing")
27
+ await readArtifactObject(root, hooks);
28
+ return {
29
+ kind: "supported",
30
+ generation: GENERATION,
31
+ reason: "native Claude Code plugin",
32
+ };
33
+ }
34
+ catch {
35
+ return {
36
+ kind: "unsupported",
37
+ reason: `Claude Code package does not provide a native Claude Code plugin: ${root}`,
38
+ };
39
+ }
40
+ }
41
+ // The ref-aware manifest version for this selection. Two selections of the
42
+ // same commit (for example a tag and the raw SHA) yield different versions.
43
+ export function expectedClaudeCodeVersion(selection) {
44
+ return manifestVersionForRef({
45
+ requestedRef: selection.requestedRef,
46
+ resolutionKind: selection.resolutionKind,
47
+ resolvedRef: selection.resolvedRef,
48
+ commit: selection.desiredCommit,
49
+ });
50
+ }
51
+ // A targeted edit of the one value: every other upstream byte, including
52
+ // formatting and unknown fields, is preserved.
53
+ async function rewriteClaudeCodeVersion(root, selection) {
54
+ const version = expectedClaudeCodeVersion(selection);
55
+ const path = join(root, CLAUDE_CODE_MANIFEST);
56
+ await readArtifactObject(root, path);
57
+ const text = (await readArtifactFile(root, path)).toString("utf8");
58
+ await writeFile(path, applyEdits(text, modify(text, ["version"], version, {})));
59
+ }
60
+ const receipts = createSnapshotReceipts({
61
+ harness: "claude-code",
62
+ label: "Claude Code",
63
+ strictGeneration: true,
64
+ assessCompatibility: assessClaudeCodeCompatibility,
65
+ });
66
+ export const readClaudeCodeReceipt = receipts.readReceipt;
67
+ export const readClaudeCodePackageAssessment = receipts.readAssessment;
68
+ export async function readClaudeCodeManifestVersion(root) {
69
+ const manifest = await readArtifactObject(root, join(root, CLAUDE_CODE_MANIFEST));
70
+ if (typeof manifest.version !== "string")
71
+ throw new SafetyError("claude-code-package", `Claude Code manifest has no version: ${root}`);
72
+ return manifest.version;
73
+ }
74
+ export async function validateClaudeCodePreparationBeforeFetch(ctx) {
75
+ try {
76
+ await assertClaudeCodeStorageSafe(claudeCodePaths(ctx.env ?? {}, process.cwd()));
77
+ return successResult("prepare", null, []);
78
+ }
79
+ catch {
80
+ return failureResult("prepare", "invalid-package", "cannot validate Claude Code artifact storage", [], []);
81
+ }
82
+ }
83
+ const preparation = createSnapshotPreparation({
84
+ harness: "claude-code",
85
+ label: "Claude Code",
86
+ paths: claudeCodePaths,
87
+ assessCompatibility: assessClaudeCodeCompatibility,
88
+ readAssessment: readClaudeCodePackageAssessment,
89
+ rewrite: rewriteClaudeCodeVersion,
90
+ });
91
+ export const claudeCodePreparationLocation = preparation.preparationLocation;
92
+ export const prepareClaudeCodeCandidate = preparation.prepareCandidate;
93
+ // The shared check compares only commit and source, so a same-commit ref
94
+ // change would otherwise leave a prepared tree carrying the old version.
95
+ export async function inspectClaudeCodePrepared(selection, ctx) {
96
+ const result = await preparation.inspectPrepared(selection, ctx);
97
+ if (!result.outcome.ok || result.outcome.result.kind !== "current")
98
+ return result;
99
+ const root = result.outcome.result.artifact.root;
100
+ try {
101
+ if ((await readClaudeCodeManifestVersion(root)) ===
102
+ expectedClaudeCodeVersion(selection))
103
+ return result;
104
+ }
105
+ catch {
106
+ return failureResult("inspect-prepared", "invalid-package", `cannot inspect Claude Code prepared artifact: ${root}`, [], []);
107
+ }
108
+ return successResult("inspect-prepared", {
109
+ kind: "needs-prepare",
110
+ observedIdentity: result.outcome.result.observedIdentity,
111
+ compatibility: {
112
+ kind: "unknown",
113
+ reason: "Claude Code prepared version does not match the selection",
114
+ },
115
+ }, []);
116
+ }
117
+ export const readClaudeCodePrepared = preparation.readPrepared;
@@ -0,0 +1,25 @@
1
+ import { displaySource } from "../../selection.js";
2
+ import { createSnapshotPresentation, probeOutput, } from "../../snapshot-presentation.js";
3
+ import { displayPath } from "../../validator.js";
4
+ const renderProbe = (facts) => {
5
+ const fields = [
6
+ ["harness", "claude-code"],
7
+ ["desired_commit", facts.selection.desiredCommit],
8
+ ["upstream_source_origin", facts.selection.upstreamSourceOrigin],
9
+ ["effective_source", displaySource(facts.selection.effectiveSource)],
10
+ ["prepared_identity", facts.prepared.observedIdentity],
11
+ ["installed_identity", facts.installed.observedIdentity],
12
+ ["installation_state", facts.installed.kind],
13
+ ["resource_state", facts.resourceState ?? "idle"],
14
+ ["ownership", facts.ownership.presentationValue],
15
+ ["conflicts", (facts.ownership.presentationConflicts ?? []).join("; ")],
16
+ ["update_control", facts.control.presentationValue],
17
+ ["compatibility", facts.compatibility.kind],
18
+ ["compatibility_reason", facts.compatibility.reason],
19
+ ["status", facts.status],
20
+ ].map(([key, value]) => [key, displayPath(value)]);
21
+ return probeOutput(fields);
22
+ };
23
+ export const claudeCodePresentation = createSnapshotPresentation("Claude Code", renderProbe, (input) => input.registration === "absent" &&
24
+ !input.pluginInstalled &&
25
+ !input.published);
@@ -0,0 +1,253 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { readArtifactFile } from "../../artifact-tree.js";
4
+ import { inspectionFailure, successResult, } from "../../adapter-result.js";
5
+ import { canonicalizeProspectivePath, classifyPathNoFollow, } from "../../safe-path.js";
6
+ import { observeSnapshot, sameSnapshotSource, } from "../../snapshot-package.js";
7
+ import { displayPath } from "../../validator.js";
8
+ import { readClaudeCodeState, runClaude, } from "./native.js";
9
+ import { assertClaudeCodeStorageSafe, claudeCodePaths, } from "./paths.js";
10
+ import { expectedClaudeCodeVersion, readClaudeCodeManifestVersion, readClaudeCodePackageAssessment, readClaudeCodeReceipt, } from "./prepare.js";
11
+ export const CLAUDE_CODE_MARKETPLACE = "superpowers-manager";
12
+ export const CLAUDE_CODE_PLUGIN_ID = "superpowers@superpowers-manager";
13
+ export const CLAUDE_CODE_MARKETPLACE_BYTES = Buffer.from(JSON.stringify({
14
+ name: CLAUDE_CODE_MARKETPLACE,
15
+ owner: { name: CLAUDE_CODE_MARKETPLACE },
16
+ plugins: [{ name: "superpowers", source: "./plugins/superpowers" }],
17
+ }) + "\n");
18
+ const SYNCED_ID = "superpowers@synced";
19
+ const DISPLAYABLE_ID = /^superpowers@[A-Za-z0-9][A-Za-z0-9._-]{0,200}$/;
20
+ function blocked(...stderr) {
21
+ return {
22
+ kind: "blocked",
23
+ output: { stdout: [], stderr: stderr.map((line) => displayPath(line)) },
24
+ };
25
+ }
26
+ function pathsFor(ctx) {
27
+ return claudeCodePaths(ctx.env ?? {}, process.cwd());
28
+ }
29
+ export async function marketplaceRegistration(native, paths) {
30
+ const entries = native.marketplaces.filter((entry) => entry.name === CLAUDE_CODE_MARKETPLACE);
31
+ if (entries.length === 0)
32
+ return "absent";
33
+ if (entries.length > 1)
34
+ throw new Error("ambiguous Claude Code marketplace");
35
+ const entry = entries[0];
36
+ if (entry.source !== "directory" || entry.path === null)
37
+ return "foreign";
38
+ return (await canonicalizeProspectivePath(entry.path)) ===
39
+ (await canonicalizeProspectivePath(paths.marketplaceRoot))
40
+ ? "owned"
41
+ : "foreign";
42
+ }
43
+ export function managerPlugin(native) {
44
+ const entries = native.plugins.filter((plugin) => plugin.id === CLAUDE_CODE_PLUGIN_ID && plugin.scope === "user");
45
+ if (entries.length > 1)
46
+ throw new Error("ambiguous Claude Code plugin");
47
+ return entries[0];
48
+ }
49
+ // An unregistered directory is removable only when every entry is part of
50
+ // the Manager's marketplace layout. Empty known directories are safe crash
51
+ // residue; a present snapshot must have a valid Manager receipt and digest.
52
+ export async function inspectClaudeCodeMarketplaceStorage(paths) {
53
+ const rootKind = await classifyPathNoFollow(paths.marketplaceRoot);
54
+ if (rootKind === "missing")
55
+ return "absent";
56
+ if (rootKind !== "directory")
57
+ return "unverified";
58
+ try {
59
+ await assertClaudeCodeStorageSafe(paths);
60
+ // Probe must present recovery before judging a crash leftover's contents.
61
+ // Mutation paths still refuse these siblings before any native call.
62
+ if ((await leftoverPublicationMaterial(paths)).length > 0)
63
+ return "recovery";
64
+ const known = (entries, allowed) => entries.every((entry) => allowed.includes(entry));
65
+ if (!known(await readdir(paths.marketplaceRoot), [
66
+ ".claude-plugin",
67
+ "plugins",
68
+ ]))
69
+ return "unverified";
70
+ const metadataRoot = join(paths.marketplaceRoot, ".claude-plugin");
71
+ if ((await classifyPathNoFollow(metadataRoot)) !== "missing") {
72
+ if (!known(await readdir(metadataRoot), ["marketplace.json"]))
73
+ return "unverified";
74
+ }
75
+ const manifestKind = await classifyPathNoFollow(paths.marketplaceManifest);
76
+ if (manifestKind !== "missing") {
77
+ if (manifestKind !== "regular-file" ||
78
+ !(await readArtifactFile(paths.marketplaceRoot, paths.marketplaceManifest, CLAUDE_CODE_MARKETPLACE_BYTES.length)).equals(CLAUDE_CODE_MARKETPLACE_BYTES))
79
+ return "unverified";
80
+ }
81
+ if ((await classifyPathNoFollow(paths.pluginsRoot)) !== "missing") {
82
+ if (!known(await readdir(paths.pluginsRoot), ["superpowers"]))
83
+ return "unverified";
84
+ if ((await classifyPathNoFollow(paths.pluginRoot)) !== "missing") {
85
+ const snapshot = await observeSnapshot(paths.pluginRoot, readClaudeCodeReceipt);
86
+ if (snapshot.kind !== "owned")
87
+ return "unverified";
88
+ }
89
+ }
90
+ return "owned";
91
+ }
92
+ catch {
93
+ return "unverified";
94
+ }
95
+ }
96
+ // A synced copy is reported as not loaded whenever another origin provides the
97
+ // same name, so it never duplicates the Manager plugin.
98
+ function conflicts(native) {
99
+ return native.plugins
100
+ .filter((plugin) => plugin.enabled &&
101
+ plugin.id.startsWith("superpowers@") &&
102
+ plugin.id !== CLAUDE_CODE_PLUGIN_ID &&
103
+ plugin.id !== SYNCED_ID)
104
+ .map((plugin) => DISPLAYABLE_ID.test(plugin.id)
105
+ ? plugin.id
106
+ : "a Claude Code plugin with a non-displayable Superpowers identity");
107
+ }
108
+ async function observeFacts(ctx, run) {
109
+ const paths = pathsFor(ctx);
110
+ await assertClaudeCodeStorageSafe(paths);
111
+ const native = await readClaudeCodeState(ctx, run);
112
+ const [registration, snapshot, marketplaceKind] = await Promise.all([
113
+ marketplaceRegistration(native, paths),
114
+ observeSnapshot(paths.pluginRoot, readClaudeCodeReceipt),
115
+ classifyPathNoFollow(paths.marketplaceRoot),
116
+ ]);
117
+ if (registration === "absent" &&
118
+ marketplaceKind !== "missing" &&
119
+ (await inspectClaudeCodeMarketplaceStorage(paths)) === "unverified")
120
+ throw new Error("unverified Claude Code marketplace storage");
121
+ return {
122
+ paths,
123
+ registration,
124
+ marketplaceSource: native.marketplaces.find((entry) => entry.name === CLAUDE_CODE_MARKETPLACE),
125
+ plugin: managerPlugin(native),
126
+ conflicts: conflicts(native),
127
+ snapshot,
128
+ published: marketplaceKind !== "missing",
129
+ };
130
+ }
131
+ function installDecision(facts) {
132
+ if (facts.registration === "foreign")
133
+ return blocked(`error: a Claude Code marketplace named ${CLAUDE_CODE_MARKETPLACE} is registered from another source; remove it manually, then retry:`, ` source: ${facts.marketplaceSource?.source ?? "unknown"}; path: ${facts.marketplaceSource?.path ?? "unknown"}`, ` claude plugin marketplace remove ${CLAUDE_CODE_MARKETPLACE}`);
134
+ if (facts.conflicts.length > 0)
135
+ return blocked("error: another Superpowers Claude Code plugin is enabled; disable it manually, then retry:", ...facts.conflicts.map((id) => DISPLAYABLE_ID.test(id) ? ` claude plugin disable ${id}` : ` ${id}`));
136
+ return { kind: "allowed" };
137
+ }
138
+ export async function inspectClaudeCodeOwnership(ctx, run = runClaude) {
139
+ const operation = "inspect-claude-code-ownership";
140
+ try {
141
+ const facts = await observeFacts(ctx, run);
142
+ if (facts.snapshot.kind === "unverified")
143
+ throw new Error("unverified Manager snapshot");
144
+ const removalVerification = facts.registration === "owned" || facts.plugin !== undefined
145
+ ? blocked("error: Claude Code still lists the Manager marketplace or plugin after removal")
146
+ : facts.published
147
+ ? blocked(`error: the Manager-owned Claude Code marketplace is still present at ${facts.paths.marketplaceRoot}`)
148
+ : { kind: "allowed" };
149
+ const presentationValue = facts.registration === "foreign"
150
+ ? "foreign marketplace registration"
151
+ : facts.snapshot.kind === "owned"
152
+ ? facts.registration === "owned"
153
+ ? `managed ${facts.snapshot.digest}`
154
+ : `managed snapshot ${facts.snapshot.digest}`
155
+ : facts.registration === "owned"
156
+ ? "managed registration without a snapshot"
157
+ : "absent";
158
+ return successResult(operation, {
159
+ installEligibility: installDecision(facts),
160
+ removalInput: {
161
+ registration: facts.registration,
162
+ pluginInstalled: facts.plugin !== undefined,
163
+ published: facts.published,
164
+ },
165
+ removalVerification,
166
+ postRemovalOutput: { stdout: [], stderr: [] },
167
+ presentationValue,
168
+ presentationConflicts: facts.conflicts,
169
+ }, []);
170
+ }
171
+ catch {
172
+ return inspectionFailure(operation, "Claude Code ownership");
173
+ }
174
+ }
175
+ export async function inspectClaudeCodeInstalled(selection, ctx, run = runClaude) {
176
+ const operation = "inspect-claude-code-installed";
177
+ let facts;
178
+ try {
179
+ facts = await observeFacts(ctx, run);
180
+ }
181
+ catch {
182
+ return inspectionFailure(operation, "Claude Code installed state");
183
+ }
184
+ const mismatch = (observedIdentity) => successResult(operation, { kind: "mismatch", observedIdentity }, []);
185
+ if (facts.snapshot.kind === "absent")
186
+ return facts.registration === "absent" &&
187
+ facts.plugin === undefined &&
188
+ !facts.published
189
+ ? successResult(operation, { kind: "absent", observedIdentity: "" }, [])
190
+ : mismatch("registered without an installed snapshot");
191
+ if (facts.snapshot.kind === "unverified")
192
+ return mismatch("unverified installed snapshot");
193
+ const observed = facts.snapshot.digest;
194
+ const plugin = facts.plugin;
195
+ if (facts.registration !== "owned" ||
196
+ plugin === undefined ||
197
+ !plugin.enabled ||
198
+ plugin.errorCount > 0 ||
199
+ facts.conflicts.length > 0)
200
+ return mismatch(observed);
201
+ try {
202
+ const prepared = await readClaudeCodePackageAssessment(facts.paths.preparedRoot);
203
+ if (prepared.compatibility.kind !== "supported" ||
204
+ prepared.receipt.commit !== selection.desiredCommit ||
205
+ !sameSnapshotSource(prepared.receipt.source, selection.effectiveSource) ||
206
+ facts.snapshot.receipt.commit !== prepared.receipt.commit ||
207
+ facts.snapshot.digest !== prepared.receipt.digest ||
208
+ plugin.version !== expectedClaudeCodeVersion(selection) ||
209
+ (await readClaudeCodeManifestVersion(facts.paths.pluginRoot)) !==
210
+ expectedClaudeCodeVersion(selection))
211
+ return mismatch(observed);
212
+ }
213
+ catch {
214
+ return mismatch(observed);
215
+ }
216
+ return successResult(operation, { kind: "current", observedIdentity: observed }, []);
217
+ }
218
+ // Filesystem-only: any `.superpowers.*` sibling is a publication backup or a
219
+ // staging directory left by an interrupted install.
220
+ export async function leftoverPublicationMaterial(paths) {
221
+ if ((await classifyPathNoFollow(paths.pluginsRoot)) === "missing")
222
+ return [];
223
+ return (await readdir(paths.pluginsRoot))
224
+ .filter((name) => name.startsWith(".superpowers."))
225
+ .sort()
226
+ .map((name) => join(paths.pluginsRoot, name));
227
+ }
228
+ export async function inspectClaudeCodeControl(ctx) {
229
+ const operation = "inspect-claude-code-control";
230
+ try {
231
+ const paths = pathsFor(ctx);
232
+ await assertClaudeCodeStorageSafe(paths);
233
+ const leftovers = await leftoverPublicationMaterial(paths);
234
+ if (leftovers.length === 0)
235
+ return successResult(operation, {
236
+ probeEligibility: { kind: "allowed" },
237
+ mutationEligibility: { kind: "allowed" },
238
+ presentationValue: "clear",
239
+ }, []);
240
+ // Shared probe requires both decisions blocked whenever recovery is
241
+ // required (`src/commands/probe.ts::function coherentControl`).
242
+ const decision = blocked("error: Claude Code recovery required; inspect and remove leftover publication material manually:", ...leftovers.map((path) => ` ${path}`));
243
+ return successResult(operation, {
244
+ probeEligibility: decision,
245
+ mutationEligibility: decision,
246
+ presentationValue: `recovery required at ${leftovers.join(", ")}`,
247
+ recoveryState: "required",
248
+ }, []);
249
+ }
250
+ catch {
251
+ return inspectionFailure(operation, "Claude Code update control");
252
+ }
253
+ }
@@ -2,8 +2,8 @@ import { Buffer } from "node:buffer";
2
2
  import { execFile } from "node:child_process";
3
3
  import { constants } from "node:fs";
4
4
  import { access, copyFile, mkdir, readFile, realpath, stat, writeFile, } from "node:fs/promises";
5
- import { delimiter, isAbsolute, join } from "node:path";
6
- import { AdapterMessageLog, failureResult, hasTerminalControl, successResult, } from "../../adapter-result.js";
5
+ import { delimiter, join } from "node:path";
6
+ import { AdapterMessageLog, failureResult, successResult, } from "../../adapter-result.js";
7
7
  import { activePluginVersionFromJson, codexInstalledPluginsFromJson, installedListingHas, marketplaceRootFromJson, } from "./json.js";
8
8
  import { CODEX_LEGACY_PLUGIN_ID, CODEX_MANAGER_PLUGIN_ID, inspectCodexConflicts, } from "./conflicts.js";
9
9
  import { oneLine } from "../../cli-arguments.js";
@@ -16,6 +16,7 @@ import { applyManifestOverlay } from "./manifest-overlay.js";
16
16
  import { readCodexBuildSource } from "../../provenance.js";
17
17
  import { codexOwnershipInspection } from "./lifecycle.js";
18
18
  import { codexInstallReceipt } from "./presentation.js";
19
+ import { isFile } from "../../safe-path.js";
19
20
  const PLUGIN_ID = CODEX_MANAGER_PLUGIN_ID;
20
21
  const MARKETPLACE_NAME = "superpowers-manager";
21
22
  const LEGACY_PLUGIN_ID = CODEX_LEGACY_PLUGIN_ID;
@@ -110,7 +111,7 @@ export function mapCodexLaunchFailure(cause, codexBin) {
110
111
  signal: null,
111
112
  stdout: Buffer.alloc(0),
112
113
  // Trailing newline matches how a real process writes stderr;
113
- // `src/adapter-result.ts:133::appendBytes` splits on newlines and terminates the
114
+ // `src/adapter-result.ts:132::appendBytes` splits on newlines and terminates the
114
115
  // final chunk at end-of-buffer either way.
115
116
  stderr: Buffer.from(`cannot launch Codex command ${codexBin}${detail}\n`, "utf8"),
116
117
  };
@@ -160,42 +161,14 @@ async function requireCodex(codexBin, env) {
160
161
  fail("command-not-found", `required Codex command not found: ${codexBin}`);
161
162
  }
162
163
  }
163
- async function directoryExists(path) {
164
- try {
165
- return (await stat(path)).isDirectory();
166
- }
167
- catch {
168
- return false;
169
- }
170
- }
171
- async function fileExists(path) {
172
- try {
173
- return (await stat(path)).isFile();
174
- }
175
- catch {
176
- return false;
177
- }
178
- }
179
164
  async function runBuild(input, env, log) {
180
165
  const { upstreamRoot, candidateRoot, fallbackManifest } = input;
181
- if (!isAbsolute(upstreamRoot)) {
182
- fail("invalid-arguments", "--upstream-root must be an absolute path");
183
- }
184
- if (!isAbsolute(candidateRoot)) {
185
- fail("invalid-arguments", "--candidate-root must be an absolute path");
186
- }
187
- if (!(await directoryExists(upstreamRoot))) {
188
- fail("invalid-arguments", `upstream root not found: ${upstreamRoot}`);
189
- }
190
- if (!(await directoryExists(candidateRoot))) {
191
- fail("invalid-arguments", `candidate root not found: ${candidateRoot}`);
192
- }
193
- if (!(await fileExists(fallbackManifest))) {
166
+ if (!(await isFile(fallbackManifest))) {
194
167
  fail("invalid-arguments", `fallback manifest not found: ${fallbackManifest}`);
195
168
  }
196
169
  const candidateManifest = join(candidateRoot, ".codex-plugin/plugin.json");
197
170
  const upstreamManifest = join(upstreamRoot, ".codex-plugin/plugin.json");
198
- const manifestSource = (await fileExists(upstreamManifest))
171
+ const manifestSource = (await isFile(upstreamManifest))
199
172
  ? "upstream"
200
173
  : "fallback";
201
174
  try {
@@ -254,8 +227,8 @@ async function runBuild(input, env, log) {
254
227
  // applyManifestOverlay's own messages already name the manifest
255
228
  // path — three are the frozen CPython wording, and the fourth (the
256
229
  // numeric-overflow diagnostic, which has no CPython oracle wording
257
- // to match) now carries the path via its own rewrap in
258
- // src/harnesses/codex/manifest-overlay.ts. Emit as-is, with no added prefix — a
230
+ // to match) carries the path from src/harnesses/codex/manifest-overlay.ts's
231
+ // emitter. Emit as-is, with no added prefix — a
259
232
  // prefix here would double up the path these messages already
260
233
  // name.
261
234
  log.appendText("stderr", oneLine(cause));
@@ -309,12 +282,6 @@ async function runBuild(input, env, log) {
309
282
  return {};
310
283
  }
311
284
  async function runInstall(packageRoot, env, log) {
312
- if (!isAbsolute(packageRoot)) {
313
- fail("invalid-arguments", "--package-root must be an absolute path");
314
- }
315
- if (!(await directoryExists(packageRoot))) {
316
- fail("invalid-arguments", `package root not found: ${packageRoot}`);
317
- }
318
285
  const codexBin = env.SUPERPOWERS_CODEX || "codex";
319
286
  const refreshMode = codexInstallRefreshMode(env);
320
287
  await requireCodex(codexBin, env);
@@ -392,11 +359,6 @@ async function runUninstall(input, env, log) {
392
359
  return {};
393
360
  }
394
361
  function ownershipFromResources(pluginPresent, marketplacePresent, legacyPresent, conflicts) {
395
- if (conflicts.some((item) => typeof item !== "string" ||
396
- item.length === 0 ||
397
- hasTerminalControl(item))) {
398
- fail("malformed-result", "expected an array of strings at conflicts");
399
- }
400
362
  const managerPresent = pluginPresent || marketplacePresent;
401
363
  const identityState = managerPresent
402
364
  ? legacyPresent
@@ -407,14 +369,27 @@ function ownershipFromResources(pluginPresent, marketplacePresent, legacyPresent
407
369
  : "neither";
408
370
  return codexOwnershipInspection(identityState, { pluginPresent, marketplacePresent }, conflicts);
409
371
  }
372
+ async function ownershipAfterListingFailure(context, env, message) {
373
+ const stored = await storedStateAfterListingFailure(env, "inspect-failed", message);
374
+ const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
375
+ return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
376
+ }
377
+ async function nativeAfterListingFailure(env, message) {
378
+ const stored = await storedStateAfterListingFailure(env, "inspect-failed", message);
379
+ return {
380
+ marketplaceRoot: stored.managerMarketplaceRoot,
381
+ pluginPresent: stored.managerPluginPresent,
382
+ pluginEnabled: stored.managerPluginEnabled,
383
+ activeVersion: null,
384
+ activeRoot: null,
385
+ };
386
+ }
410
387
  async function runOwnership(context, env, log) {
411
388
  const codexBin = env.SUPERPOWERS_CODEX || "codex";
412
389
  await requireCodex(codexBin, env);
413
390
  const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
414
391
  if (commandFailed(plugins)) {
415
- const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
416
- const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
417
- return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
392
+ return await ownershipAfterListingFailure(context, env, `cannot list Codex plugins via '${codexBin} plugin list --json'`);
418
393
  }
419
394
  let managerPlugin;
420
395
  let legacyPlugin;
@@ -430,9 +405,7 @@ async function runOwnership(context, env, log) {
430
405
  }
431
406
  const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
432
407
  if (commandFailed(marketplaces)) {
433
- const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
434
- const conflicts = await inspectCodexConflicts({ root: context.root, env }, stored.installedPlugins);
435
- return ownershipFromResources(stored.managerPluginPresent, true, stored.legacyPluginPresent || stored.legacyMarketplaceRoot !== null, conflicts);
408
+ return await ownershipAfterListingFailure(context, env, `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
436
409
  }
437
410
  let managerMarketplace;
438
411
  let legacyMarketplace;
@@ -484,14 +457,7 @@ export function codexReadNativeState(context) {
484
457
  await requireCodex(codexBin, env);
485
458
  const plugins = await listingCommand(log, codexBin, ["plugin", "list", "--json"], env);
486
459
  if (commandFailed(plugins)) {
487
- const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex plugins via '${codexBin} plugin list --json'`);
488
- return {
489
- marketplaceRoot: stored.managerMarketplaceRoot,
490
- pluginPresent: stored.managerPluginPresent,
491
- pluginEnabled: stored.managerPluginEnabled,
492
- activeVersion: null,
493
- activeRoot: null,
494
- };
460
+ return await nativeAfterListingFailure(env, `cannot list Codex plugins via '${codexBin} plugin list --json'`);
495
461
  }
496
462
  let manager;
497
463
  let activeVersion;
@@ -512,14 +478,7 @@ export function codexReadNativeState(context) {
512
478
  }
513
479
  const marketplaces = await listingCommand(log, codexBin, ["plugin", "marketplace", "list", "--json"], env);
514
480
  if (commandFailed(marketplaces)) {
515
- const stored = await storedStateAfterListingFailure(env, "inspect-failed", `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
516
- return {
517
- marketplaceRoot: stored.managerMarketplaceRoot,
518
- pluginPresent: stored.managerPluginPresent,
519
- pluginEnabled: stored.managerPluginEnabled,
520
- activeVersion: null,
521
- activeRoot: null,
522
- };
481
+ return await nativeAfterListingFailure(env, `cannot list Codex marketplaces via '${codexBin} plugin marketplace list --json'`);
523
482
  }
524
483
  let marketplaceRoot;
525
484
  try {
@@ -1,17 +1,12 @@
1
1
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { isAbsenceError } from "../../safe-path.js";
3
4
  export const CODEX_MANAGER_PLUGIN_ID = "superpowers@superpowers-manager";
4
5
  export const CODEX_LEGACY_PLUGIN_ID = "superpowers@superpowers-wrapper";
5
6
  const SUPERPOWERS_PLUGIN_PREFIX = "superpowers@";
6
7
  const DISPLAYABLE_PLUGIN_ID = /^superpowers@[A-Za-z0-9][A-Za-z0-9._-]*/;
7
8
  const NATIVE_ROUTE = ".agents/skills/superpowers";
8
9
  const NATIVE_ROUTE_CONFLICT = `native Codex skills route ~/${NATIVE_ROUTE} has indeterminate activity`;
9
- function isMissingPath(cause) {
10
- if (cause === null || typeof cause !== "object" || !("code" in cause)) {
11
- return false;
12
- }
13
- return cause.code === "ENOENT" || cause.code === "ENOTDIR";
14
- }
15
10
  function isUnmanagedPluginId(pluginId) {
16
11
  if (pluginId === CODEX_MANAGER_PLUGIN_ID ||
17
12
  pluginId === CODEX_LEGACY_PLUGIN_ID) {
@@ -53,14 +48,14 @@ async function nativeRouteConflict(ctx) {
53
48
  routeIsSymlink = (await lstat(route)).isSymbolicLink();
54
49
  }
55
50
  catch (cause) {
56
- return isMissingPath(cause) ? "" : NATIVE_ROUTE_CONFLICT;
51
+ return isAbsenceError(cause) ? "" : NATIVE_ROUTE_CONFLICT;
57
52
  }
58
53
  if (!routeIsSymlink) {
59
54
  try {
60
55
  await lstat(asset);
61
56
  }
62
57
  catch (cause) {
63
- return isMissingPath(cause) ? "" : NATIVE_ROUTE_CONFLICT;
58
+ return isAbsenceError(cause) ? "" : NATIVE_ROUTE_CONFLICT;
64
59
  }
65
60
  }
66
61
  try {
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
2
2
  import { posix } from "node:path";
3
3
  import { COMMIT_RE, SEMVER_RE } from "../../domain/refs.js";
4
4
  import { compareByCodePoint, pythonStrip } from "../../python-text.js";
5
+ import { isAbsenceError } from "../../safe-path.js";
5
6
  import { DEFAULT_FS_DEPS, validateSkillFrontmatter, } from "../../skill-validation.js";
6
7
  import { parseStrictJson } from "../../strict-json.js";
7
8
  const MANIFEST_JSON_PROFILE = {
@@ -18,14 +19,6 @@ class InspectionFailure extends Error {
18
19
  /** Any error from a directory enumeration, absence-like included. */
19
20
  class EnumerationFailure extends Error {
20
21
  }
21
- /**
22
- * `ENOENT`/`ENOTDIR` mean *missing*; every other error rejects. This is the
23
- * only place the absence set is defined.
24
- */
25
- function isAbsenceError(cause) {
26
- const code = cause?.code;
27
- return code === "ENOENT" || code === "ENOTDIR";
28
- }
29
22
  const STRICT_DECODER = new TextDecoder("utf-8", {
30
23
  fatal: true,
31
24
  ignoreBOM: true,
@@ -447,9 +440,9 @@ async function validateHookSubtree(pluginRoot, hooksRoot, errors, deps) {
447
440
  isLink = (await inspectLink(path, deps)) === "symlink";
448
441
  }
449
442
  catch {
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.
443
+ // The `src/harnesses/codex/generated-plugin.ts:575::isLink =` probe reaches the first catch bounded by `src/harnesses/codex/generated-plugin.ts:584::let rawTarget`; the `src/harnesses/codex/generated-plugin.ts:586::rawTarget = decodePathBytes` readlink reaches the same site text.
444
+ // The three probes sharing the *subtree* string are `src/harnesses/codex/generated-plugin.ts:619::resolvedDirectory =`, `src/harnesses/codex/generated-plugin.ts:629::children =`, and
445
+ // `src/harnesses/codex/generated-plugin.ts:638::deps, true)) === "directory"`, not this one.
453
446
  errors.push(`generated hook symlink could not be inspected: ${path}`);
454
447
  return false;
455
448
  }