superpowers-manager 0.8.0 → 0.9.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 (66) 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 +33 -3
  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 +36 -69
  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 +28 -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 +2 -4
  39. package/dist/harnesses/opencode/discovery.js +1 -5
  40. package/dist/harnesses/opencode/install.js +12 -93
  41. package/dist/harnesses/opencode/native.js +28 -49
  42. package/dist/harnesses/opencode/package.js +11 -55
  43. package/dist/harnesses/opencode/prepare.js +2 -7
  44. package/dist/harnesses/opencode/presentation.js +2 -7
  45. package/dist/harnesses/pi/install.js +31 -120
  46. package/dist/harnesses/pi/native.js +9 -34
  47. package/dist/harnesses/pi/package.js +9 -50
  48. package/dist/harnesses/pi/prepare.js +3 -8
  49. package/dist/harnesses/pi/presentation.js +2 -7
  50. package/dist/harnesses/pi/settings.js +5 -9
  51. package/dist/harnesses/pi/shared-skills.js +23 -53
  52. package/dist/harnesses/pi/state.js +3 -10
  53. package/dist/provenance.js +3 -19
  54. package/dist/resource-lock.js +11 -16
  55. package/dist/safe-path.js +28 -1
  56. package/dist/selection.js +15 -19
  57. package/dist/snapshot-journal.js +101 -0
  58. package/dist/snapshot-package.js +62 -1
  59. package/dist/snapshot-prepare.js +9 -2
  60. package/dist/snapshot-presentation.js +9 -0
  61. package/dist/upstream.js +8 -12
  62. package/dist/validator.js +26 -23
  63. package/dist/workspace.js +29 -35
  64. package/package.json +24 -13
  65. package/dist/python-json.js +0 -4
  66. package/dist/upstream-workspace.js +0 -8
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Superpowers Manager
2
2
 
