superpowers-manager 0.5.0 → 0.5.1

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 (36) hide show
  1. package/README.md +38 -8
  2. package/dist/artifact-tree.js +2 -1
  3. package/dist/cli.js +2 -2
  4. package/dist/commands/install.js +2 -2
  5. package/dist/commands/prepare.js +4 -4
  6. package/dist/commands/probe.js +4 -4
  7. package/dist/commands/uninstall.js +3 -3
  8. package/dist/commands/update.js +1 -1
  9. package/dist/domain/refs.js +4 -0
  10. package/dist/{adapter.js → harnesses/codex/adapter.js} +10 -10
  11. package/dist/{codex-compatibility.js → harnesses/codex/compatibility.js} +7 -7
  12. package/dist/{codex-conflicts.js → harnesses/codex/conflicts.js} +1 -1
  13. package/dist/{generated-plugin.js → harnesses/codex/generated-plugin.js} +7 -73
  14. package/dist/{codex-harness.js → harnesses/codex/harness.js} +5 -5
  15. package/dist/{hooks.js → harnesses/codex/hooks.js} +6 -6
  16. package/dist/{codex-json.js → harnesses/codex/json.js} +2 -2
  17. package/dist/{lifecycle.js → harnesses/codex/lifecycle.js} +2 -2
  18. package/dist/{manifest-overlay.js → harnesses/codex/manifest-overlay.js} +4 -4
  19. package/dist/{codex-prepare.js → harnesses/codex/prepare.js} +7 -7
  20. package/dist/{codex-presentation.js → harnesses/codex/presentation.js} +3 -3
  21. package/dist/{codex-state.js → harnesses/codex/state.js} +3 -3
  22. package/dist/{pi-compatibility.js → harnesses/pi/compatibility.js} +3 -3
  23. package/dist/{pi-harness.js → harnesses/pi/harness.js} +7 -7
  24. package/dist/{pi-install.js → harnesses/pi/install.js} +8 -8
  25. package/dist/{pi-native.js → harnesses/pi/native.js} +4 -4
  26. package/dist/{pi-package.js → harnesses/pi/package.js} +6 -6
  27. package/dist/{pi-prepare.js → harnesses/pi/prepare.js} +6 -6
  28. package/dist/{pi-settings.js → harnesses/pi/settings.js} +3 -3
  29. package/dist/{pi-shared-skills.js → harnesses/pi/shared-skills.js} +1 -1
  30. package/dist/{pi-state.js → harnesses/pi/state.js} +8 -8
  31. package/dist/python-text.js +16 -0
  32. package/dist/skill-validation.js +52 -0
  33. package/dist/validate-generated-plugin-cli.js +1 -1
  34. package/package.json +4 -3
  35. /package/dist/{pi-paths.js → harnesses/pi/paths.js} +0 -0
  36. /package/dist/{pi-presentation.js → harnesses/pi/presentation.js} +0 -0
package/README.md CHANGED
@@ -1,9 +1,45 @@
1
1
  # Superpowers Manager
2
2
 
