superpowers-manager 0.2.0 → 0.4.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 (68) hide show
  1. package/README.md +340 -66
  2. package/dist/adapter-result.js +204 -0
  3. package/dist/adapter.js +690 -0
  4. package/dist/atomic.js +142 -0
  5. package/dist/cli-arguments.js +32 -0
  6. package/dist/cli.js +313 -0
  7. package/dist/codex-harness.js +271 -0
  8. package/dist/codex-json.js +89 -0
  9. package/dist/codex-prepare.js +199 -0
  10. package/dist/codex-presentation.js +259 -0
  11. package/dist/codex-state.js +78 -0
  12. package/dist/commands/context.js +1 -0
  13. package/dist/commands/install.js +291 -0
  14. package/dist/commands/pin.js +79 -0
  15. package/dist/commands/prepare.js +420 -0
  16. package/dist/commands/probe.js +257 -0
  17. package/dist/commands/track-latest.js +44 -0
  18. package/dist/commands/uninstall.js +217 -0
  19. package/dist/commands/unpin.js +95 -0
  20. package/dist/commands/update.js +79 -0
  21. package/dist/domain/fingerprint.js +4 -0
  22. package/dist/domain/refs.js +37 -0
  23. package/dist/effective-selection.js +123 -0
  24. package/dist/generated-plugin.js +748 -0
  25. package/dist/git.js +38 -0
  26. package/dist/harness.js +1 -0
  27. package/dist/hooks.js +290 -0
  28. package/dist/lifecycle.js +135 -0
  29. package/dist/manifest-overlay.js +125 -0
  30. package/dist/provenance.js +111 -0
  31. package/dist/python-json-format.js +83 -0
  32. package/dist/python-json.js +29 -0
  33. package/dist/python-text.js +74 -0
  34. package/dist/safe-path.js +137 -0
  35. package/dist/safety-error.js +10 -0
  36. package/dist/selection-state-cli.js +85 -0
  37. package/dist/selection-store.js +172 -0
  38. package/dist/selection.js +200 -0
  39. package/dist/status.js +24 -0
  40. package/dist/strict-json.js +248 -0
  41. package/dist/upstream-cli.js +110 -0
  42. package/dist/upstream-version.js +40 -0
  43. package/dist/upstream.js +253 -0
  44. package/dist/validate-generated-plugin-cli.js +118 -0
  45. package/dist/validator.js +317 -0
  46. package/dist/workspace.js +139 -0
  47. package/package.json +25 -4
  48. package/bin/superpowers-manager.js +0 -290
  49. package/scripts/adapters/codex/adapter +0 -603
  50. package/scripts/adapters/codex/lib.sh +0 -449
  51. package/scripts/adapters/codex/validate-generated-plugin.py +0 -314
  52. package/scripts/core/adapter.sh +0 -103
  53. package/scripts/core/common.sh +0 -59
  54. package/scripts/core/lifecycle.sh +0 -141
  55. package/scripts/core/provenance.sh +0 -135
  56. package/scripts/core/selection-state.py +0 -414
  57. package/scripts/core/selection.sh +0 -168
  58. package/scripts/core/status.sh +0 -27
  59. package/scripts/core/upstream.sh +0 -280
  60. package/scripts/core/validate-adapter-response.py +0 -284
  61. package/scripts/install +0 -59
  62. package/scripts/pin +0 -51
  63. package/scripts/prepare +0 -117
  64. package/scripts/probe +0 -82
  65. package/scripts/track-latest +0 -30
  66. package/scripts/uninstall +0 -35
  67. package/scripts/unpin +0 -41
  68. package/scripts/update +0 -34
