superpowers-manager 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +44 -26
  2. package/dist/adapter-result.js +5 -9
  3. package/dist/atomic.js +4 -14
  4. package/dist/cli-arguments.js +1 -27
  5. package/dist/cli.js +32 -52
  6. package/dist/commands/adapter-call.js +28 -0
  7. package/dist/commands/install.js +26 -107
  8. package/dist/commands/mutation.js +7 -0
  9. package/dist/commands/pin.js +1 -1
  10. package/dist/commands/prepare.js +39 -129
  11. package/dist/commands/probe.js +35 -264
  12. package/dist/commands/track-latest.js +1 -1
  13. package/dist/commands/uninstall.js +16 -92
  14. package/dist/commands/update.js +5 -25
  15. package/dist/effective-selection.js +7 -9
  16. package/dist/git-tree.js +85 -0
  17. package/dist/harness-command-result.js +47 -0
  18. package/dist/harnesses/codex/adapter.js +231 -510
  19. package/dist/harnesses/codex/compatibility.js +2 -17
  20. package/dist/harnesses/codex/conflicts.js +2 -3
  21. package/dist/harnesses/codex/harness.js +18 -263
  22. package/dist/harnesses/codex/hooks.js +1 -1
  23. package/dist/harnesses/codex/json.js +1 -11
  24. package/dist/harnesses/codex/lifecycle.js +80 -72
  25. package/dist/harnesses/codex/manifest-overlay.js +23 -18
  26. package/dist/harnesses/codex/paths.js +14 -29
  27. package/dist/harnesses/codex/presentation.js +8 -37
  28. package/dist/harnesses/codex/publication.js +13 -37
  29. package/dist/harnesses/codex/recovery.js +5 -5
  30. package/dist/harnesses/codex/stored-state.js +1 -1
  31. package/dist/harnesses/opencode/config.js +256 -0
  32. package/dist/harnesses/opencode/discovery.js +587 -0
  33. package/dist/harnesses/opencode/harness.js +37 -0
  34. package/dist/harnesses/opencode/install.js +601 -0
  35. package/dist/harnesses/opencode/native.js +57 -0
  36. package/dist/harnesses/opencode/package.js +89 -0
  37. package/dist/harnesses/opencode/paths.js +38 -0
  38. package/dist/harnesses/opencode/prepare.js +30 -0
  39. package/dist/harnesses/opencode/presentation.js +28 -0
  40. package/dist/harnesses/opencode/state.js +174 -0
  41. package/dist/harnesses/pi/compatibility.js +5 -37
  42. package/dist/harnesses/pi/install.js +7 -5
  43. package/dist/harnesses/pi/native.js +3 -47
  44. package/dist/harnesses/pi/package.js +5 -107
  45. package/dist/harnesses/pi/prepare.js +13 -104
  46. package/dist/harnesses/pi/presentation.js +23 -66
  47. package/dist/harnesses/pi/shared-skills.js +28 -39
  48. package/dist/harnesses/pi/state.js +7 -27
  49. package/dist/provenance.js +2 -2
  50. package/dist/python-json-format.js +0 -69
  51. package/dist/python-json.js +2 -27
  52. package/dist/python-text.js +9 -69
  53. package/dist/safe-path.js +2 -2
  54. package/dist/selection-store.js +2 -22
  55. package/dist/snapshot-package.js +78 -0
  56. package/dist/snapshot-prepare.js +99 -0
  57. package/dist/snapshot-presentation.js +44 -0
  58. package/dist/validator.js +39 -73
  59. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  60. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  61. package/node_modules/jsonc-parser/README.md +364 -0
  62. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  63. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  64. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  65. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  66. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  67. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  68. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  69. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  70. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  71. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  72. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  73. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  74. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  75. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  76. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  77. package/node_modules/jsonc-parser/package.json +37 -0
  78. package/package.json +8 -4
  79. package/dist/domain/fingerprint.js +0 -4
  80. package/dist/selection-state-cli.js +0 -85
  81. package/dist/status.js +0 -24
  82. package/dist/upstream-cli.js +0 -110
  83. package/dist/validate-generated-plugin-cli.js +0 -118
@@ -1,112 +1,10 @@
1
- import { execFile } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
4
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
- import { promisify } from "node:util";
6
- import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, addArtifactHashField, digestArtifactTree, readArtifactObject, } from "../../artifact-tree.js";
1
+ import { join } from "node:path";
2
+ import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, } from "../../artifact-tree.js";
7
3
  import { COMMIT_RE } from "../../domain/refs.js";