3
- Install and explicitly refresh the latest stable [`obra/superpowers`](https://github.com/obra/superpowers) release. Superpowers Manager supports Codex and the Pi coding agent without waiting for a marketplace copy to catch up.
3
+ Install [`obra/superpowers`](https://github.com/obra/superpowers) directly from upstream for Codex and the Pi coding agent. Stay on a known-good release or commit, try a branch, and upgrade when you choose—without waiting for a marketplace copy to catch up.
4
4
 
5
5
  > Unofficial community integration. Not affiliated with the `obra/superpowers` maintainers.
6
6
 
7
+ ## Choose what you run
8
+
9
+ Start with the latest stable release by default, or choose the upstream version that works for you. There is no background updater.
10
+
11
+ | What you want | How to choose it |
12
+ |---|---|
13
+ | Stay on a known-good version | `pin` a release tag or full 40-character commit SHA |
14
+ | Try an upstream fix before its release | Set `SUPERPOWERS_REF` to a branch or other resolvable ref for that invocation |
15
+ | Return to the latest stable release | Save `track-latest`, then explicitly install or update |
16
+
17
+ For example, save a release pin and install it for your agent:
18
+
19
+ ```sh
20
+ npx superpowers-manager pin v6.1.1
21
+ npx superpowers-manager install --harness codex
22
+ ```
23
+
24
+ Use `--harness pi` for Pi. The pin is shared by both agents, but each installation changes only when you run its `install` or `update` command. A saved pin keeps subsequent installs and updates on that version unless you change or override the selection.
25
+
26
+ To try a branch, replace `feature/foo` with an existing upstream branch:
27
+
28
+ ```sh
29
+ SUPERPOWERS_REF=feature/foo npx superpowers-manager install --harness codex
30
+ ```
31
+
32
+ This override applies only to that command and leaves your saved selection unchanged; the installed version remains until you explicitly change it. Branches can move. Persistent `pin` accepts exact version tags (including prereleases) and full commit SHAs.
33
+
34
+ When you are ready to return to the latest stable release:
35
+
36
+ ```sh
37
+ npx superpowers-manager track-latest
38
+ npx superpowers-manager update --harness codex
39
+ ```
40
+
41
+ See [version selection](https://github.com/j7an/superpowers-manager/blob/main/docs/usage.md#choosing-a-source) for source overrides, precedence, and `unpin`.
42
+
7
43
  ## Start with your agent
8
44
 
9
45
  The two harnesses share upstream selection, while each has independent prepared and installed state. Omitting `--harness` selects Codex.
@@ -53,7 +89,7 @@ The manager never removes another Superpowers provider. See the [ownership guida
53
89
 
54
90
  ## Shared selection and lifecycle
55
91
 
56
- Use `pin`, `track-latest`, or `unpin` to save shared upstream intent. They do not activate Codex or Pi; run the selected harness's `install` or `update` to apply that choice. There is no background updater.
92
+ The [selection commands above](#choose-what-you-run) save shared upstream intent. They do not activate Codex or Pi; run the selected harness's `install` or `update` to apply that choice. `unpin` restores the packaged fallback policy.
57
93
 
58
94
  | Command | Purpose |
59
95
  |---|---|
@@ -65,12 +101,6 @@ Use `pin`, `track-latest`, or `unpin` to save shared upstream intent. They do no
65
101
 
66
102
  Use `--harness codex` or `--harness pi` with each targeted lifecycle command.
67
103
 
68
- ```sh
69
- npx superpowers-manager pin v6.1.1
70
- npx superpowers-manager track-latest
71
- npx superpowers-manager unpin
72
- ```
73
-
74
104
  ```text
75
105
  shared selection -> prepare / inspect / activate Codex
76
106
  -> prepare / inspect / activate Pi
@@ -4,7 +4,8 @@ import { join } from "node:path";
4
4
  import { assertExistingContained, assertNoFollowType, assertSymlinkTargetContained, } from "./safe-path.js";
5
5
  import { SafetyError } from "./safety-error.js";
6
6
  import { parseStrictJson } from "./strict-json.js";
7
- import { compareByCodePoint, DEFAULT_FS_DEPS, validateSkillFrontmatter, } from "./generated-plugin.js";
7
+ import { compareByCodePoint } from "./python-text.js";
8
+ import { DEFAULT_FS_DEPS, validateSkillFrontmatter, } from "./skill-validation.js";
8
9
  export const ARTIFACT_RECEIPT = ".superpowers-manager.json";
9
10
  export const ARTIFACT_DIGEST_RE = /^[a-f0-9]{64}$/;
10
11
  const DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { extractHarnessOptions, oneLine, UsageError } from "./cli-arguments.js";
5
- import { codexHarness } from "./codex-harness.js";
6
- import { piHarness } from "./pi-harness.js";
5
+ import { codexHarness } from "./harnesses/codex/harness.js";
6
+ import { piHarness } from "./harnesses/pi/harness.js";
7
7
  import { createResourceCoordinator } from "./resource-lock.js";
8
8
  import { runInstall } from "./commands/install.js";
9
9
  import { runPin } from "./commands/pin.js";
@@ -1,6 +1,6 @@
1
1
  // Ports scripts/install. The shell sourced common.sh, provenance.sh,
2
2
  // status.sh, lifecycle.sh and adapter.sh; the predicates now live in
3
- // src/lifecycle.ts, the generated-metadata read lives in src/provenance.ts,
3
+ // src/harnesses/codex/lifecycle.ts, the generated-metadata read lives in src/provenance.ts,
4
4
  // and the adapter arrives through ctx.adapter.
5
5
  import { tmpdir } from "node:os";
6
6
  import { oneLine } from "../cli-arguments.js";
@@ -163,7 +163,7 @@ async function gatherInstallStages(ctx, selection, artifact) {
163
163
  // is what turns a failed inspection into "error: installed manager
164
164
  // fingerprint inspection failed after install."
165
165
  // renderInstallVerification's failed-inspection arm
166
- // (`src/codex-presentation.ts:332::if (inspection.status !== 0 || !inspection.outcome.ok) {`)
166
+ // (`src/harnesses/codex/presentation.ts:329::if (inspection.status !== 0 || !inspection.outcome.ok) {`)
167
167
  // exists for this result-bearing path; the lifecycle compatibility
168
168
  // export delegates through the same arm. Returning
169
169
  // failed() instead reported the adapter's own generic diagnostic and
@@ -14,7 +14,7 @@ import { runWithMutation } from "./mutation.js";
14
14
  // Every message this module writes is hand-written here. The cause is attached
15
15
  // for debuggability and never reaches a stream: oneLine (src/cli-arguments.ts)
16
16
  // reads .message only. Same arrangement as hookError
17
- // (`src/hooks.ts:44::function hookError`).
17
+ // (`src/harnesses/codex/hooks.ts:44::function hookError`).
18
18
  function prepareError(message, cause) {
19
19
  return new SafetyError("prepare", message, { cause });
20
20
  }
@@ -341,8 +341,8 @@ async function performPrepare(argv, ctx) {
341
341
  // failure (its cause is the shared runner's (src/validator.ts) captured
342
342
  // spawn error, which oneLine never reads -- it takes .message only);
343
343
  // readManifest's three hookError messages
344
- // (`src/hooks.ts:113-138::readManifest`), pinned by
345
- // `tests/unit/hooks.test.ts:95::void test("readManifest diagnostics` as carrying no reader vocabulary or
344
+ // (`src/harnesses/codex/hooks.ts:113-138::readManifest`), pinned by
345
+ // `tests/unit/harnesses/codex/hooks.test.ts:95::void test("readManifest diagnostics` as carrying no reader vocabulary or
346
346
  // errno; and SafetyErrors from gitSafeSource, writeProvenance, and
347
347
  // withWorkspace.
348
348
  //
@@ -382,7 +382,7 @@ async function performPrepare(argv, ctx) {
382
382
  // only, so it bounds how much of that text lands, not what it may
383
383
  // contain.
384
384
  //
385
- // runAdapter's closing `throw cause` (src/adapter.ts) does NOT arrive
385
+ // runAdapter's closing `throw cause` (src/harnesses/codex/adapter.ts) does NOT arrive
386
386
  // here -- the call site catches it and converts it to a hand-written
387
387
  // message per AGENTS.md's reader-diagnostics rule.
388
388
  //
@@ -11,7 +11,7 @@ export const PROBE_USAGE = "error: usage: superpowers-manager probe [--porcelain
11
11
  // silent diagnostics regression, not a simplification.
12
12
  //
13
13
  // Interpolating error.message and each hint is the sanctioned form (AGENTS.md):
14
- // src/adapter.ts has 52 `fail()` sites and none interpolates a caught error's
14
+ // src/harnesses/codex/adapter.ts has 52 `fail()` sites and none interpolates a caught error's
15
15
  // message, so the callee owns every failure reachable on this path. Those two
16
16
  // writes now live in writeAdapterFailure (src/adapter-result.ts), which
17
17
  // validates all three strings before the first of them reaches the stream.
@@ -121,7 +121,7 @@ function validControl(value) {
121
121
  // evidence and report it as success.
122
122
  //
123
123
  // It does still THROW for a non-AdapterFailure cause (runAdapter's closing
124
- // `throw cause`, src/adapter.ts). That is caught here rather than in runProbe's
124
+ // `throw cause`, src/harnesses/codex/adapter.ts). That is caught here rather than in runProbe's
125
125
  // outer catch, because the two need different diagnostics -- see spec §3.3a.
126
126
  //
127
127
  // Reached through ctx.adapter, not a direct module-level dependency on the
@@ -377,9 +377,9 @@ export async function runProbe(argv, ctx) {
377
377
  // which catches every failure and returns "".
378
378
  //
379
379
  // A non-AdapterFailure re-thrown by runAdapter's closing `throw cause`
380
- // (src/adapter.ts) does NOT reach here: inspect() catches it and converts
380
+ // (src/harnesses/codex/adapter.ts) does NOT reach here: inspect() catches it and converts
381
381
  // it to a hand-written message per AGENTS.md's reader-diagnostics rule -- a
382
- // rethrown cause is exactly the failure src/adapter.ts declined to own, so
382
+ // rethrown cause is exactly the failure src/harnesses/codex/adapter.ts declined to own, so
383
383
  // its text must never reach this stream. See §3.3a.
384
384
  //
385
385
  // gatherProbe performs no writes of its own, so this catch cannot also be
@@ -1,5 +1,5 @@
1
1
  // Ports scripts/uninstall. The shell sourced common.sh, provenance.sh,
2
- // lifecycle.sh and adapter.sh; the predicates now live in src/lifecycle.ts
2
+ // lifecycle.sh and adapter.sh; the predicates now live in src/harnesses/codex/lifecycle.ts
3
3
  // and the adapter arrives through ctx.adapter.
4
4
  import { tmpdir } from "node:os";
5
5
  import { oneLine } from "../cli-arguments.js";
@@ -126,7 +126,7 @@ async function gatherUninstall(ctx) {
126
126
  // Reachable only for mkdtemp failure, with nothing collected yet: this
127
127
  // callback never throws -- every ctx.adapter throw is already caught
128
128
  // inside invoke(), and presenceFlag/verifyUninstalledResources/
129
- // reportLegacyState are pure (src/lifecycle.ts's header comment) -- so a
129
+ // reportLegacyState are pure (src/harnesses/codex/lifecycle.ts's header comment) -- so a
130
130
  // post-success cleanup failure is handled by onCleanupFailure above and
131
131
  // cannot reach here. Wrapping with `outcomes` anyway keeps the class
132
132
  // total over its declared contract rather than assuming the callback's
@@ -163,7 +163,7 @@ async function performUninstall(argv, ctx) {
163
163
  // blindly.
164
164
  //
165
165
  // A cause outside ctx.adapter's AdapterFailure guard
166
- // (`src/adapter.ts:973-999::async function runCodexOperation(`) does NOT
166
+ // (`src/harnesses/codex/adapter.ts:973-999::async function runCodexOperation(`) does NOT
167
167
  // reach here: invoke() catches it inside gatherUninstall and converts it
168
168
  // to a hand-written message carried as UninstallOutcome data, exactly as
169
169
  // src/commands/probe.ts's inspect() does for the same cause.
@@ -1,5 +1,5 @@
1
1
  // Ports scripts/update. The shell sourced common.sh and lifecycle.sh; the
2
- // predicates now live in src/lifecycle.ts. Unlike install and uninstall,
2
+ // predicates now live in src/harnesses/codex/lifecycle.ts. Unlike install and uninstall,
3
3
  // update issues no ctx.adapter call of its own: every adapter interaction
4
4
  // this module needs already happens inside gatherProbe, runPrepare or
5
5
  // runInstall, each of which owns its own §4.2a-conformant invoke() gate. That
@@ -3,6 +3,10 @@ const SEMVER_BASE_SOURCE = "(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0
3
3
  "(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?";
4
4
  export const SEMVER_BASE_RE = new RegExp(`^${SEMVER_BASE_SOURCE}$`);
5
5
  export const TAG_RE = new RegExp(`^v${SEMVER_BASE_SOURCE}$`);
6
+ // The Python's own SEMVER_RE (:15-22). Deliberately not SEMVER_BASE_SOURCE
7
+ // from this module, which omits the `+build` component; widening the shared
8
+ // constant would change TAG_RE and is out of scope.
9
+ export const SEMVER_RE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
6
10
  export const COMMIT_RE = /^[0-9a-f]{40}$/;
7
11
  export const COMMIT_INPUT_RE = /^[0-9A-Fa-f]{40}$/;
8
12
  export function isTagRef(value) {
@@ -4,17 +4,17 @@ import { constants } from "node:fs";
4
4
  import { access, copyFile, mkdir, readFile, realpath, stat, writeFile, } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { delimiter, isAbsolute, join } from "node:path";
7
- import { AdapterMessageLog, failureResult, successResult, } from "./adapter-result.js";
8
- import { activePluginVersionFromJson, installedListingHas, marketplaceRootFromJson, } from "./codex-json.js";
9
- import { CODEX_LEGACY_PLUGIN_ID, CODEX_MANAGER_PLUGIN_ID, inspectCodexConflicts, } from "./codex-conflicts.js";
10
- import { oneLine } from "./cli-arguments.js";
11
- import { installedCommitFromRoot, installedRootForVersion, pathsEqual, } from "./codex-state.js";
7
+ import { AdapterMessageLog, failureResult, successResult, } from "../../adapter-result.js";
8
+ import { activePluginVersionFromJson, installedListingHas, marketplaceRootFromJson, } from "./json.js";
9
+ import { CODEX_LEGACY_PLUGIN_ID, CODEX_MANAGER_PLUGIN_ID, inspectCodexConflicts, } from "./conflicts.js";
10
+ import { oneLine } from "../../cli-arguments.js";
11
+ import { installedCommitFromRoot, installedRootForVersion, pathsEqual, } from "./state.js";
12
12
  import { validateGeneratedPlugin } from "./generated-plugin.js";
13
13
  import { classifyHooks, materializeHooks, readManifest, } from "./hooks.js";
14
14
  import { applyManifestOverlay } from "./manifest-overlay.js";
15
- import { readCodexBuildSource } from "./provenance.js";
16
- import { isAcceptedSplitValue } from "./validate-generated-plugin-cli.js";
17
- import { withWorkspace, workspaceRemovalFailure } from "./workspace.js";
15
+ import { readCodexBuildSource } from "../../provenance.js";
16
+ import { isAcceptedSplitValue } from "../../validate-generated-plugin-cli.js";
17
+ import { withWorkspace, workspaceRemovalFailure } from "../../workspace.js";
18
18
  const PLUGIN_ID = CODEX_MANAGER_PLUGIN_ID;
19
19
  const MARKETPLACE_NAME = "superpowers-manager";
20
20
  const LEGACY_PLUGIN_ID = CODEX_LEGACY_PLUGIN_ID;
@@ -89,7 +89,7 @@ function runCommand(file, args, env) {
89
89
  });
90
90
  });
91
91
  }
92
- // Exported only so tests/unit/adapter.test.js can assert the env scrub
92
+ // Exported only so tests/unit/harnesses/codex/adapter.test.ts can assert the env scrub
93
93
  // directly. No production caller uses this name.
94
94
  export { runCommand as runCommandForTest };
95
95
  function commandFailed(result) {
@@ -281,7 +281,7 @@ async function runBuild(input, env, log) {
281
281
  // path — three are the frozen CPython wording, and the fourth (the
282
282
  // numeric-overflow diagnostic, which has no CPython oracle wording
283
283
  // to match) now carries the path via its own rewrap in
284
- // src/manifest-overlay.ts. Emit as-is, with no added prefix — a
284
+ // src/harnesses/codex/manifest-overlay.ts. Emit as-is, with no added prefix — a
285
285
  // prefix here would double up the path these messages already
286
286
  // name.
287
287
  log.appendText("stderr", oneLine(cause));
@@ -1,14 +1,14 @@
1
1
  import { join } from "node:path";
2
- import { validateNativeSkill } from "./artifact-tree.js";
3
- import { classifyPathNoFollow, assertExistingContained } from "./safe-path.js";
2
+ import { validateNativeSkill } from "../../artifact-tree.js";
3
+ import { classifyPathNoFollow, assertExistingContained, } from "../../safe-path.js";
4
4
  import { classifyHooks, readManifest } from "./hooks.js";
5
- import { SEMVER_RE } from "./generated-plugin.js";
6
- import { validateSource } from "./selection.js";
5
+ import { SEMVER_RE } from "../../domain/refs.js";
6
+ import { validateSource } from "../../selection.js";
7
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";
8
+ import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, digestArtifactTree, readArtifactObject, } from "../../artifact-tree.js";
9
+ import { COMMIT_RE } from "../../domain/refs.js";
10
10
  import { validateGeneratedPlugin } from "./generated-plugin.js";
11
- import { SafetyError } from "./safety-error.js";
11
+ import { SafetyError } from "../../safety-error.js";
12
12
  export async function writeCodexAssessment(root, selection, manifestSource) {
13
13
  const receipt = {
14
14
  schema: 1,
@@ -1,6 +1,6 @@
1
1
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { codexInstalledPluginsFromJson, } from "./codex-json.js";
3
+ import { codexInstalledPluginsFromJson, } from "./json.js";
4
4
  export const CODEX_MANAGER_PLUGIN_ID = "superpowers@superpowers-manager";
5
5
  export const CODEX_LEGACY_PLUGIN_ID = "superpowers@superpowers-wrapper";
6
6
  const SUPERPOWERS_PLUGIN_PREFIX = "superpowers@";
@@ -1,20 +1,9 @@
1
- import { lstat, readdir, readFile, readlink, stat } from "node:fs/promises";
2
1
  import { homedir } from "node:os";
3
2
  import { posix } from "node:path";
4
- import { COMMIT_RE } from "./domain/refs.js";
5
- import { pythonSplitlines, pythonStrip } from "./python-text.js";
6
- import { parseStrictJson } from "./strict-json.js";
7
- export const DEFAULT_FS_DEPS = {
8
- lstat,
9
- stat,
10
- readdir,
11
- readlink,
12
- readFile,
13
- };
14
- // The Python's own SEMVER_RE (:15-22). Deliberately not SEMVER_BASE_SOURCE
15
- // from src/domain/refs.ts, which omits the `+build` component; widening the
16
- // shared constant would change TAG_RE and is out of scope.
17
- export const SEMVER_RE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
3
+ import { COMMIT_RE, SEMVER_RE } from "../../domain/refs.js";
4
+ import { compareByCodePoint, pythonStrip } from "../../python-text.js";
5
+ import { DEFAULT_FS_DEPS, validateSkillFrontmatter, } from "../../skill-validation.js";
6
+ import { parseStrictJson } from "../../strict-json.js";
18
7
  const MANIFEST_JSON_PROFILE = {
19
8
  duplicateKeys: "last-wins",
20
9
  nonStandardConstants: "reject",
@@ -443,61 +432,6 @@ const REQUIRED_FILES = [
443
432
  "README.md",
444
433
  "CODE_OF_CONDUCT.md",
445
434
  ];
446
- /** Python `sorted()` orders by code point; JavaScript's default sort does not. */
447
- export function compareByCodePoint(left, right) {
448
- // `Array.from` splits by code point, exactly as spreading would; oxlint's
449
- // `no-misused-spread` rejects the spread form, and grapheme segmentation is
450
- // the wrong unit here — Python compares code points.
451
- const leftPoints = Array.from(left);
452
- const rightPoints = Array.from(right);
453
- const shared = Math.min(leftPoints.length, rightPoints.length);
454
- for (let index = 0; index < shared; index += 1) {
455
- const a = leftPoints[index].codePointAt(0);
456
- const b = rightPoints[index].codePointAt(0);
457
- if (a !== b)
458
- return a - b;
459
- }
460
- return leftPoints.length - rightPoints.length;
461
- }
462
- export async function validateSkillFrontmatter(skillMd, skillName, errors, deps) {
463
- let contents;
464
- try {
465
- contents = STRICT_DECODER.decode(await deps.readFile(skillMd));
466
- }
467
- catch {
468
- errors.push(`skill \`${skillName}\` has unreadable UTF-8 \`SKILL.md\``);
469
- return;
470
- }
471
- if (contents === "") {
472
- errors.push(`skill \`${skillName}\` has empty \`SKILL.md\``);
473
- return;
474
- }
475
- const lines = pythonSplitlines(contents);
476
- if (lines.length === 0 || lines[0] !== "---") {
477
- errors.push(`skill \`${skillName}\` must start with \`---\``);
478
- return;
479
- }
480
- const closingIndex = lines.indexOf("---", 1);
481
- if (closingIndex === -1) {
482
- errors.push(`skill \`${skillName}\` frontmatter is not closed`);
483
- return;
484
- }
485
- const frontmatter = lines.slice(1, closingIndex);
486
- for (const key of ["name", "description"]) {
487
- const matches = frontmatter.filter((line) => line.startsWith(`${key}:`));
488
- if (matches.length !== 1) {
489
- errors.push(`skill \`${skillName}\` frontmatter must contain exactly one top-level \`${key}:\``);
490
- continue;
491
- }
492
- const value = pythonStrip(matches[0].slice(matches[0].indexOf(":") + 1));
493
- if (value === "" ||
494
- value === "''" ||
495
- value === '""' ||
496
- value.startsWith("#")) {
497
- errors.push(`skill \`${skillName}\` frontmatter field \`${key}\` must be non-empty`);
498
- }
499
- }
500
- }
501
435
  async function validateHookSubtree(pluginRoot, hooksRoot, errors, deps) {
502
436
  let resolvedRoot;
503
437
  try {
@@ -513,9 +447,9 @@ async function validateHookSubtree(pluginRoot, hooksRoot, errors, deps) {
513
447
  isLink = (await inspectLink(path, deps)) === "symlink";
514
448
  }
515
449
  catch {
516
- // The `src/generated-plugin.ts:667::isLink =` probe reaches the first catch bounded by `src/generated-plugin.ts:672-676::let rawTarget`; the `src/generated-plugin.ts:678::rawTarget = decodePathBytes` readlink reaches the same site text.
517
- // The three probes sharing the *subtree* string are `src/generated-plugin.ts:711::resolvedDirectory =`, `src/generated-plugin.ts:721::children =`, and
518
- // `src/generated-plugin.ts:730::deps, true)) === "directory"`, not this one.
450
+ // The `src/harnesses/codex/generated-plugin.ts:583::isLink =` probe reaches the first catch bounded by `src/harnesses/codex/generated-plugin.ts:592::let rawTarget`; the `src/harnesses/codex/generated-plugin.ts:594::rawTarget = decodePathBytes` readlink reaches the same site text.
451
+ // The three probes sharing the *subtree* string are `src/harnesses/codex/generated-plugin.ts:627::resolvedDirectory =`, `src/harnesses/codex/generated-plugin.ts:637::children =`, and
452
+ // `src/harnesses/codex/generated-plugin.ts:646::deps, true)) === "directory"`, not this one.
519
453
  errors.push(`generated hook symlink could not be inspected: ${path}`);
520
454
  return false;
521
455
  }
@@ -1,11 +1,11 @@
1
1
  import { join, resolve } from "node:path";
2
- import { failureResult, hasTerminalControl, successResult, } from "./adapter-result.js";
2
+ import { failureResult, hasTerminalControl, successResult, } from "../../adapter-result.js";
3
3
  import { codexInspect, codexInstall, codexRemove, } from "./adapter.js";
4
- import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./codex-prepare.js";
5
- import { codexInstallReceipt, codexPresentation, } from "./codex-presentation.js";
4
+ import { codexPreparationLocation, inspectCodexPrepared, prepareCodexCandidate, readCodexPrepared, validateCodexPreparationBeforeFetch, } from "./prepare.js";
5
+ import { codexInstallReceipt, codexPresentation } from "./presentation.js";
6
6
  import { reportLegacyState, requireManagedUpdateControl, requireNoLegacyState, verifyUninstalledResources, } from "./lifecycle.js";
7
- import { SafetyError } from "./safety-error.js";
8
- import { commitMatches } from "./status.js";
7
+ import { SafetyError } from "../../safety-error.js";
8
+ import { commitMatches } from "../../status.js";
9
9
  function preserveFailure(result) {
10
10
  if (result.outcome.ok) {
11
11
  throw new Error("cannot preserve a successful adapter result as failure");
@@ -1,8 +1,8 @@
1
1
  import { copyFile, cp, mkdir, readdir, readFile, readlink, stat, symlink, } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, join, resolve } from "node:path";
3
- import { assertExistingContained, assertProspectiveContained, classifyPathNoFollow, } from "./safe-path.js";
4
- import { SafetyError } from "./safety-error.js";
5
- import { parseStrictJson } from "./strict-json.js";
3
+ import { assertExistingContained, assertProspectiveContained, classifyPathNoFollow, } from "../../safe-path.js";
4
+ import { SafetyError } from "../../safety-error.js";
5
+ import { parseStrictJson } from "../../strict-json.js";
6
6
  // Annotated, not `as const`, matching
7
7
  // `src/provenance.ts:20::export const PROVENANCE_STRICT_PROFILE` and
8
8
  // `src/selection-store.ts:19::const SELECTION_JSON_PROFILE`. The annotation
@@ -77,11 +77,11 @@ export async function readManifest(path) {
77
77
  catch {
78
78
  // Deliberately drops the cause on both branches. `detail(cause)` on the
79
79
  // read branch surfaced a raw errno (`ENOENT: … open '<path>'`), which the
80
- // `hook classification failed:` site in src/adapter.ts re-emits onto the
80
+ // `hook classification failed:` site in src/harnesses/codex/adapter.ts re-emits onto the
81
81
  // terminal these commands write to; the parse branch surfaced
82
- // strict-json's own wording under a prefix src/manifest-overlay.ts also
82
+ // strict-json's own wording under a prefix src/harnesses/codex/manifest-overlay.ts also
83
83
  // uses with CPython wording.
84
- // Same text as the `cannot read manifest JSON in` site in src/adapter.ts.
84
+ // Same text as the `cannot read manifest JSON in` site in src/harnesses/codex/adapter.ts.
85
85
  throw hookError(`cannot read manifest JSON in ${path}`);
86
86
  }
87
87
  let parsed;
@@ -1,5 +1,5 @@
1
- import { SafetyError } from "./safety-error.js";
2
- import { parseStrictJson, } from "./strict-json.js";
1
+ import { SafetyError } from "../../safety-error.js";
2
+ import { parseStrictJson, } from "../../strict-json.js";
3
3
  const ACCEPT_CONSTANTS = {
4
4
  duplicateKeys: "last-wins",
5
5
  nonStandardConstants: "accept",
@@ -52,7 +52,7 @@ export function reportLegacyState(identityState) {
52
52
  }
53
53
  return unknownState(identityState);
54
54
  }
55
- export { verifyInstalledFingerprint, } from "./codex-presentation.js";
55
+ export { verifyInstalledFingerprint, } from "./presentation.js";
56
56
  // Ported from
57
57
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:62-70::spw_require_managed_update_control`.
58
58
  // Both refusals reached spw_die in the shell, so both carry the `error: ` prefix
@@ -101,7 +101,7 @@ export function verifyUninstalledResources(inspectResult) {
101
101
  // rather than getting its own message.
102
102
  // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/adapter.sh:70::expected`
103
103
  // emits "expected Boolean adapter result at resources.plugin" for input {} —
104
- // the input tests/unit/lifecycle.test.js's "a non-object resources falls
104
+ // the input tests/unit/harnesses/codex/lifecycle.test.ts's "a non-object resources falls
105
105
  // through to the Boolean message" exercises — so a distinct "not an object"
106
106
  // message here would be a port-only divergence. Parity, not divergence. Spec
107
107
  // §6.2.3 item 3.
@@ -1,6 +1,6 @@
1
- import { escapeNonAscii, formatPythonNumber } from "./python-json-format.js";
2
- import { SafetyError } from "./safety-error.js";
3
- import { isRawNumber, isRawObject, parseStrictJsonPreservingNumbers, } from "./strict-json.js";
1
+ import { escapeNonAscii, formatPythonNumber, } from "../../python-json-format.js";
2
+ import { SafetyError } from "../../safety-error.js";
3
+ import { isRawNumber, isRawObject, parseStrictJsonPreservingNumbers, } from "../../strict-json.js";
4
4
  const OVERLAY_PROFILE = {
5
5
  duplicateKeys: "last-wins",
6
6
  nonStandardConstants: "reject",
@@ -12,7 +12,7 @@ const OVERLAY_PROFILE = {
12
12
  * Byte-for-byte equivalent to the CPython implementation this replaces:
13
13
  * `json.dump(data, f, indent=2, allow_nan=False)` plus a trailing newline.
14
14
  * The byte-parity evidence lives in
15
- * `tests/baseline/manifest-overlay-parity.test.js` (tracked as
15
+ * `tests/baseline/harnesses/codex/manifest-overlay-parity.test.ts` (tracked as
16
16
  * `MANIFEST-READER-OVERLAY-01` in `docs/baseline/traceability.md`) against
17
17
  * the fixtures under `tests/fixtures/baseline/overlay-parity/`. Known,
18
18
  * deliberate divergences from strict byte parity are recorded in
@@ -1,14 +1,14 @@
1
1
  import { cp, mkdir, rm, stat } from "node:fs/promises";
2
2
  import { isAbsolute, join, resolve } from "node:path";
3
- import { failureResult, successResult, } from "./adapter-result.js";
3
+ import { failureResult, successResult, } from "../../adapter-result.js";
4
4
  import { codexBuild } from "./adapter.js";
5
- import { ARTIFACT_RECEIPT } from "./artifact-tree.js";
6
- import { assessCodexCompatibility, readCodexAssessment, writeCodexAssessment, } from "./codex-compatibility.js";
5
+ import { ARTIFACT_RECEIPT } from "../../artifact-tree.js";
6
+ import { assessCodexCompatibility, readCodexAssessment, writeCodexAssessment, } from "./compatibility.js";
7
7
  import { readManifest } from "./hooks.js";
8
- import { readGeneratedCommitLenient, readStrictProvenanceField, writeProvenance, } from "./provenance.js";
9
- import { classifyPathNoFollow } from "./safe-path.js";
10
- import { SafetyError } from "./safety-error.js";
11
- import { manifestVersionForRef } from "./upstream-version.js";
8
+ import { readGeneratedCommitLenient, readStrictProvenanceField, writeProvenance, } from "../../provenance.js";
9
+ import { classifyPathNoFollow } from "../../safe-path.js";
10
+ import { SafetyError } from "../../safety-error.js";
11
+ import { manifestVersionForRef } from "../../upstream-version.js";
12
12
  // Order is inherited from the original prepare command; the first miss wins.
13
13
  const REQUIRED_UPSTREAM = [
14
14
  { path: "skills", label: "skills/" },
@@ -1,5 +1,5 @@
1
- import { normalizeCodexInstall, normalizeCodexInstalled, } from "./codex-harness.js";
2
- import { displaySource } from "./selection.js";
1
+ import { normalizeCodexInstall, normalizeCodexInstalled } from "./harness.js";
2
+ import { displaySource } from "../../selection.js";
3
3
  // One ordered table drives both formats, preserving the public human and
4
4
  // porcelain field order exposed through the harness presentation.
5
5
  export function fields(f) {
@@ -116,7 +116,7 @@ export function codexInstallReceipt(missingHint, mismatchHint) {
116
116
  mismatchVerificationOutput: verificationOutput("mismatch", mismatchHint),
117
117
  };
118
118
  }
119
- // Compatibility export target for src/lifecycle.ts. Delegate to the production
119
+ // Compatibility export target for src/harnesses/codex/lifecycle.ts. Delegate to the production
120
120
  // normalizers and renderer so the retained verdict cannot drift from commands.
121
121
  export function verifyInstalledFingerprint(desiredCommit, installResult, inspectResult) {
122
122
  const receipt = normalizeCodexInstall(installResult);
@@ -1,8 +1,8 @@
1
1
  import { readFile, realpath } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
- import { COMMIT_INPUT_RE } from "./domain/refs.js";
4
- import { SafetyError } from "./safety-error.js";
5
- import { parseStrictJson, } from "./strict-json.js";
3
+ import { COMMIT_INPUT_RE } from "../../domain/refs.js";
4
+ import { SafetyError } from "../../safety-error.js";
5
+ import { parseStrictJson, } from "../../strict-json.js";
6
6
  const INSTALLED_PROFILE = {
7
7
  duplicateKeys: "last-wins",
8
8
  nonStandardConstants: "reject",
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { readArtifactFile, readArtifactObject, validateNativeSkill, } from "./artifact-tree.js";
5
- import { SEMVER_RE } from "./generated-plugin.js";
6
- import { validateSource } from "./selection.js";
4
+ import { readArtifactFile, readArtifactObject, validateNativeSkill, } from "../../artifact-tree.js";
5
+ import { SEMVER_RE } from "../../domain/refs.js";
6
+ import { validateSource } from "../../selection.js";
7
7
  const OFFICIAL_SOURCES = new Set([
8
8
  "https://github.com/obra/superpowers",
9
9
  "https://github.com/obra/superpowers.git",
@@ -1,10 +1,10 @@
1
- import { failureResult, successResult } from "./adapter-result.js";
2
- import { classifyPathNoFollow } from "./safe-path.js";
3
- import { installPi, removePi } from "./pi-install.js";
4
- import { piPaths } from "./pi-paths.js";
5
- import { inspectPiPrepared, piPreparationLocation, preparePiCandidate, readPiPrepared, } from "./pi-prepare.js";
6
- import { piPresentation } from "./pi-presentation.js";
7
- import { inspectPiControl, inspectPiInstalled, inspectPiOwnership, } from "./pi-state.js";
1
+ import { failureResult, successResult } from "../../adapter-result.js";
2
+ import { classifyPathNoFollow } from "../../safe-path.js";
3
+ import { installPi, removePi } from "./install.js";
4
+ import { piPaths } from "./paths.js";
5
+ import { inspectPiPrepared, piPreparationLocation, preparePiCandidate, readPiPrepared, } from "./prepare.js";
6
+ import { piPresentation } from "./presentation.js";
7
+ import { inspectPiControl, inspectPiInstalled, inspectPiOwnership, } from "./state.js";
8
8
  export const piHarness = {
9
9
  preparationLocation: piPreparationLocation,
10
10
  mutationRoots: async (ctx) => [
@@ -1,14 +1,14 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { cp, lstat, mkdir, open, readFile, readdir, rm, rmdir, unlink, } from "node:fs/promises";
3
3
  import { basename, dirname, join } from "node:path";
4
- import { failureResult, successResult, } from "./adapter-result.js";
5
- import { atomicWriteFile, beginDirectoryPublication, } from "./atomic.js";
6
- import { normalizePiRuntimeVersion, runPi } from "./pi-native.js";
7
- import { digestPiTree, readPiPackageAssessment, readPiReceipt, } from "./pi-package.js";
8
- import { piPaths } from "./pi-paths.js";
9
- import { readPiSettings, resolveCanonicalPiLocalSource, } from "./pi-settings.js";
10
- import { inspectPiControl, inspectPiOwnership, } from "./pi-state.js";
11
- import { assertNoFollowType, canonicalizeProspectivePath, classifyPathNoFollow, } from "./safe-path.js";
4
+ import { failureResult, successResult, } from "../../adapter-result.js";
5
+ import { atomicWriteFile, beginDirectoryPublication, } from "../../atomic.js";
6
+ import { normalizePiRuntimeVersion, runPi } from "./native.js";
7
+ import { digestPiTree, readPiPackageAssessment, readPiReceipt, } from "./package.js";
8
+ import { piPaths } from "./paths.js";
9
+ import { readPiSettings, resolveCanonicalPiLocalSource, } from "./settings.js";
10
+ import { inspectPiControl, inspectPiOwnership, } from "./state.js";
11
+ import { assertNoFollowType, canonicalizeProspectivePath, classifyPathNoFollow, } from "../../safe-path.js";
12
12
  const DEFAULTS = {
13
13
  run: runPi,
14
14
  beginPublication: beginDirectoryPublication,
@@ -1,9 +1,9 @@
1
1
  import { tmpdir } from "node:os";
2
2
  import { isAbsolute, resolve, sep } from "node:path";
3
- import { AdapterMessageLog, failureResult, successResult, } from "./adapter-result.js";
4
- import { SEMVER_RE } from "./generated-plugin.js";
5
- import { BOUNDED_EXECUTABLE, runValidator as runBoundedCommand, } from "./validator.js";
6
- import { withWorkspace } from "./workspace.js";
3
+ import { AdapterMessageLog, failureResult, successResult, } from "../../adapter-result.js";
4
+ import { SEMVER_RE } from "../../domain/refs.js";
5
+ import { BOUNDED_EXECUTABLE, runValidator as runBoundedCommand, } from "../../validator.js";
6
+ import { withWorkspace } from "../../workspace.js";
7
7
  function selectedExecutable(env, invocationCwd) {
8
8
  const configured = env.SUPERPOWERS_PI || "pi";
9
9
  return configured.includes(sep) && !isAbsolute(configured)
@@ -3,12 +3,12 @@ import { createHash } from "node:crypto";
3
3
  import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
- import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, addArtifactHashField, digestArtifactTree, readArtifactObject, } from "./artifact-tree.js";
7
- import { COMMIT_RE } from "./domain/refs.js";
8
- import { assertNoFollowType, assertSymlinkTargetContained, } from "./safe-path.js";
9
- import { SafetyError } from "./safety-error.js";
10
- import { validateSource } from "./selection.js";
11
- import { assessPiCompatibility } from "./pi-compatibility.js";
6
+ import { ARTIFACT_DIGEST_RE, ARTIFACT_RECEIPT, addArtifactHashField, digestArtifactTree, readArtifactObject, } from "../../artifact-tree.js";
7
+ import { COMMIT_RE } from "../../domain/refs.js";
8
+ import { assertNoFollowType, assertSymlinkTargetContained, } from "../../safe-path.js";
9
+ import { SafetyError } from "../../safety-error.js";
10
+ import { validateSource } from "../../selection.js";
11
+ import { assessPiCompatibility } from "./compatibility.js";
12
12
  const exec = promisify(execFile);
13
13
  const DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
14
14
  // A consistency checksum, not authentication. A coherent manual rewrite of
@@ -1,11 +1,11 @@
1
1
  import { writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { failureResult, successResult, } from "./adapter-result.js";
4
- import { ARTIFACT_RECEIPT } from "./artifact-tree.js";
5
- import { piPaths } from "./pi-paths.js";
6
- import { assessPiCompatibility, samePiSource } from "./pi-compatibility.js";
7
- import { digestPiTree, materializePiTree, readPiPackageAssessment, piReceiptBinding, } from "./pi-package.js";
8
- import { classifyPathNoFollow } from "./safe-path.js";
3
+ import { failureResult, successResult, } from "../../adapter-result.js";
4
+ import { ARTIFACT_RECEIPT } from "../../artifact-tree.js";
5
+ 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";
9
9
  export function piPreparationLocation(ctx) {
10
10
  return {
11
11
  destinationRoot: piPaths(ctx.env ?? {}, process.cwd()).preparedRoot,
@@ -1,9 +1,9 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { canonicalizeProspectivePath } from "./safe-path.js";
5
- import { SafetyError } from "./safety-error.js";
6
- import { parseStrictJson, } from "./strict-json.js";
4
+ import { canonicalizeProspectivePath } from "../../safe-path.js";
5
+ import { SafetyError } from "../../safety-error.js";
6
+ import { parseStrictJson, } from "../../strict-json.js";
7
7
  const SETTINGS_PROFILE = {
8
8
  duplicateKeys: "last-wins",
9
9
  nonStandardConstants: "reject",
@@ -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 } 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("/");
@@ -1,12 +1,12 @@
1
1
  import { join } from "node:path";
2
- import { sharedPiSkillsActivity } from "./pi-shared-skills.js";
3
- import { failureResult, successResult, } from "./adapter-result.js";
4
- import { readArtifactObject } from "./artifact-tree.js";
5
- import { samePiSource } from "./pi-compatibility.js";
6
- import { digestPiTree, readPiPackageAssessment, readPiReceipt, } from "./pi-package.js";
7
- import { piPaths } from "./pi-paths.js";
8
- import { readPiSettings, resolveCanonicalPiLocalSource, resolvePiLocalSource, } from "./pi-settings.js";
9
- import { classifyPathNoFollow } from "./safe-path.js";
2
+ import { sharedPiSkillsActivity } from "./shared-skills.js";
3
+ import { failureResult, successResult, } from "../../adapter-result.js";
4
+ import { readArtifactObject } from "../../artifact-tree.js";
5
+ import { samePiSource } from "./compatibility.js";
6
+ import { digestPiTree, readPiPackageAssessment, readPiReceipt, } from "./package.js";
7
+ import { piPaths } from "./paths.js";
8
+ import { readPiSettings, resolveCanonicalPiLocalSource, resolvePiLocalSource, } from "./settings.js";
9
+ import { classifyPathNoFollow } from "../../safe-path.js";
10
10
  // Verified remote spellings only: Pi 0.85.1 accepts http:// as Git. Unprefixed
11
11
  // github: and git+ forms stay local; git:-wrapped aliases and explicit transports
12
12
  // normalize only an exact GitHub authority and repository path (native source
@@ -72,3 +72,19 @@ export function pythonSplitlines(value) {
72
72
  lines.push(value.slice(start));
73
73
  return lines;
74
74
  }
75
+ /** Python `sorted()` orders by code point; JavaScript's default sort does not. */
76
+ export function compareByCodePoint(left, right) {
77
+ // `Array.from` splits by code point, exactly as spreading would; oxlint's
78
+ // `no-misused-spread` rejects the spread form, and grapheme segmentation is
79
+ // the wrong unit here — Python compares code points.
80
+ const leftPoints = Array.from(left);
81
+ const rightPoints = Array.from(right);
82
+ const shared = Math.min(leftPoints.length, rightPoints.length);
83
+ for (let index = 0; index < shared; index += 1) {
84
+ const a = leftPoints[index].codePointAt(0);
85
+ const b = rightPoints[index].codePointAt(0);
86
+ if (a !== b)
87
+ return a - b;
88
+ }
89
+ return leftPoints.length - rightPoints.length;
90
+ }
@@ -0,0 +1,52 @@
1
+ import { lstat, readdir, readFile, readlink, stat } from "node:fs/promises";
2
+ import { pythonSplitlines, pythonStrip } from "./python-text.js";
3
+ export const DEFAULT_FS_DEPS = {
4
+ lstat,
5
+ stat,
6
+ readdir,
7
+ readlink,
8
+ readFile,
9
+ };
10
+ const STRICT_DECODER = new TextDecoder("utf-8", {
11
+ fatal: true,
12
+ ignoreBOM: true,
13
+ });
14
+ export async function validateSkillFrontmatter(skillMd, skillName, errors, deps) {
15
+ let contents;
16
+ try {
17
+ contents = STRICT_DECODER.decode(await deps.readFile(skillMd));
18
+ }
19
+ catch {
20
+ errors.push(`skill \`${skillName}\` has unreadable UTF-8 \`SKILL.md\``);
21
+ return;
22
+ }
23
+ if (contents === "") {
24
+ errors.push(`skill \`${skillName}\` has empty \`SKILL.md\``);
25
+ return;
26
+ }
27
+ const lines = pythonSplitlines(contents);
28
+ if (lines.length === 0 || lines[0] !== "---") {
29
+ errors.push(`skill \`${skillName}\` must start with \`---\``);
30
+ return;
31
+ }
32
+ const closingIndex = lines.indexOf("---", 1);
33
+ if (closingIndex === -1) {
34
+ errors.push(`skill \`${skillName}\` frontmatter is not closed`);
35
+ return;
36
+ }
37
+ const frontmatter = lines.slice(1, closingIndex);
38
+ for (const key of ["name", "description"]) {
39
+ const matches = frontmatter.filter((line) => line.startsWith(`${key}:`));
40
+ if (matches.length !== 1) {
41
+ errors.push(`skill \`${skillName}\` frontmatter must contain exactly one top-level \`${key}:\``);
42
+ continue;
43
+ }
44
+ const value = pythonStrip(matches[0].slice(matches[0].indexOf(":") + 1));
45
+ if (value === "" ||
46
+ value === "''" ||
47
+ value === '""' ||
48
+ value.startsWith("#")) {
49
+ errors.push(`skill \`${skillName}\` frontmatter field \`${key}\` must be non-empty`);
50
+ }
51
+ }
52
+ }
@@ -1,6 +1,6 @@
1
1
  import { realpathSync } from "node:fs";
2
2
  import { oneLine, parseFlags, UsageError } from "./cli-arguments.js";
3
- import { validateGeneratedPlugin, } from "./generated-plugin.js";
3
+ import { validateGeneratedPlugin, } from "./harnesses/codex/generated-plugin.js";
4
4
  const FLAGS = [
5
5
  "plugin-root",
6
6
  "source",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "superpowers-manager",
3
- "version": "0.5.0",
4
- "description": "Install and update the latest stable obra/superpowers release directly from upstream for Codex and Pi (pi-coding-agent), without waiting for agent marketplaces to catch up.",
3
+ "version": "0.5.1",
4
+ "description": "Install obra/superpowers directly from upstream for Codex and Pi. Stay on a known-good release or commit, try a branch, and upgrade when you choose.",
5
5
  "keywords": [
6
6
  "superpowers",
7
7
  "obra-superpowers",
@@ -16,7 +16,8 @@
16
16
  "installer",
17
17
  "updater",
18
18
  "pi",
19
- "pi-coding-agent"
19
+ "pi-coding-agent",
20
+ "version-pinning"
20
21
  ],
21
22
  "license": "MIT",
22
23
  "type": "module",
File without changes