@@ -0,0 +1,79 @@
1
+ import { tmpdir } from "node:os";
2
+ import { oneLine } from "../cli-arguments.js";
3
+ import { isTagRef, normalizeCommitInput } from "../domain/refs.js";
4
+ import { loadSavedSelection, selectionStatePath, UPSTREAM_URL_DEFAULT, } from "../effective-selection.js";
5
+ import { normalizePinnedArguments, validateSource } from "../selection.js";
6
+ import { writeSelectionState } from "../selection-store.js";
7
+ import { resolveExactTag, verifyRawCommit } from "../upstream.js";
8
+ // Runs every step that can throw, and performs no writes of its own: that is
9
+ // the point of this shape (mirrors src/commands/unpin.ts's attemptUnpin) —
10
+ // runPin's try (below) can therefore never itself raise EPIPE, since nothing
11
+ // left inside it writes to a stream.
12
+ async function attemptPin(requested, ctx) {
13
+ // Read first. This is a deliberate redundant boundary check, not the
14
+ // enforcing guard: writeSelectionState below already refuses to overwrite
15
+ // a corrupt existing record on its own (src/selection-store.ts's
16
+ // writeSelectionState calls readSelectionState on the same path before
17
+ // writing, under "Invalid existing state must block overwrite" —
18
+ // readSelectionState's own readOpenedRecord/parseRecordBytes is what
19
+ // validates an existing record, via validateRecord at
20
+ // `src/selection-store.ts:103::return validateRecord`). Calling loadSavedSelection here preserves
21
+ // `git show 349fe2ed405b371ec2de1347bb3fc50c6bc15dc4:scripts/pin:26-27::read`'s read-then-resolve-then-write shape, so this command
22
+ // stays fail-closed on its own terms rather than solely by depending on the
23
+ // store's internals.
24
+ await loadSavedSelection(ctx.env);
25
+ const source = ctx.env.SUPERPOWERS_UPSTREAM_URL || UPSTREAM_URL_DEFAULT;
26
+ validateSource(source);
27
+ let resolvedRef;
28
+ let commit;
29
+ if (isTagRef(requested)) {
30
+ resolvedRef = requested;
31
+ commit = await resolveExactTag(source, requested);
32
+ }
33
+ else {
34
+ // `git show 349fe2ed405b371ec2de1347bb3fc50c6bc15dc4:scripts/pin:39::upper` lowercases a raw-commit request before use; both
35
+ // requested_ref and resolved_ref end up equal to the normalized commit.
36
+ resolvedRef = normalizeCommitInput(requested);
37
+ commit = await verifyRawCommit(source, resolvedRef, tmpdir());
38
+ }
39
+ await writeSelectionState(selectionStatePath(ctx.env), normalizePinnedArguments({
40
+ source,
41
+ requestedRef: resolvedRef,
42
+ resolvedRef,
43
+ commit,
44
+ }));
45
+ return { resolvedRef, commit };
46
+ }
47
+ export async function runPin(argv, ctx) {
48
+ // Arity and ref syntax are already decided in src/cli.ts's parseArgs (the
49
+ // TAG_RE / COMMIT_INPUT_RE check ahead of dispatch) and must not be
50
+ // duplicated here — one source, checked before any tool lookup.
51
+ const requested = argv[0];
52
+ let result;
53
+ try {
54
+ result = await attemptPin(requested, ctx);
55
+ }
56
+ catch (cause) {
57
+ // Every throw reachable here is a hand-written SafetyError from
58
+ // loadSavedSelection, validateSource, resolveExactTag, verifyRawCommit,
59
+ // or writeSelectionState — re-emitting a subordinate module's own
60
+ // diagnostic is the sanctioned form of interpolation (see AGENTS.md's
61
+ // diagnostics convention). That does not make every such message clean:
62
+ // resolveExactTag's and verifyRawCommit's SafetyErrors splice git's own
63
+ // combined stdout+stderr into their text (src/upstream.ts's `combined()`,
64
+ // used at :219 for `ls-remote` and :262 for `init`), so raw git output can
65
+ // reach ctx.stderr through this catch. Not a regression — scripts/pin
66
+ // piped git's output into its own error text the same way — but `pin` is
67
+ // the first in-process command able to surface it. oneLine() collapses
68
+ // that spliced output to one line, containing the harm to one line of
69
+ // git text rather than the arbitrarily many the shell original allowed.
70
+ // attemptPin performs no writes of its own (see above), so this catch
71
+ // cannot also be reached by an EPIPE from a write this function made
72
+ // itself — the write below runs only after this try/catch has already
73
+ // resolved.
74
+ ctx.stderr.write(`error: ${oneLine(cause)}\n`);
75
+ return 1;
76
+ }
77
+ ctx.stdout.write(`pinned upstream selection to ${result.resolvedRef} at ${result.commit}\n`);
78
+ return 0;
79
+ }
@@ -0,0 +1,420 @@
1
+ import { mkdir, stat } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
3
+ import { atomicReplaceDir } from "../atomic.js";
4
+ import { oneLine } from "../cli-arguments.js";
5
+ import { computeEffectiveSelection } from "../effective-selection.js";
6
+ import { runGit } from "../git.js";
7
+ import { SafetyError } from "../safety-error.js";
8
+ import { fetchExactCommit, gitSafeSource } from "../upstream.js";
9
+ import { BOUNDED_EXECUTABLE, UNBOUNDED_LEGACY, launchFailureMessage, resolveValidator, runValidator, displayPath, } from "../validator.js";
10
+ import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
11
+ import { replayOutcome } from "./probe.js";
12
+ // Every message this module writes is hand-written here. The cause is attached
13
+ // for debuggability and never reaches a stream: oneLine (src/cli-arguments.ts)
14
+ // reads .message only. Same arrangement as hookError
15
+ // (`src/hooks.ts:44::function hookError`).
16
+ function prepareError(message, cause) {
17
+ return new SafetyError("prepare", message, { cause });
18
+ }
19
+ // `[ -f ]` — regular file. `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:108::[ -f "$additional_validator` uses -f, and
20
+ // tests/baseline/cli-parity.test.js's "CLI-ENV-MANIFEST-TEMPLATE-01 fallback
21
+ // template bytes and non-file rejection" also covers the separately extracted
22
+ // fallback check.
23
+ async function regularFileExists(path) {
24
+ try {
25
+ return (await stat(path)).isFile();
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ // `[ -d ]` — `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:50::if [ -d`. A regular file named `.git` is what a git
32
+ // worktree or `clone --separate-git-dir` leaves behind; `-e` would take the
33
+ // fetch branch and let git follow its `gitdir:` pointer, where the shell took
34
+ // the clone branch. `src/upstream.ts:332::if (!(await isDirectory` makes the
35
+ // same distinction.
36
+ async function directoryExists(path) {
37
+ try {
38
+ return (await stat(path)).isDirectory();
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ // mkdir throws raw ErrnoExceptions. Every command-owned call goes through here
45
+ // so the message on the stream is this module's, not Node's.
46
+ async function owned(message, fn) {
47
+ try {
48
+ return await fn();
49
+ }
50
+ catch (cause) {
51
+ throw prepareError(message, cause);
52
+ }
53
+ }
54
+ // Each stream's marker goes to that stream's own destination, so a reader of either
55
+ // sees its own truncation and neither is silently short.
56
+ function withTruncationMarker(captured, stream) {
57
+ if (captured.droppedBytes === 0)
58
+ return captured.text;
59
+ return `${captured.text}\n[superpowers-manager: validator ${stream} truncated, ${captured.droppedBytes} bytes dropped]\n`;
60
+ }
61
+ // D8's disclosure. The variable is ambient process env and nothing filters it, so a
62
+ // consumer's CI can set it; naming what actually ran is what makes a supply-chain
63
+ // surprise visible. Paths go through displayPath, so a control character in either
64
+ // one cannot reach the terminal raw.
65
+ function disclosureLine(resolution) {
66
+ const configured = displayPath(resolution.configured);
67
+ if (resolution.resolved === null) {
68
+ // The manager does not know the final target and does not pretend to. But a
69
+ // dangling symlink is a KNOWN fact even when resolution fails, and saying "the
70
+ // OS selects it" about a path-like value would be wrong.
71
+ if (resolution.isSymlink) {
72
+ return `[superpowers-manager: running external validator ${configured} (a symlink whose target could not be resolved)]\n`;
73
+ }
74
+ if (configured.includes(sep)) {
75
+ return `[superpowers-manager: running external validator ${configured} (unresolved)]\n`;
76
+ }
77
+ return `[superpowers-manager: running external validator ${configured} (a bare name; PATH selects the file, and the manager does not guess which)]\n`;
78
+ }
79
+ const via = resolution.isSymlink ? " via symlink" : "";
80
+ return `[superpowers-manager: running external validator ${configured}${via} -> ${displayPath(resolution.resolved)}]\n`;
81
+ }
82
+ const NO_VALIDATOR_OUTPUT = { stdout: "", stderr: "" };
83
+ function validStagingLeaf(value) {
84
+ return (value.length > 0 &&
85
+ value !== "." &&
86
+ value !== ".." &&
87
+ !value.includes("/") &&
88
+ !value.includes("\\"));
89
+ }
90
+ async function gatherPrepare(ctx) {
91
+ const env = ctx.env;
92
+ // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:16::invocation_root=` — captured before the two case statements below.
93
+ // getcwd(3) returns the physical path, so this matches `pwd -P` without a
94
+ // realpath call.
95
+ const cwd = process.cwd();
96
+ const configuredCache = env.SUPERPOWERS_CACHE_DIR || join(ctx.root, ".cache", "upstream");
97
+ const cacheParent = isAbsolute(configuredCache)
98
+ ? configuredCache
99
+ : resolve(cwd, configuredCache);
100
+ const adapterContext = { root: ctx.root, env };
101
+ let location;
102
+ try {
103
+ location = ctx.adapter.preparationLocation(adapterContext);
104
+ }
105
+ catch (cause) {
106
+ throw prepareError("cannot determine preparation location", cause);
107
+ }
108
+ if (!isAbsolute(location.destinationRoot)) {
109
+ throw prepareError("adapter returned a non-absolute preparation destination");
110
+ }
111
+ if (!validStagingLeaf(location.stagingLeaf)) {
112
+ throw prepareError("adapter returned an invalid preparation staging leaf");
113
+ }
114
+ const pluginRoot = location.destinationRoot;
115
+ const additionalValidator = env.SUPERPOWERS_VALIDATOR || "";
116
+ const executableValidator = env.SUPERPOWERS_VALIDATOR_EXECUTABLE || "";
117
+ const cache = join(cacheParent, "superpowers");
118
+ const tmpParent = dirname(pluginRoot);
119
+ await owned(`cannot create directory: ${tmpParent}`, () => mkdir(tmpParent, { recursive: true }));
120
+ let cleanupWarning = null;
121
+ const outcome = await withWorkspace(tmpParent, ".superpowers.prepare.", async (workspace) => {
122
+ const outcomes = [];
123
+ const failed = (message) => ({
124
+ kind: "failed",
125
+ outcomes,
126
+ validator: NO_VALIDATOR_OUTPUT,
127
+ message,
128
+ });
129
+ const candidate = join(workspace, location.stagingLeaf);
130
+ const selection = await computeEffectiveSelection(ctx.root, env);
131
+ let prefetch;
132
+ try {
133
+ prefetch =
134
+ await ctx.adapter.validatePreparationBeforeFetch(adapterContext);
135
+ }
136
+ catch {
137
+ return failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
138
+ .unexpected);
139
+ }
140
+ outcomes.push(prefetch.outcome);
141
+ if (!prefetch.outcome.ok)
142
+ return failed(null);
143
+ if (prefetch.status !== 0) {
144
+ return failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
145
+ .invalidStatus);
146
+ }
147
+ await owned(`cannot create directory: ${cacheParent}`, () => mkdir(cacheParent, { recursive: true }));
148
+ if (selection.selectionMode === "pinned") {
149
+ await fetchExactCommit(selection.effectiveSource, selection.desiredCommit, cache, workspace);
150
+ }
151
+ else {
152
+ const source = gitSafeSource(selection.effectiveSource);
153
+ if (await directoryExists(join(cache, ".git"))) {
154
+ const fetched = await runGit([
155
+ "-C",
156
+ cache,
157
+ "fetch",
158
+ "--tags",
159
+ "--prune",
160
+ "--",
161
+ source,
162
+ ]);
163
+ // runGit returns its status rather than throwing, so no git output is
164
+ // in scope. `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:52::spw_die "cannot fetch` names the source and nothing else, and
165
+ // computeEffectiveSelection ran validateSource first, so a
166
+ // credential-bearing source never reaches here.
167
+ if (fetched.status !== 0) {
168
+ return failed(`cannot fetch upstream repo: ${selection.effectiveSource}`);
169
+ }
170
+ }
171
+ else {
172
+ const cloned = await runGit(["clone", "--", source, cache]);
173
+ if (cloned.status !== 0) {
174
+ return failed(`cannot clone upstream repo: ${selection.effectiveSource}`);
175
+ }
176
+ }
177
+ }
178
+ const checkedOut = await runGit([
179
+ "-C",
180
+ cache,
181
+ "checkout",
182
+ "--detach",
183
+ selection.desiredCommit,
184
+ ]);
185
+ if (checkedOut.status !== 0) {
186
+ return failed(`cannot check out upstream commit: ${selection.desiredCommit}`);
187
+ }
188
+ let prepared;
189
+ try {
190
+ prepared = await ctx.adapter.prepareCandidate({
191
+ upstreamRoot: cache,
192
+ workspaceRoot: workspace,
193
+ candidateRoot: candidate,
194
+ selection,
195
+ }, adapterContext);
196
+ }
197
+ catch {
198
+ return failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
199
+ .unexpected);
200
+ }
201
+ outcomes.push(prepared.outcome);
202
+ if (prepared.status !== 0 || !prepared.outcome.ok) {
203
+ return failed(prepared.outcome.ok
204
+ ? ctx.adapter.presentation.callFailure("prepare", adapterContext)
205
+ .invalidStatus
206
+ : null);
207
+ }
208
+ if (prepared.outcome.result.root !== candidate) {
209
+ return failed("adapter returned an unexpected preparation root");
210
+ }
211
+ if (prepared.outcome.result.commit !== selection.desiredCommit) {
212
+ return failed("adapter returned an unexpected preparation commit");
213
+ }
214
+ let validator = NO_VALIDATOR_OUTPUT;
215
+ if (additionalValidator.length > 0) {
216
+ // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:108::[ -f "$additional_validator` — `[ -f ]`.
217
+ if (!(await regularFileExists(additionalValidator))) {
218
+ return {
219
+ kind: "failed",
220
+ outcomes,
221
+ validator,
222
+ message: `additional plugin validator not found: ${additionalValidator}`,
223
+ };
224
+ }
225
+ const ran = await runValidator(["python3", additionalValidator, candidate], UNBOUNDED_LEGACY, env, workspace);
226
+ if (ran.kind === "launchFailed") {
227
+ // PARITY, and it must stay a THROW. The legacy path rejects the workspace
228
+ // callback on a spawn failure; withWorkspace returns the callback error
229
+ // and the `outcomes` collected above are LOST. That loss is a recorded,
230
+ // deliberately unassigned defect. Returning a failed outcome here would
231
+ // replay those outcomes instead -- observably different control flow for
232
+ // CLI-ENV-VALIDATOR-01, which this PR is not authorized to change.
233
+ throw prepareError(`cannot execute additional plugin validator: ${additionalValidator}`, ran.cause);
234
+ }
235
+ validator = { stdout: ran.stdout.text, stderr: ran.stderr.text };
236
+ if (ran.kind !== "exited" || ran.code !== 0) {
237
+ return {
238
+ kind: "failed",
239
+ outcomes,
240
+ validator,
241
+ message: "additional plugin validation failed",
242
+ };
243
+ }
244
+ }
245
+ else if (executableValidator.length > 0) {
246
+ const resolution = await resolveValidator(executableValidator);
247
+ const ran = await runValidator([executableValidator, candidate], BOUNDED_EXECUTABLE, env, workspace);
248
+ // The disclosure lands BEFORE any early return, so a launch failure still
249
+ // reports what the manager tried to run and what it resolved to — exactly
250
+ // the path where an operator most needs it. It is assigned twice rather
251
+ // than once because `launchFailed` carries no captured streams: there was
252
+ // no process to capture from.
253
+ validator = { stdout: disclosureLine(resolution), stderr: "" };
254
+ if (ran.kind === "launchFailed") {
255
+ return {
256
+ kind: "failed",
257
+ outcomes,
258
+ validator,
259
+ message: launchFailureMessage(ran.errno, resolution),
260
+ };
261
+ }
262
+ validator = {
263
+ stdout: disclosureLine(resolution) +
264
+ withTruncationMarker(ran.stdout, "stdout"),
265
+ stderr: withTruncationMarker(ran.stderr, "stderr"),
266
+ };
267
+ if (ran.kind === "timedOut") {
268
+ return {
269
+ kind: "failed",
270
+ outcomes,
271
+ validator,
272
+ message: `external plugin validation timed out after ${Math.round(ran.afterMs / 1000)}s`,
273
+ };
274
+ }
275
+ if (ran.code !== 0) {
276
+ return {
277
+ kind: "failed",
278
+ outcomes,
279
+ validator,
280
+ message: "external plugin validation failed",
281
+ };
282
+ }
283
+ }
284
+ // The swap must run inside the workspace callback: withWorkspace removes
285
+ // the workspace on return, and the candidate lives in it.
286
+ //
287
+ // atomicReplaceDir's outer catch (`src/atomic.ts:208-215::if (cause`) wraps every
288
+ // non-SafetyError into a SafetyError, so the callee owns every failure on
289
+ // this path and re-emitting its own diagnostic is the sanctioned form of
290
+ // interpolation. The hand-written prefix carries the live root, which the
291
+ // callee's message does not.
292
+ try {
293
+ await atomicReplaceDir(candidate, pluginRoot);
294
+ }
295
+ catch (cause) {
296
+ return {
297
+ kind: "failed",
298
+ outcomes,
299
+ validator,
300
+ message: `cannot install generated tree into ${pluginRoot}: ${oneLine(cause)}`,
301
+ };
302
+ }
303
+ return {
304
+ kind: "ok",
305
+ outcomes,
306
+ validator,
307
+ resolvedRef: selection.resolvedRef,
308
+ commit: selection.desiredCommit,
309
+ };
310
+ }, {
311
+ // Suppresses withWorkspace's throw on a POST-SUCCESS cleanup failure, so
312
+ // the PrepareOutcome the callback already computed still reaches
313
+ // runPrepare instead of being discarded. The reporter runs synchronously,
314
+ // as the option requires (`src/workspace.ts:103-106::Must`).
315
+ onCleanupFailure: (path) => {
316
+ cleanupWarning = workspaceRemovalFailure(path);
317
+ },
318
+ });
319
+ return { outcome, cleanupWarning };
320
+ }
321
+ export async function runPrepare(argv, ctx) {
322
+ // scripts/prepare never reads "$@", so extra arguments are ignored. This is a
323
+ // deliberate asymmetry with probe, whose shell original rejected unknown
324
+ // arguments and whose arity therefore moved into parseArgs in slice 2.
325
+ void argv;
326
+ let run;
327
+ try {
328
+ run = await gatherPrepare(ctx);
329
+ }
330
+ catch (cause) {
331
+ // Hand-written messages, per AGENTS.md's reader-diagnostics rule.
332
+ // Reachable here: prepareError(), from this module's owned() wrappers,
333
+ // its two manifest-version checks, asResolutionKind, and the
334
+ // additional-validator branch's own throw on a legacy-validator launch
335
+ // failure (its cause is the shared runner's (src/validator.ts) captured
336
+ // spawn error, which oneLine never reads -- it takes .message only);
337
+ // readManifest's three hookError messages
338
+ // (`src/hooks.ts:113-138::readManifest`), pinned by
339
+ // `tests/unit/hooks.test.ts:95::void test("readManifest diagnostics` as carrying no reader vocabulary or
340
+ // errno; and SafetyErrors from gitSafeSource, writeProvenance, and
341
+ // withWorkspace.
342
+ //
343
+ // FOUR exceptions, all inherited and none a regression:
344
+ // 1. resolveRef splices git's combined stdout+stderr into its own text
345
+ // on the NON-PINNED path (src/upstream.ts), reached via
346
+ // computeEffectiveSelection (src/effective-selection.ts).
347
+ // This is the DEFAULT invocation -- plain `prepare`, `track-latest`,
348
+ // and any non-40-hex SUPERPOWERS_REF -- not an exotic corner. Pinned
349
+ // by
350
+ // `tests/unit/upstream.test.ts:453-462::void test("resolveRef reports a query failure for latest`,
351
+ // :471-481, and :483-501.
352
+ // 2. fetchExactCommit splices the same combined stdout+stderr into its
353
+ // own text on the PINNED path (both of its own splice sites in
354
+ // src/upstream.ts, and proveCommit's, which it calls). This is the
355
+ // rarer of the two raw-git-output paths, not the only one.
356
+ // 3. `src/selection-store.ts:120-124::cause.module === "selection") {`
357
+ // (same shape at :49, :86, :98) is the
358
+ // module AGENTS.md's `src/selection-store.ts` bullet grandfathers:
359
+ // it interpolates the caught error's own message, so Node errno
360
+ // prose can reach this stream -- sanctioned, nothing here needs
361
+ // fixing.
362
+ // 4. Every runGit call site in this module (fetch, clone, checkout) can
363
+ // reject instead of resolving: `src/git.ts:47-52::reject(new SafetyError` wraps every string
364
+ // errno other than ENOENT in
365
+ // `new SafetyError("git", \`cannot run git: ${failure.message}\`)`,
366
+ // and that Node spawn-level message reaches ctx.stderr through this
367
+ // catch. So the claim that the non-pinned clone/fetch/checkout
368
+ // branch names only the source is true for a non-zero *exit status*
369
+ // (handled explicitly below, in gatherPrepare) but false for a
370
+ // *spawn-level* failure -- runGit throws rather than returning a
371
+ // status in that case, and this outer catch is what stands between
372
+ // it and the stream.
373
+ //
374
+ // oneLine() at this catch collapses each of exceptions 1, 2, and 4 -- the
375
+ // three carrying git-derived text -- to a single line. It collapses CR/LF
376
+ // only, so it bounds how much of that text lands, not what it may
377
+ // contain.
378
+ //
379
+ // runAdapter's closing `throw cause` (src/adapter.ts) does NOT arrive
380
+ // here -- the call site catches it and converts it to a hand-written
381
+ // message per AGENTS.md's reader-diagnostics rule.
382
+ //
383
+ // gatherPrepare performs no writes of its own, so this catch cannot also be
384
+ // reached by an EPIPE from prepare's own output: every write below runs
385
+ // only after this try/catch has resolved.
386
+ ctx.stderr.write(`error: ${oneLine(cause)}\n`);
387
+ return 1;
388
+ }
389
+ const { outcome, cleanupWarning } = run;
390
+ for (const each of outcome.outcomes)
391
+ replayOutcome(each, ctx);
392
+ if (outcome.validator.stdout.length > 0) {
393
+ ctx.stdout.write(outcome.validator.stdout);
394
+ }
395
+ if (outcome.validator.stderr.length > 0) {
396
+ ctx.stderr.write(outcome.validator.stderr);
397
+ }
398
+ let status;
399
+ if (outcome.kind === "failed") {
400
+ if (outcome.message !== null) {
401
+ ctx.stderr.write(`error: ${outcome.message}\n`);
402
+ }
403
+ status = 1;
404
+ }
405
+ else {
406
+ ctx.stdout.write(`prepared ${outcome.resolvedRef} at ${outcome.commit}\n`);
407
+ status = 0;
408
+ }
409
+ if (cleanupWarning !== null) {
410
+ // A leaked workspace is reported even when the outcome above was itself a
411
+ // success: the generated-tree replacement that produced it already
412
+ // completed before cleanup ran, so it is not being reported as unverified
413
+ // -- but something did still go wrong, and AGENTS.md's fail-closed rule
414
+ // extends to it. Mirrors
415
+ // `src/commands/install.ts:395-403::if (cleanupWarning`.
416
+ ctx.stderr.write(`error: ${cleanupWarning}\n`);
417
+ return 1;
418
+ }
419
+ return status;
420
+ }