3
- Install [`obra/superpowers`](https://github.com/obra/superpowers) directly from upstream for Codex, the Pi coding agent, and OpenCode. 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.
3
+ Install [`obra/superpowers`](https://github.com/obra/superpowers) directly from upstream for Codex, the Pi coding agent, OpenCode, and Claude Code. 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
 
@@ -21,7 +21,7 @@ npx superpowers-manager pin v6.1.1
21
21
  npx superpowers-manager install --harness codex
22
22
  ```
23
23
 
24
- Use `--harness pi` for Pi or `--harness opencode` for OpenCode. The pin is shared by all harnesses, 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.
24
+ Use `--harness pi` for Pi, `--harness opencode` for OpenCode, or `--harness claude-code` for Claude Code. The pin is shared by all harnesses, 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
25
 
26
26
  To try a branch, replace `feature/foo` with an existing upstream branch:
27
27
 
@@ -42,7 +42,7 @@ See [version selection](https://github.com/j7an/superpowers-manager/blob/main/do
42
42
 
43
43
  ## Start with your agent
44
44
 
45
- The three harnesses share upstream selection, while each has independent prepared and installed state. Omitting `--harness` selects Codex.
45
+ The four harnesses share upstream selection, while each has independent prepared and installed state. Omitting `--harness` selects Codex.
46
46
 
47
47
  ### Codex
48
48
 
@@ -84,6 +84,19 @@ OpenCode after a successful install, activating update, or removal. See the
84
84
  [OpenCode reference](https://github.com/j7an/superpowers-manager/blob/main/docs/opencode.md)
85
85
  for global configuration, conflicts, and recovery behavior.
86
86
 
87
+ ### Claude Code
88
+
89
+ ```sh
90
+ npx superpowers-manager install --harness claude-code
91
+ npx superpowers-manager probe --harness claude-code
92
+ npx superpowers-manager update --harness claude-code
93
+ ```
94
+
95
+ Claude Code installs a validated, Manager-owned local marketplace snapshot.
96
+ Restart Claude Code or run `/reload-plugins` after an install, activating update,
97
+ or removal. See the [Claude Code reference](https://github.com/j7an/superpowers-manager/blob/main/docs/claude-code.md)
98
+ for conflicts, hooks, and recovery.
99
+
87
100
  To remove a Manager-owned installation, choose one harness explicitly:
88
101
 
89
102
  ### Uninstall Codex
@@ -104,23 +117,30 @@ npx superpowers-manager uninstall --harness pi
104
117
  npx superpowers-manager uninstall --harness opencode
105
118
  ```
106
119
 
120
+ ### Uninstall Claude Code
121
+
122
+ ```sh
123
+ npx superpowers-manager uninstall --harness claude-code
124
+ ```
125
+
107
126
  The manager never removes another Superpowers provider. See the [ownership guidance](https://github.com/j7an/superpowers-manager/blob/main/docs/usage.md#provider-ownership) before switching providers.
108
127
 
109
128
  ## Shared selection and lifecycle
110
129
 
111
- The [selection commands above](#choose-what-you-run) save shared upstream intent. They do not activate Codex, Pi, or OpenCode; run the selected harness's `install` or `update` to apply that choice. `unpin` restores the packaged fallback policy.
130
+ The [selection commands above](#choose-what-you-run) save shared upstream intent. They do not activate Codex, Pi, OpenCode, or Claude Code; run the selected harness's `install` or `update` to apply that choice. `unpin` restores the packaged fallback policy.
112
131
 
113
132
  See [targeted lifecycle commands](https://github.com/j7an/superpowers-manager/blob/main/docs/usage.md#targeted-lifecycle) for preparation, inspection, installation, updates, and removal.
114
133
 
115
- Use `--harness codex`, `--harness pi`, or `--harness opencode` with each targeted lifecycle command.
134
+ Use `--harness codex`, `--harness pi`, `--harness opencode`, or `--harness claude-code` with each targeted lifecycle command.
116
135
 
117
136
  ```text
118
137
  shared selection -> prepare / inspect / activate Codex
119
138
  -> prepare / inspect / activate Pi
120
139
  -> prepare / inspect / activate OpenCode
140
+ -> prepare / inspect / activate Claude Code
121
141
  ```
122
142
 
123
- The [usage reference](https://github.com/j7an/superpowers-manager/blob/main/docs/usage.md) covers version selection, precedence, offline behavior, `prepare`, validators, and every lifecycle command. The harness references cover [Codex registration and hooks](https://github.com/j7an/superpowers-manager/blob/main/docs/codex.md) and [Pi snapshots and recovery](https://github.com/j7an/superpowers-manager/blob/main/docs/pi.md).
143
+ The [usage reference](https://github.com/j7an/superpowers-manager/blob/main/docs/usage.md) covers version selection, precedence, offline behavior, `prepare`, validators, and every lifecycle command. The harness references cover [Codex registration and hooks](https://github.com/j7an/superpowers-manager/blob/main/docs/codex.md), [Pi snapshots and recovery](https://github.com/j7an/superpowers-manager/blob/main/docs/pi.md), [OpenCode snapshots](https://github.com/j7an/superpowers-manager/blob/main/docs/opencode.md), and [Claude Code marketplace registration](https://github.com/j7an/superpowers-manager/blob/main/docs/claude-code.md).
124
144
 
125
145
  ## Requirements and platforms
126
146
 
@@ -128,16 +148,16 @@ The installed package requires Node >=24. Native source, tests, and packaging to
128
148
 
129
149
  <!-- requirements:begin -->
130
150
 
131
- | Command | git | Codex CLI (default) | Pi CLI (`--harness pi`) | OpenCode CLI (`--harness opencode`) |
132
- | -------------- | --- | ------------------- | ----------------------- | ----------------------------------- |
133
- | `pin` | yes | no | no | no |
134
- | `track-latest` | no | no | no | no |
135
- | `unpin` | no | no | no | no |
136
- | `prepare` | yes | no | no | no |
137
- | `probe` | yes | yes | no | no |
138
- | `install` | yes | yes | yes | yes |
139
- | `update` | yes | yes | yes | yes |
140
- | `uninstall` | no | yes | yes | no |
151
+ | Command | git | Codex CLI (default) | Pi CLI (`--harness pi`) | OpenCode CLI (`--harness opencode`) | Claude Code CLI (`--harness claude-code`) |
152
+ | -------------- | --- | ------------------- | ----------------------- | ----------------------------------- | ----------------------------------------- |
153
+ | `pin` | yes | no | no | no | no |
154
+ | `track-latest` | no | no | no | no | no |
155
+ | `unpin` | no | no | no | no | no |
156
+ | `prepare` | yes | no | no | no | no |
157
+ | `probe` | yes | yes | no | no | yes |
158
+ | `install` | yes | yes | yes | yes | yes |
159
+ | `update` | yes | yes | yes | yes | yes |
160
+ | `uninstall` | no | yes | yes | no | yes |
141
161
 
142
162
  <!-- requirements:end -->
143
163
 
package/dist/atomic.js CHANGED
@@ -17,9 +17,7 @@ export async function atomicWriteFile(path, bytes, options) {
17
17
  break;
18
18
  }
19
19
  catch (cause) {
20
- if (cause instanceof Error &&
21
- "code" in cause &&
22
- cause.code === "EEXIST") {
20
+ if (isErrno(cause, "EEXIST")) {
23
21
  continue;
24
22
  }
25
23
  throw cause;
@@ -20,7 +20,10 @@ export function extractHarnessOptions(command, argv) {
20
20
  : token.slice("--harness=".length);
21
21
  if (!value || value.startsWith("--"))
22
22
  throw new UsageError("option --harness requires a value");
23
- if (value !== "codex" && value !== "pi" && value !== "opencode")
23
+ if (value !== "codex" &&
24
+ value !== "pi" &&
25
+ value !== "opencode" &&
26
+ value !== "claude-code")
24
27
  throw new UsageError(`unknown harness: ${value}`);
25
28
  harness = value;
26
29
  }
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ 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
5
  import { codexHarness } from "./harnesses/codex/harness.js";
6
+ import { claudeCodeHarness } from "./harnesses/claude-code/harness.js";
6
7
  import { piHarness } from "./harnesses/pi/harness.js";
7
8
  import { openCodeHarness } from "./harnesses/opencode/harness.js";
8
9
  import { createResourceCoordinator } from "./resource-lock.js";
@@ -16,23 +17,8 @@ import { runUnpin } from "./commands/unpin.js";
16
17
  import { runUpdate } from "./commands/update.js";
17
18
  import { COMMIT_INPUT_RE, TAG_RE } from "./domain/refs.js";
18
19
  import { configurationErrors } from "./validator.js";
19
- const SUBCOMMANDS = [
20
- "pin",
21
- "track-latest",
22
- "unpin",
23
- "prepare",
24
- "probe",
25
- "install",
26
- "update",
27
- "uninstall",
28
- ];
29
- // Keyed by Subcommand itself. Until slice 6 this was keyed by a mapped type
30
- // that filtered Subcommand through a DISPATCH table of "spawn" | "in-process"
31
- // literals; with every command in-process that filter was the identity, so the
32
- // table, its mode union and the mapped type were one indirection with nothing
33
- // left to discriminate. The guarantee is unchanged and is the whole protection
34
- // — tests/bin/bin-dispatch.test.js records that decision: a Subcommand added
35
- // without a handler registered here is a compile error, not a runtime surprise.
20
+ // Keyed by HarnessCommand: a command added to that type without a handler
21
+ // here is a compile error, not a runtime surprise.
36
22
  const IN_PROCESS_HANDLERS = {
37
23
  pin: runPin,
38
24
  "track-latest": runTrackLatest,
@@ -98,7 +84,7 @@ function parseArgs(argv) {
98
84
  return { kind: "help" };
99
85
  if (first === "--version")
100
86
  return { kind: "version" };
101
- if (first && SUBCOMMANDS.includes(first)) {
87
+ if (first && Object.hasOwn(IN_PROCESS_HANDLERS, first)) {
102
88
  const command = first;
103
89
  let extracted;
104
90
  try {
@@ -228,7 +214,7 @@ function usage() {
228
214
  " install --harness opencode [--allow-experimental]",
229
215
  " update --harness opencode [--allow-experimental]",
230
216
  " uninstall --harness opencode",
231
- " --harness=codex, --harness=pi, and --harness=opencode are also accepted after the command.",
217
+ " --harness=codex, --harness=pi, --harness=opencode, and --harness=claude-code are also accepted after the command.",
232
218
  "",
233
219
  "Environment overrides (used by in-process commands): SUPERPOWERS_REF,",
234
220
  "SUPERPOWERS_UPSTREAM_URL, SUPERPOWERS_CODEX, SUPERPOWERS_CACHE_DIR,",
@@ -236,7 +222,7 @@ function usage() {
236
222
  "SUPERPOWERS_PLUGIN_ROOT, SUPERPOWERS_MANIFEST_TEMPLATE,",
237
223
  "SUPERPOWERS_VALIDATOR_EXECUTABLE,",
238
224
  "SUPERPOWERS_INSTALLED_SEARCH_ROOT, SUPERPOWERS_INSTALL_REFRESH_MODE,",
239
- "SUPERPOWERS_OPENCODE",
225
+ "SUPERPOWERS_OPENCODE, SUPERPOWERS_CLAUDE_CODE, CLAUDE_CONFIG_DIR",
240
226
  "",
241
227
  "SUPERPOWERS_VALIDATOR is removed; unset it and use",
242
228
  "SUPERPOWERS_VALIDATOR_EXECUTABLE with an executable validator.",
@@ -266,38 +252,23 @@ async function main() {
266
252
  console.error(usage());
267
253
  process.exit(2);
268
254
  }
269
- const status = parsed.options.harness === "opencode"
270
- ? await dispatch(openCodeHarness, parsed)
271
- : parsed.options.harness === "pi"
272
- ? await dispatch(piHarness, parsed)
273
- : await dispatch(codexHarness, parsed);
255
+ const status = parsed.options.harness === "claude-code"
256
+ ? await dispatch(claudeCodeHarness, parsed, root)
257
+ : parsed.options.harness === "opencode"
258
+ ? await dispatch(openCodeHarness, parsed, root)
259
+ : parsed.options.harness === "pi"
260
+ ? await dispatch(piHarness, parsed, root)
261
+ : await dispatch(codexHarness, parsed, root);
274
262
  process.exit(status);
275
263
  }
276
- async function dispatch(adapter, parsed) {
277
- const root = resolvePackageRoot(import.meta.filename);
278
- if (!root) {
279
- console.error("error: cannot resolve the superpowers-manager package root");
280
- return 1;
281
- }
264
+ async function dispatch(adapter, parsed, root) {
282
265
  const pf = preflight(parsed.cmd, process.env, process.platform, adapter);
283
266
  if (!pf.ok) {
284
267
  for (const e of pf.errors)
285
268
  console.error(`error: ${e}`);
286
269
  return 1;
287
270
  }
288
- // Dispatch is no longer a branch: slice 4b flipped the last spawned command,
289
- // so every subcommand runs here, and slice 6 deleted the DISPATCH table whose
290
- // only remaining job was narrowing this registry's key type. `parsed.cmd` is
291
- // a Subcommand and the registry is keyed by Subcommand, so no cast is needed.
292
- // The exhaustiveness check on IN_PROCESS_HANDLERS makes an unregistered
293
- // handler a compile error; the `!handler` guard below is the runtime backstop
294
- // for that guarantee. It is unreachable through production code, and no
295
- // fixture reaches it either.
296
271
  const handler = IN_PROCESS_HANDLERS[parsed.cmd];
297
- if (!handler) {
298
- console.error(`error: no in-process handler registered for: ${parsed.cmd}`);
299
- return 1;
300
- }
301
272
  const ctx = {
302
273
  root,
303
274
  env: process.env,
@@ -26,3 +26,27 @@ export class GatherFailure extends Error {
26
26
  this.outcomes = outcomes;
27
27
  }
28
28
  }
29
+ export function writeOutput(output, ctx) {
30
+ for (const line of output.stdout)
31
+ ctx.stdout.write(`${line}\n`);
32
+ for (const line of output.stderr)
33
+ ctx.stderr.write(`${line}\n`);
34
+ }
35
+ // callAdapter plus outcome collection: every returned result's outcome is
36
+ // recorded for replay. acceptValue turns a successful value it rejects into an
37
+ // invalid status without recording that outcome.
38
+ export async function invoke(call, failure, outcomes, acceptValue) {
39
+ let result = await callAdapter(call, failure);
40
+ if (result.ok && acceptValue !== undefined) {
41
+ try {
42
+ if (!acceptValue(result.result.outcome.result))
43
+ result = { ok: false, result: null, message: failure.invalidStatus };
44
+ }
45
+ catch {
46
+ result = { ok: false, result: null, message: failure.invalidStatus };
47
+ }
48
+ }
49
+ if (result.result !== null)
50
+ outcomes.push(result.result.outcome);
51
+ return result;
52
+ }
@@ -1,40 +1,21 @@
1
1
  import { tmpdir } from "node:os";
2
2
  import { oneLine } from "../cli-arguments.js";
3
- import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
4
- import { GatherFailure, callAdapter, } from "./adapter-call.js";
5
- import { gatherProbe, replayOutcome } from "./probe.js";
3
+ import { withWorkspaceReporting, } from "../workspace.js";
4
+ import { GatherFailure, invoke, writeOutput } from "./adapter-call.js";
5
+ import { probeFacts, replayOutcome } from "./probe.js";
6
6
  import { runPrepare } from "./prepare.js";
7
7
  import { runWithMutation } from "./mutation.js";
8
8
  import { activationBlock } from "../harness-compatibility.js";
9
- export function writeOutput(output, ctx) {
10
- for (const line of output.stdout)
11
- ctx.stdout.write(`${line}\n`);
12
- for (const line of output.stderr)
13
- ctx.stderr.write(`${line}\n`);
14
- }
15
- async function invoke(call, failure, outcomes, acceptValue) {
16
- let result = await callAdapter(call, failure);
17
- if (result.ok && acceptValue !== undefined) {
18
- try {
19
- if (!acceptValue(result.result.outcome.result))
20
- result = { ok: false, result: null, message: failure.invalidStatus };
21
- }
22
- catch {
23
- result = { ok: false, result: null, message: failure.invalidStatus };
24
- }
25
- }
26
- if (result.result !== null)
27
- outcomes.push(result.result.outcome);
28
- return result;
29
- }
30
9
  // Collect outcomes without writing so output failures cannot be classified as
31
10
  // inspection failures. Replay collected outcomes after gathering completes.
11
+ // The callback never throws: invoke() catches every ctx.adapter failure and
12
+ // every predicate here is pure, so a post-success cleanup failure is the only
13
+ // case withWorkspaceReporting's cleanupWarning has to carry.
32
14
  async function gatherInstallStages(ctx, selection, artifact) {
33
15
  const parent = ctx.env.TMPDIR ?? tmpdir();
34
16
  const outcomes = [];
35
- let cleanupWarning = null;
36
17
  try {
37
- const outcome = await withWorkspace(parent, "superpowers-manager.install.", async (workspace) => {
18
+ return await withWorkspaceReporting(parent, "superpowers-manager.install.", async (workspace) => {
38
19
  const env = { ...ctx.env, TMPDIR: workspace };
39
20
  const failed = (message) => ({
40
21
  kind: "failed",
@@ -187,53 +168,25 @@ async function gatherInstallStages(ctx, selection, artifact) {
187
168
  stdout: output.stdout,
188
169
  stderr: output.stderr,
189
170
  };
190
- }, {
191
- // Suppresses withWorkspace's throw on a POST-SUCCESS cleanup failure,
192
- // so the StageOutcome the callback already computed still comes back
193
- // as `outcome` below instead of being discarded. `report` runs
194
- // synchronously, as the option requires (src/workspace.ts).
195
- onCleanupFailure: (path) => {
196
- cleanupWarning = workspaceRemovalFailure(path);
197
- },
198
171
  });
199
- return { outcome, cleanupWarning };
200
172
  }
201
173
  catch (cause) {
202
- // Reachable only for mkdtemp failure (nothing collected yet) -- the
203
- // callback above never throws, so a post-success cleanup failure is
204
- // already handled by onCleanupFailure and cannot reach here.
174
+ // Reachable only for mkdtemp failure (nothing collected yet): the
175
+ // callback never throws, and a post-success cleanup failure comes back
176
+ // as cleanupWarning.
205
177
  throw new GatherFailure("install gather failed", cause, outcomes);
206
178
  }
207
179
  }
208
180
  export async function runInstall(argv, ctx) {
209
181
  if (ctx.adapter.presentation.installNotice !== "")
210
182
  ctx.stdout.write(`${ctx.adapter.presentation.installNotice}\n`);
211
- return await runWithMutation("install", ctx, async (scoped) => performInstall(argv, scoped));
183
+ // argv is ignored: scripts/install never reads "$@".
184
+ return await runWithMutation("install", ctx, async (scoped) => performInstall(scoped));
212
185
  }
213
- async function performInstall(argv, ctx) {
214
- // scripts/install never reads "$@", so extra arguments are silently
215
- // ignored -- the same asymmetry runPrepare and runUninstall document.
216
- void argv;
217
- let probe;
218
- try {
219
- probe = await gatherProbe(ctx);
220
- }
221
- catch (cause) {
222
- // Gathering does not write, so this catch cannot misclassify an output
223
- // failure. oneLine() bounds any inherited diagnostic to one line.
224
- ctx.stderr.write(`error: ${oneLine(cause)}\n`);
225
- return 1;
226
- }
227
- // Replay first, on both paths, before any decision -- clause 1.
228
- for (const outcome of probe.outcomes)
229
- replayOutcome(outcome, ctx);
230
- if (probe.status === 1) {
231
- if (probe.message !== null) {
232
- ctx.stderr.write(`error: ${probe.message}\n`);
233
- }
186
+ async function performInstall(ctx) {
187
+ const facts = await probeFacts(ctx);
188
+ if (facts === null)
234
189
  return 1;
235
- }
236
- const facts = probe.facts;
237
190
  if (facts.ownership.installEligibility.kind === "blocked") {
238
191
  writeOutput(facts.ownership.installEligibility.output, ctx);
239
192
  return 1;
@@ -282,7 +235,7 @@ async function performInstall(argv, ctx) {
282
235
  ctx.stderr.write(`error: ${oneLine(inner)}\n`);
283
236
  return 1;
284
237
  }
285
- const { outcome, cleanupWarning } = stage;
238
+ const { value: outcome, cleanupWarning } = stage;
286
239
  for (const each of outcome.outcomes)
287
240
  replayOutcome(each, ctx);
288
241
  let status;
@@ -1,5 +1,5 @@
1
1
  import { computeEffectiveSelection } from "../effective-selection.js";
2
- import { upstreamCacheRoot } from "../upstream-workspace.js";
2
+ import { upstreamCacheRoot } from "../upstream.js";
3
3
  import { SafetyError } from "../safety-error.js";
4
4
  import { isAbsolute } from "node:path";
5
5
  import { oneLine } from "../cli-arguments.js";
@@ -42,13 +42,13 @@ export async function withMutation(command, ctx, action) {
42
42
  }
43
43
  const resources = [location.destinationRoot, ...mutationRoots];
44
44
  if (command === "prepare") {
45
- resources.push(upstreamCacheRoot(ctx.root, ctx.env, process.cwd()));
45
+ resources.push(upstreamCacheRoot(ctx.root, ctx.env));
46
46
  }
47
47
  // The coordinator sorts each acquisition and refuses conflicts immediately.
48
48
  // A nested prepare may add a cache lock that sorts before an outer target;
49
49
  // it cannot deadlock because no acquisition waits, while already-held roots
50
50
  // are reentrant for this invocation.
51
- return await ctx.coordination.withResources(resources, async () => action(scoped));
51
+ return await ctx.coordination.withResources(resources, async () => action(scoped, location));
52
52
  }
53
53
  export async function runWithMutation(command, ctx, action) {
54
54
  const errors = configurationErrors(command, ctx.env);
@@ -59,9 +59,9 @@ export async function runWithMutation(command, ctx, action) {
59
59
  }
60
60
  let actionThrew = false;
61
61
  try {
62
- return await withMutation(command, ctx, async (scoped) => {
62
+ return await withMutation(command, ctx, async (scoped, location) => {
63
63
  try {
64
- return await action(scoped);
64
+ return await action(scoped, location);
65
65
  }
66
66
  catch (cause) {
67
67
  actionThrew = true;
@@ -2,7 +2,7 @@ import { tmpdir } from "node:os";
2
2
  import { oneLine } from "../cli-arguments.js";
3
3
  import { isTagRef, normalizeCommitInput } from "../domain/refs.js";
4
4
  import { loadSavedSelection, selectionStatePath, UPSTREAM_URL_DEFAULT, } from "../effective-selection.js";
5
- import { normalizePinnedArguments, validateSource } from "../selection.js";
5
+ import { validateSource } from "../selection.js";
6
6
  import { writeSelectionState } from "../selection-store.js";
7
7
  import { resolveExactTag, verifyRawCommit } from "../upstream.js";
8
8
  // Runs every step that can throw, and performs no writes of its own: that is
@@ -36,12 +36,17 @@ async function attemptPin(requested, ctx) {
36
36
  resolvedRef = normalizeCommitInput(requested);
37
37
  commit = await verifyRawCommit(source, resolvedRef, tmpdir());
38
38
  }
39
- await writeSelectionState(selectionStatePath(ctx.env), normalizePinnedArguments({
39
+ // resolvedRef is already normalized (lowercase for a raw commit; a v-tag is
40
+ // unchanged), and commit comes lowercase from verifyRawCommit or git
41
+ // ls-remote. validateRecord still rejects anything else.
42
+ await writeSelectionState(selectionStatePath(ctx.env), {
43
+ schema_version: 1,
44
+ mode: "pinned",
40
45
  source,
41
- requestedRef: resolvedRef,
42
- resolvedRef,
46
+ requested_ref: resolvedRef,
47
+ resolved_ref: resolvedRef,
43
48
  commit,
44
- }));
49
+ });
45
50
  return { resolvedRef, commit };
46
51
  }
47
52
  export async function runPin(argv, ctx) {