8
- import { assertNoFollowType, assertSymlinkTargetContained, } from "../../safe-path.js";
9
4
  import { SafetyError } from "../../safety-error.js";
10
5
  import { validateSource } from "../../selection.js";
11
6
  import { assessPiCompatibility } from "./compatibility.js";
12
- const exec = promisify(execFile);
13
- const DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
14
- // A consistency checksum, not authentication. A coherent manual rewrite of
15
- // both receipt and bytes remains possible; installed comparison must still use
16
- // an independently validated candidate.
17
- export function piReceiptBinding(receipt) {
18
- const hash = createHash("sha256");
19
- for (const field of [
20
- String(receipt.schema),
21
- receipt.manager,
22
- receipt.harness,
23
- receipt.source,
24
- receipt.commit,
25
- receipt.digest,
26
- ])
27
- addArtifactHashField(hash, Buffer.from(field));
28
- return hash.digest("hex");
29
- }
30
- async function gitBytes(repository, args) {
31
- try {
32
- return (await exec("git", ["--no-replace-objects", "-C", repository, ...args], {
33
- encoding: null,
34
- maxBuffer: 256 * 1024 * 1024,
35
- })).stdout;
36
- }
37
- catch (cause) {
38
- throw new SafetyError("pi-package", `cannot read Pi tree objects from repository: ${repository}`, { cause });
39
- }
40
- }
41
- export async function materializePiTree(repository, commit, destination) {
42
- try {
43
- if (!COMMIT_RE.test(commit))
44
- throw new Error("commit");
45
- if (!(await gitBytes(repository, ["cat-file", "-t", commit])).equals(Buffer.from("commit\n")))
46
- throw new Error("not a commit");
47
- await assertNoFollowType(destination, ["missing"]);
48
- const bytes = await gitBytes(repository, [
49
- "ls-tree",
50
- "-rz",
51
- "--full-tree",
52
- commit,
53
- ]);
54
- const entries = [];
55
- let offset = 0;
56
- while (offset < bytes.length) {
57
- const end = bytes.indexOf(0, offset);
58
- if (end < 0)
59
- throw new Error("tree framing");
60
- const record = DECODER.decode(bytes.subarray(offset, end));
61
- offset = end + 1;
62
- const match = /^(100644|100755|120000) blob ([a-f0-9]{40})\t([\s\S]+)$/.exec(record);
63
- if (!match)
64
- throw new Error("tree entry");
65
- const path = match[3];
66
- if (path
67
- .split("/")
68
- .some((part) => !part ||
69
- part === "." ||
70
- part === ".." ||
71
- part.toLowerCase() === ".git") ||
72
- isAbsolute(path) ||
73
- path.includes("\\") ||
74
- path.split("/")[0] === ARTIFACT_RECEIPT)
75
- throw new Error("tree path");
76
- entries.push({ mode: match[1], oid: match[2], path });
77
- }
78
- await mkdir(destination, { recursive: true });
79
- for (const entry of entries) {
80
- await mkdir(dirname(join(destination, entry.path)), { recursive: true });
81
- }
82
- for (const entry of entries.filter((entry) => entry.mode !== "120000")) {
83
- const path = join(destination, entry.path);
84
- await writeFile(path, await gitBytes(repository, ["cat-file", "blob", entry.oid]), { flag: "wx", mode: entry.mode === "100755" ? 0o755 : 0o644 });
85
- await chmod(path, entry.mode === "100755" ? 0o755 : 0o644);
86
- }
87
- for (const entry of entries.filter((entry) => entry.mode === "120000")) {
88
- const target = DECODER.decode(await gitBytes(repository, ["cat-file", "blob", entry.oid]));
89
- const path = join(destination, entry.path);
90
- const suffix = relative(resolve(destination), resolve(dirname(path), target));
91
- if (!target ||
92
- target.includes("\0") ||
93
- isAbsolute(target) ||
94
- suffix === ".." ||
95
- suffix.startsWith("../") ||
96
- isAbsolute(suffix))
97
- throw new Error("link escape");
98
- await symlink(target, path);
99
- }
100
- for (const entry of entries.filter((entry) => entry.mode === "120000"))
101
- await assertSymlinkTargetContained(destination, join(destination, entry.path));
102
- }
103
- catch (cause) {
104
- throw new SafetyError("pi-package", `cannot materialize Pi tree from ${repository} into ${destination}`, { cause });
105
- }
106
- }
107
- export async function digestPiTree(root) {
108
- return await digestArtifactTree(root);
109
- }
7
+ import { snapshotReceiptBinding } from "../../snapshot-package.js";
110
8
  export async function readPiReceipt(root) {
111
9
  const path = join(root, ARTIFACT_RECEIPT);
112
10
  try {
@@ -123,7 +21,7 @@ export async function readPiReceipt(root) {
123
21
  !ARTIFACT_DIGEST_RE.test(value.binding))
124
22
  throw new Error("receipt fields");
125
23
  validateSource(value.source);
126
- if (piReceiptBinding(value) !== value.binding)
24
+ if (snapshotReceiptBinding(value) !== value.binding)
127
25
  throw new Error("receipt binding");
128
26
  const compatibility = value.compatibility;
129
27
  if (!compatibility ||
@@ -144,7 +42,7 @@ export async function readPiReceipt(root) {
144
42
  }
145
43
  export async function readPiPackageAssessment(root) {
146
44
  const receipt = await readPiReceipt(root);
147
- if ((await digestPiTree(root)) !== receipt.digest)
45
+ if ((await digestArtifactTree(root)) !== receipt.digest)
148
46
  throw new SafetyError("pi-package", `Pi artifact digest mismatch: ${root}`);
149
47
  const compatibility = await assessPiCompatibility(root, {
150
48
  effectiveSource: receipt.source,
@@ -1,111 +1,20 @@
1
- import { writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { failureResult, successResult, } from "../../adapter-result.js";
4
- import { ARTIFACT_RECEIPT } from "../../artifact-tree.js";
1
+ import { createSnapshotPreparation } from "../../snapshot-prepare.js";
5
2
  import { piPaths } from "./paths.js";
6
- import { assessPiCompatibility, samePiSource } from "./compatibility.js";
7
- import { digestPiTree, materializePiTree, readPiPackageAssessment, piReceiptBinding, } from "./package.js";
8
- import { classifyPathNoFollow } from "../../safe-path.js";
3
+ import { assessPiCompatibility } from "./compatibility.js";
4
+ import { readPiPackageAssessment } from "./package.js";
9
5
  export function piPreparationLocation(ctx) {
10
6
  return {
11
7
  destinationRoot: piPaths(ctx.env ?? {}, process.cwd()).preparedRoot,
12
8
  stagingLeaf: "superpowers",
13
9
  };
14
10
  }
15
- export async function preparePiCandidate(input, _ctx) {
16
- try {
17
- await materializePiTree(input.upstreamRoot, input.selection.desiredCommit, input.candidateRoot);
18
- const compatibility = await assessPiCompatibility(input.candidateRoot, input.selection);
19
- if (compatibility.kind === "unsupported" ||
20
- compatibility.kind === "unknown")
21
- return failureResult("prepare", "unsupported", compatibility.reason, [], []);
22
- const digest = await digestPiTree(input.candidateRoot);
23
- const identity = {
24
- schema: 1,
25
- manager: "superpowers-manager",
26
- harness: "pi",
27
- source: input.selection.effectiveSource,
28
- commit: input.selection.desiredCommit,
29
- digest,
30
- };
31
- const receipt = {
32
- ...identity,
33
- binding: piReceiptBinding(identity),
34
- compatibility,
35
- };
36
- await writeFile(join(input.candidateRoot, ARTIFACT_RECEIPT), JSON.stringify(receipt) + "\n", { flag: "wx" });
37
- return successResult("prepare", {
38
- root: input.candidateRoot,
39
- commit: receipt.commit,
40
- identity: digest,
41
- compatibility,
42
- }, []);
43
- }
44
- catch {
45
- return failureResult("prepare", "invalid-package", `cannot prepare Pi artifact: ${input.candidateRoot}`, [], []);
46
- }
47
- }
48
- async function readArtifactAssessment(root) {
49
- const { receipt, compatibility } = await readPiPackageAssessment(root);
50
- return {
51
- receipt,
52
- artifact: {
53
- root,
54
- commit: receipt.commit,
55
- identity: receipt.digest,
56
- compatibility,
57
- },
58
- };
59
- }
60
- async function readArtifact(root) {
61
- const { artifact } = await readArtifactAssessment(root);
62
- const { compatibility } = artifact;
63
- if (compatibility.kind !== "supported" &&
64
- compatibility.kind !== "experimental")
65
- throw new Error("unsupported profile");
66
- return artifact;
67
- }
68
- export async function inspectPiPrepared(selection, ctx) {
69
- const root = piPreparationLocation(ctx).destinationRoot;
70
- const unknown = {
71
- kind: "unknown",
72
- reason: "Pi prepared compatibility evidence is missing",
73
- };
74
- try {
75
- if ((await classifyPathNoFollow(join(root, ARTIFACT_RECEIPT))) === "missing")
76
- return successResult("inspect-prepared", { kind: "needs-prepare", observedIdentity: "", compatibility: unknown }, []);
77
- const { artifact, receipt } = await readArtifactAssessment(root);
78
- if (artifact.commit !== selection.desiredCommit ||
79
- !samePiSource(receipt.source, selection.effectiveSource))
80
- return successResult("inspect-prepared", {
81
- kind: "needs-prepare",
82
- observedIdentity: artifact.identity,
83
- compatibility: unknown,
84
- }, []);
85
- if (artifact.compatibility.kind !== "supported" &&
86
- artifact.compatibility.kind !== "experimental")
87
- return successResult("inspect-prepared", {
88
- kind: "needs-prepare",
89
- observedIdentity: artifact.identity,
90
- compatibility: artifact.compatibility,
91
- }, []);
92
- return successResult("inspect-prepared", {
93
- kind: "current",
94
- artifact,
95
- observedIdentity: artifact.identity,
96
- compatibility: artifact.compatibility,
97
- }, []);
98
- }
99
- catch {
100
- return failureResult("inspect-prepared", "invalid-package", `cannot inspect Pi prepared artifact: ${root}`, [], []);
101
- }
102
- }
103
- export async function readPiPrepared(ctx) {
104
- const root = piPreparationLocation(ctx).destinationRoot;
105
- try {
106
- return successResult("read-prepared", await readArtifact(root), []);
107
- }
108
- catch {
109
- return failureResult("read-prepared", "invalid-package", `cannot read Pi prepared artifact: ${root}`, [], []);
110
- }
111
- }
11
+ const preparation = createSnapshotPreparation({
12
+ harness: "pi",
13
+ label: "Pi",
14
+ preparationLocation: piPreparationLocation,
15
+ assessCompatibility: assessPiCompatibility,
16
+ readAssessment: readPiPackageAssessment,
17
+ });
18
+ export const preparePiCandidate = preparation.prepareCandidate;
19
+ export const inspectPiPrepared = preparation.inspectPrepared;
20
+ export const readPiPrepared = preparation.readPrepared;
@@ -1,67 +1,24 @@
1
- export const piPresentation = {
2
- installNotice: "",
3
- currentNotice: "The Superpowers Pi snapshot is current.",
4
- renderProbe(facts) {
5
- const fields = [
6
- ["harness", "pi"],
7
- ["desired_commit", facts.selection.desiredCommit],
8
- ["prepared_identity", facts.prepared.observedIdentity],
9
- ["installed_identity", facts.installed.observedIdentity],
10
- ["installation_state", facts.installed.kind],
11
- ["resource_state", facts.resourceState ?? "idle"],
12
- ["ownership", facts.ownership.presentationValue],
13
- ["conflicts", (facts.ownership.presentationConflicts ?? []).join("; ")],
14
- ["update_control", facts.control.presentationValue],
15
- ["compatibility", facts.compatibility.kind],
16
- ["compatibility_reason", facts.compatibility.reason],
17
- ["status", facts.status],
18
- ];
19
- return {
20
- human: fields
21
- .map(([key, value]) => `${key.replaceAll("_", " ")}: ${value || "not present"}\n`)
22
- .join(""),
23
- porcelain: fields.map(([key, value]) => `${key}=${value}\n`).join(""),
24
- };
25
- },
26
- renderInstallVerification(_desiredCommit, receipt, inspection) {
27
- if (receipt.status !== 0 || !receipt.outcome.ok)
28
- return {
29
- stdout: [],
30
- stderr: [
31
- "error: Pi activation did not return a verified installation receipt",
32
- ],
33
- };
34
- if (inspection.status !== 0 || !inspection.outcome.ok)
35
- return receipt.outcome.result.missingVerificationOutput;
36
- if (inspection.outcome.result.kind === "current")
37
- return {
38
- stdout: [
39
- "Installed the frozen Superpowers Pi snapshot. Restart Pi to load it.",
40
- ],
41
- stderr: [],
42
- };
43
- return inspection.outcome.result.kind === "absent"
44
- ? receipt.outcome.result.missingVerificationOutput
45
- : receipt.outcome.result.mismatchVerificationOutput;
46
- },
47
- renderRemovalCompletion(_ownership, removalInput) {
48
- return removalInput.receiptDigest === null &&
49
- removalInput.registrationIdentity === null
50
- ? {
51
- stdout: ["No managed Superpowers Pi installation is present."],
52
- stderr: [],
53
- }
54
- : {
55
- stdout: [
56
- "Removed the managed Superpowers Pi installation. Restart Pi to load the resulting state.",
57
- ],
58
- stderr: [],
59
- };
60
- },
61
- callFailure(site) {
62
- return {
63
- unexpected: `Pi ${site} did not complete`,
64
- invalidStatus: `Pi ${site} returned an invalid status`,
65
- };
66
- },
1
+ import { createSnapshotPresentation } from "../../snapshot-presentation.js";
2
+ const renderProbe = (facts) => {
3
+ const fields = [
4
+ ["harness", "pi"],
5
+ ["desired_commit", facts.selection.desiredCommit],
6
+ ["prepared_identity", facts.prepared.observedIdentity],
7
+ ["installed_identity", facts.installed.observedIdentity],
8
+ ["installation_state", facts.installed.kind],
9
+ ["resource_state", facts.resourceState ?? "idle"],
10
+ ["ownership", facts.ownership.presentationValue],
11
+ ["conflicts", (facts.ownership.presentationConflicts ?? []).join("; ")],
12
+ ["update_control", facts.control.presentationValue],
13
+ ["compatibility", facts.compatibility.kind],
14
+ ["compatibility_reason", facts.compatibility.reason],
15
+ ["status", facts.status],
16
+ ];
17
+ return {
18
+ human: fields
19
+ .map(([key, value]) => `${key.replaceAll("_", " ")}: ${value || "not present"}\n`)
20
+ .join(""),
21
+ porcelain: fields.map(([key, value]) => `${key}=${value}\n`).join(""),
22
+ };
67
23
  };
24
+ export const piPresentation = createSnapshotPresentation("Pi", renderProbe, (input) => input.receiptDigest === null && input.registrationIdentity === null);
@@ -1,7 +1,7 @@
1
1
  import { readdir, readFile, readlink, realpath } from "node:fs/promises";
2
2
  import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { classifyPathNoFollow } from "../../safe-path.js";
4
+ import { classifyPathNoFollow, isContained } from "../../safe-path.js";
5
5
  const PI_IGNORE_FILES = [".gitignore", ".ignore", ".fdignore"];
6
6
  function toPosixPath(path) {
7
7
  return path.split(sep).join("/");
@@ -164,6 +164,20 @@ function normalizeExactSkillPattern(pattern) {
164
164
  : pattern;
165
165
  return toPosixPath(normalized);
166
166
  }
167
+ function forcedSkillActivity(path, identityRoot, controls) {
168
+ const identities = exactSkillIdentities(path, identityRoot);
169
+ for (const [prefix, activity] of [
170
+ ["-", "disabled"],
171
+ ["+", "active"],
172
+ ]) {
173
+ const patterns = controls
174
+ .filter((control) => control.startsWith(prefix))
175
+ .map((control) => normalizeExactSkillPattern(control.slice(1)));
176
+ if (patterns.some((pattern) => identities.includes(pattern)))
177
+ return activity;
178
+ }
179
+ return null;
180
+ }
167
181
  function hasUnknownMinimatchSyntax(pattern) {
168
182
  return (pattern.startsWith("#") ||
169
183
  pattern.includes("!") ||
@@ -171,19 +185,9 @@ function hasUnknownMinimatchSyntax(pattern) {
171
185
  toPosixPath(pattern).includes("//"));
172
186
  }
173
187
  function sharedSkillActivity(path, agentsRoot, controls) {
174
- const exactIdentities = exactSkillIdentities(path, agentsRoot);
175
- const forceExcludes = controls
176
- .filter((control) => control.startsWith("-"))
177
- .map((control) => normalizeExactSkillPattern(control.slice(1)));
178
- if (forceExcludes.some((pattern) => exactIdentities.includes(pattern))) {
179
- return "disabled";
180
- }
181
- const forceIncludes = controls
182
- .filter((control) => control.startsWith("+"))
183
- .map((control) => normalizeExactSkillPattern(control.slice(1)));
184
- if (forceIncludes.some((pattern) => exactIdentities.includes(pattern))) {
185
- return "active";
186
- }
188
+ const forced = forcedSkillActivity(path, agentsRoot, controls);
189
+ if (forced !== null)
190
+ return forced;
187
191
  const excludePatterns = controls
188
192
  .filter((control) => control.startsWith("!"))
189
193
  .map((control) => control.slice(1));
@@ -233,11 +237,6 @@ function resolveTopLevelSkillPath(source, paths) {
233
237
  ? resolve(normalized)
234
238
  : resolve(paths.agentDir, normalized);
235
239
  }
236
- function containedBy(root, candidate) {
237
- const suffix = relative(root, candidate);
238
- return (suffix === "" ||
239
- (!isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`)));
240
- }
241
240
  async function hasSafeSharedSkillAncestors(path, sharedRoot) {
242
241
  const suffix = relative(sharedRoot, dirname(path));
243
242
  const directories = [sharedRoot];
@@ -260,19 +259,9 @@ async function hasSafeSharedSkillAncestors(path, sharedRoot) {
260
259
  return true;
261
260
  }
262
261
  function explicitSharedSkillActivity(path, paths, controls) {
263
- const exactIdentities = exactSkillIdentities(path, paths.agentDir);
264
- const forceExcludes = controls
265
- .filter((control) => control.startsWith("-"))
266
- .map((control) => normalizeExactSkillPattern(control.slice(1)));
267
- if (forceExcludes.some((pattern) => exactIdentities.includes(pattern))) {
268
- return "disabled";
269
- }
270
- const forceIncludes = controls
271
- .filter((control) => control.startsWith("+"))
272
- .map((control) => normalizeExactSkillPattern(control.slice(1)));
273
- if (forceIncludes.some((pattern) => exactIdentities.includes(pattern))) {
274
- return "active";
275
- }
262
+ const forced = forcedSkillActivity(path, paths.agentDir, controls);
263
+ if (forced !== null)
264
+ return forced;
276
265
  const excludePatterns = controls
277
266
  .filter((control) => control.startsWith("!"))
278
267
  .map((control) => control.slice(1));
@@ -321,7 +310,7 @@ async function explicitSharedSkillActivities(settings, paths) {
321
310
  lexicalKind = await classifyPathNoFollow(path);
322
311
  }
323
312
  catch {
324
- if (containedBy(sharedRoot, path) || containedBy(path, sharedRoot)) {
313
+ if (isContained(sharedRoot, path) || isContained(path, sharedRoot)) {
325
314
  indeterminate = true;
326
315
  }
327
316
  continue;
@@ -333,12 +322,12 @@ async function explicitSharedSkillActivities(settings, paths) {
333
322
  canonicalPath = await realpath(path);
334
323
  }
335
324
  catch {
336
- let related = containedBy(sharedRoot, path) || containedBy(path, sharedRoot);
325
+ let related = isContained(sharedRoot, path) || isContained(path, sharedRoot);
337
326
  if (!related && lexicalKind === "symlink") {
338
327
  try {
339
328
  const target = resolve(dirname(path), await readlink(path));
340
329
  related =
341
- containedBy(sharedRoot, target) || containedBy(target, sharedRoot);
330
+ isContained(sharedRoot, target) || isContained(target, sharedRoot);
342
331
  }
343
332
  catch {
344
333
  // Without a relationship to the known route, preserve unrelated state.
@@ -348,10 +337,10 @@ async function explicitSharedSkillActivities(settings, paths) {
348
337
  indeterminate = true;
349
338
  continue;
350
339
  }
351
- const related = containedBy(sharedRoot, path) ||
352
- containedBy(path, sharedRoot) ||
353
- containedBy(canonicalSharedRoot, canonicalPath) ||
354
- containedBy(canonicalPath, canonicalSharedRoot);
340
+ const related = isContained(sharedRoot, path) ||
341
+ isContained(path, sharedRoot) ||
342
+ isContained(canonicalSharedRoot, canonicalPath) ||
343
+ isContained(canonicalPath, canonicalSharedRoot);
355
344
  if (!related)
356
345
  continue;
357
346
  let canonicalKind;
@@ -1,9 +1,9 @@
1
1
  import { join } from "node:path";
2
2
  import { sharedPiSkillsActivity } from "./shared-skills.js";
3
- import { failureResult, successResult, } from "../../adapter-result.js";
3
+ import { inspectionFailure, successResult, } from "../../adapter-result.js";
4
4
  import { readArtifactObject } from "../../artifact-tree.js";
5
- import { samePiSource } from "./compatibility.js";
6
- import { digestPiTree, readPiPackageAssessment, readPiReceipt, } from "./package.js";
5
+ import { observeSnapshot, sameSnapshotSource, } from "../../snapshot-package.js";
6
+ import { readPiPackageAssessment, readPiReceipt, } from "./package.js";
7
7
  import { piPaths } from "./paths.js";
8
8
  import { readPiSettings, resolveCanonicalPiLocalSource, resolvePiLocalSource, } from "./settings.js";
9
9
  import { classifyPathNoFollow } from "../../safe-path.js";
@@ -111,23 +111,6 @@ async function observeSettings(paths) {
111
111
  }
112
112
  return { settings, registration };
113
113
  }
114
- async function observeSnapshot(paths) {
115
- const kind = await classifyPathNoFollow(paths.installedRoot);
116
- if (kind === "missing")
117
- return { kind: "absent" };
118
- if (kind !== "directory")
119
- return { kind: "unverified" };
120
- try {
121
- const receipt = await readPiReceipt(paths.installedRoot);
122
- const digest = await digestPiTree(paths.installedRoot);
123
- return receipt.digest === digest
124
- ? { kind: "owned", receipt, digest }
125
- : { kind: "unverified" };
126
- }
127
- catch {
128
- return { kind: "unverified" };
129
- }
130
- }
131
114
  async function isNamedLocalSuperpowers(entry, paths, canonicalInstalledRoot) {
132
115
  const root = resolvePiLocalSource(entry.source, paths.agentDir, paths.homeDir);
133
116
  if (root === null ||
@@ -176,16 +159,13 @@ async function unmanagedConflicts(settings, paths) {
176
159
  }
177
160
  return [...conflicts];
178
161
  }
179
- function inspectionFailure(operation, subject) {
180
- return failureResult(operation, "invalid-state", `cannot inspect ${subject}`, [], []);
181
- }
182
162
  export async function inspectPiOwnership(ctx) {
183
163
  const operation = "inspect-pi-ownership";
184
164
  const paths = pathsFor(ctx);
185
165
  try {
186
166
  const [{ settings, registration }, snapshot] = await Promise.all([
187
167
  observeSettings(paths),
188
- observeSnapshot(paths),
168
+ observeSnapshot(paths.installedRoot, readPiReceipt),
189
169
  ]);
190
170
  if (snapshot.kind === "unverified") {
191
171
  return inspectionFailure(operation, `Pi ownership at ${paths.installedRoot}`);
@@ -240,7 +220,7 @@ export async function inspectPiInstalled(selection, ctx) {
240
220
  try {
241
221
  [settings, snapshot] = await Promise.all([
242
222
  observeSettings(paths),
243
- observeSnapshot(paths),
223
+ observeSnapshot(paths.installedRoot, readPiReceipt),
244
224
  ]);
245
225
  }
246
226
  catch {
@@ -267,9 +247,9 @@ export async function inspectPiInstalled(selection, ctx) {
267
247
  if ((intended.compatibility.kind !== "supported" &&
268
248
  intended.compatibility.kind !== "experimental") ||
269
249
  intended.receipt.commit !== selection.desiredCommit ||
270
- !samePiSource(intended.receipt.source, selection.effectiveSource) ||
250
+ !sameSnapshotSource(intended.receipt.source, selection.effectiveSource) ||
271
251
  snapshot.receipt.commit !== intended.receipt.commit ||
272
- !samePiSource(snapshot.receipt.source, intended.receipt.source) ||
252
+ !sameSnapshotSource(snapshot.receipt.source, intended.receipt.source) ||
273
253
  snapshot.digest !== intended.receipt.digest) {
274
254
  return mismatch;
275
255
  }
@@ -67,8 +67,8 @@ export function generatedMetadataPath(root) {
67
67
  // Ported from
68
68
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:33-37::spw_generated_commit_or_empty`.
69
69
  // Lenient by design: a missing or malformed generated provenance file yields
70
- // "", which `statusForCommits` reads as "needs prepare". Aborting here would
71
- // deny the operator the remediation path.
70
+ // "", which the typed prepared-state flow reports as "needs prepare".
71
+ // Aborting here would deny the operator the remediation path.
72
72
  export async function generatedCommitOrEmpty(root) {
73
73
  return readGeneratedCommitLenient(generatedMetadataPath(root));
74
74
  }
@@ -1,72 +1,3 @@
1
- import { SafetyError } from "./safety-error.js";
2
- /**
3
- * Render a JSON number token the way CPython's `json.dump` does.
4
- *
5
- * Classification is by source text, matching CPython's own: a token containing
6
- * `.` or `e`/`E` parses as a float, anything else as an arbitrary-width int.
7
- * Integers are emitted verbatim so widths above 2^53 survive; `-0` is the only
8
- * normalization JSON's grammar leaves room for, since leading zeros and a
9
- * leading `+` are already forbidden.
10
- */
11
- export function formatPythonNumber(raw) {
12
- if (!/[.eE]/.test(raw)) {
13
- // NaN/Infinity/-Infinity contain no `.` or `e`/`E`, so they would
14
- // otherwise fall into the "emit verbatim" integer branch and echo back
15
- // as invalid JSON. A reader running a non-standard-constants accept
16
- // profile (see src/strict-json.ts) can hand this function exactly these
17
- // three tokens as a raw number source, so this function must reject them
18
- // itself rather than assume its caller already did. CPython's
19
- // `json.dump(..., allow_nan=False)` raises ValueError for all three.
20
- if (raw === "NaN" || raw === "Infinity" || raw === "-Infinity") {
21
- throw new SafetyError("manifest-overlay", `JSON number out of range: ${raw}`);
22
- }
23
- return raw === "-0" ? "0" : raw;
24
- }
25
- const value = Number(raw);
26
- if (!Number.isFinite(value)) {
27
- throw new SafetyError("manifest-overlay", `JSON number out of range: ${raw}`);
28
- }
29
- return formatPythonFloat(value);
30
- }
31
- function formatPythonFloat(value) {
32
- // toExponential() drops the sign of negative zero; CPython's repr keeps it.
33
- // Note this differs from the integer -0, which normalizes to 0 — consistent,
34
- // because -0 has no `.` or `e` and so classifies as an integer.
35
- if (Object.is(value, -0))
36
- return "-0.0";
37
- // Called with no argument, toExponential() yields the shortest digit string
38
- // that uniquely identifies the double — the same guarantee CPython's repr
39
- // relies on. Taking digits and exponent separately is what lets us apply
40
- // CPython's notation threshold instead of JavaScript's.
41
- const [mantissa, exponentText] = value.toExponential().split("e");
42
- const exponent = Number(exponentText);
43
- const negative = mantissa.startsWith("-");
44
- const digits = mantissa.replace("-", "").replace(".", "");
45
- let body;
46
- if (exponent >= -4 && exponent < 16) {
47
- // Fixed notation, built by MOVING the decimal point through the digit
48
- // string above. Do NOT route this through value.toFixed(): toExponential()
49
- // has already produced the shortest digits that identify this double, and
50
- // toFixed performs a second, independent rounding of the full decimal
51
- // expansion. The two disagree — 1888570120608320.2 formats as
52
- // ...320.3 via toFixed and ...320.2 in CPython. Measured 2026-07-31.
53
- if (exponent >= digits.length - 1) {
54
- body = `${digits}${"0".repeat(exponent - (digits.length - 1))}.0`;
55
- }
56
- else if (exponent >= 0) {
57
- body = `${digits.slice(0, exponent + 1)}.${digits.slice(exponent + 1)}`;
58
- }
59
- else {
60
- body = `0.${"0".repeat(-exponent - 1)}${digits}`;
61
- }
62
- }
63
- else {
64
- const sign = exponent < 0 ? "-" : "+";
65
- const magnitude = String(Math.abs(exponent)).padStart(2, "0");
66
- body = `${mantissa.replace("-", "")}e${sign}${magnitude}`;
67
- }
68
- return negative ? `-${body}` : body;
69
- }
70
1
  /**
71
2
  * Apply CPython's `ensure_ascii=True` to an already-JSON-encoded string. Input
72
3
  * must be the output of JSON.stringify, which has already escaped quotes,