superpowers-manager 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -26
- package/dist/adapter-result.js +5 -9
- package/dist/atomic.js +4 -14
- package/dist/cli-arguments.js +1 -27
- package/dist/cli.js +32 -52
- package/dist/commands/adapter-call.js +28 -0
- package/dist/commands/install.js +26 -107
- package/dist/commands/mutation.js +7 -0
- package/dist/commands/pin.js +1 -1
- package/dist/commands/prepare.js +39 -129
- package/dist/commands/probe.js +35 -264
- package/dist/commands/track-latest.js +1 -1
- package/dist/commands/uninstall.js +16 -92
- package/dist/commands/update.js +5 -25
- package/dist/effective-selection.js +7 -9
- package/dist/git-tree.js +85 -0
- package/dist/harness-command-result.js +47 -0
- package/dist/harnesses/codex/adapter.js +231 -510
- package/dist/harnesses/codex/compatibility.js +2 -17
- package/dist/harnesses/codex/conflicts.js +2 -3
- package/dist/harnesses/codex/harness.js +18 -263
- package/dist/harnesses/codex/hooks.js +1 -1
- package/dist/harnesses/codex/json.js +1 -11
- package/dist/harnesses/codex/lifecycle.js +80 -72
- package/dist/harnesses/codex/manifest-overlay.js +23 -18
- package/dist/harnesses/codex/paths.js +14 -29
- package/dist/harnesses/codex/presentation.js +8 -37
- package/dist/harnesses/codex/publication.js +13 -37
- package/dist/harnesses/codex/recovery.js +5 -5
- package/dist/harnesses/codex/stored-state.js +1 -1
- package/dist/harnesses/opencode/config.js +256 -0
- package/dist/harnesses/opencode/discovery.js +587 -0
- package/dist/harnesses/opencode/harness.js +37 -0
- package/dist/harnesses/opencode/install.js +601 -0
- package/dist/harnesses/opencode/native.js +57 -0
- package/dist/harnesses/opencode/package.js +89 -0
- package/dist/harnesses/opencode/paths.js +38 -0
- package/dist/harnesses/opencode/prepare.js +30 -0
- package/dist/harnesses/opencode/presentation.js +28 -0
- package/dist/harnesses/opencode/state.js +174 -0
- package/dist/harnesses/pi/compatibility.js +5 -37
- package/dist/harnesses/pi/install.js +7 -5
- package/dist/harnesses/pi/native.js +3 -47
- package/dist/harnesses/pi/package.js +5 -107
- package/dist/harnesses/pi/prepare.js +13 -104
- package/dist/harnesses/pi/presentation.js +23 -66
- package/dist/harnesses/pi/shared-skills.js +28 -39
- package/dist/harnesses/pi/state.js +7 -27
- package/dist/provenance.js +2 -2
- package/dist/python-json-format.js +0 -69
- package/dist/python-json.js +2 -27
- package/dist/python-text.js +9 -69
- package/dist/safe-path.js +2 -2
- package/dist/selection-store.js +2 -22
- package/dist/snapshot-package.js +78 -0
- package/dist/snapshot-prepare.js +99 -0
- package/dist/snapshot-presentation.js +44 -0
- package/dist/validator.js +39 -73
- package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/node_modules/jsonc-parser/LICENSE.md +21 -0
- package/node_modules/jsonc-parser/README.md +364 -0
- package/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
- package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
- package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
- package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
- package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
- package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
- package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
- package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
- package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
- package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
- package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
- package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
- package/node_modules/jsonc-parser/package.json +37 -0
- package/package.json +8 -4
- package/dist/domain/fingerprint.js +0 -4
- package/dist/selection-state-cli.js +0 -85
- package/dist/status.js +0 -24
- package/dist/upstream-cli.js +0 -110
- package/dist/validate-generated-plugin-cli.js +0 -118
|
@@ -3,6 +3,7 @@ import { upstreamCacheRoot } from "../upstream-workspace.js";
|
|
|
3
3
|
import { SafetyError } from "../safety-error.js";
|
|
4
4
|
import { isAbsolute } from "node:path";
|
|
5
5
|
import { oneLine } from "../cli-arguments.js";
|
|
6
|
+
import { configurationErrors } from "../validator.js";
|
|
6
7
|
export async function withMutation(command, ctx, action) {
|
|
7
8
|
// Selection validation precedes every adapter call and lock creation. A
|
|
8
9
|
// nested mutation receives the already resolved value and cannot observe a
|
|
@@ -50,6 +51,12 @@ export async function withMutation(command, ctx, action) {
|
|
|
50
51
|
return await ctx.coordination.withResources(resources, async () => action(scoped));
|
|
51
52
|
}
|
|
52
53
|
export async function runWithMutation(command, ctx, action) {
|
|
54
|
+
const errors = configurationErrors(command, ctx.env);
|
|
55
|
+
if (errors.length > 0) {
|
|
56
|
+
for (const message of errors)
|
|
57
|
+
ctx.stderr.write(`error: ${message}\n`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
53
60
|
let actionThrew = false;
|
|
54
61
|
try {
|
|
55
62
|
return await withMutation(command, ctx, async (scoped) => {
|
package/dist/commands/pin.js
CHANGED
|
@@ -17,7 +17,7 @@ async function attemptPin(requested, ctx) {
|
|
|
17
17
|
// writing, under "Invalid existing state must block overwrite" —
|
|
18
18
|
// readSelectionState's own readOpenedRecord/parseRecordBytes is what
|
|
19
19
|
// validates an existing record, via validateRecord at
|
|
20
|
-
// `src/selection-store.ts:
|
|
20
|
+
// `src/selection-store.ts:99::return validateRecord`). Calling loadSavedSelection here preserves
|
|
21
21
|
// `git show 349fe2ed405b371ec2de1347bb3fc50c6bc15dc4:scripts/pin:26-27::read`'s read-then-resolve-then-write shape, so this command
|
|
22
22
|
// stays fail-closed on its own terms rather than solely by depending on the
|
|
23
23
|
// store's internals.
|
package/dist/commands/prepare.js
CHANGED
|
@@ -7,7 +7,7 @@ import { runGit } from "../git.js";
|
|
|
7
7
|
import { SafetyError } from "../safety-error.js";
|
|
8
8
|
import { fetchExactCommit, gitSafeSource } from "../upstream.js";
|
|
9
9
|
import { upstreamCacheRoot } from "../upstream-workspace.js";
|
|
10
|
-
import { BOUNDED_EXECUTABLE,
|
|
10
|
+
import { BOUNDED_EXECUTABLE, launchFailureMessage, resolveValidator, runValidator, displayPath, } from "../validator.js";
|
|
11
11
|
import { withWorkspace, workspaceRemovalFailure } from "../workspace.js";
|
|
12
12
|
import { replayOutcome } from "./probe.js";
|
|
13
13
|
import { runWithMutation } from "./mutation.js";
|
|
@@ -18,18 +18,6 @@ import { runWithMutation } from "./mutation.js";
|
|
|
18
18
|
function prepareError(message, cause) {
|
|
19
19
|
return new SafetyError("prepare", message, { cause });
|
|
20
20
|
}
|
|
21
|
-
// `[ -f ]` — regular file. `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:108::[ -f "$additional_validator` uses -f, and
|
|
22
|
-
// tests/baseline/cli-parity.test.js's "CLI-ENV-MANIFEST-TEMPLATE-01 fallback
|
|
23
|
-
// template bytes and non-file rejection" also covers the separately extracted
|
|
24
|
-
// fallback check.
|
|
25
|
-
async function regularFileExists(path) {
|
|
26
|
-
try {
|
|
27
|
-
return (await stat(path)).isFile();
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
21
|
// `[ -d ]` — `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:50::if [ -d`. A regular file named `.git` is what a git
|
|
34
22
|
// worktree or `clone --separate-git-dir` leaves behind; `-e` would take the
|
|
35
23
|
// fetch branch and let git follow its `gitdir:` pointer, where the shell took
|
|
@@ -90,10 +78,9 @@ function validStagingLeaf(value) {
|
|
|
90
78
|
!value.includes("\\"));
|
|
91
79
|
}
|
|
92
80
|
async function gatherPrepare(ctx) {
|
|
81
|
+
// Collect outcomes without writing so output failures cannot be classified as
|
|
82
|
+
// inspection failures. Replay collected outcomes after gathering completes.
|
|
93
83
|
const env = ctx.env;
|
|
94
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:16::invocation_root=` — captured before the two case statements below.
|
|
95
|
-
// getcwd(3) returns the physical path, so this matches `pwd -P` without a
|
|
96
|
-
// realpath call.
|
|
97
84
|
const cwd = process.cwd();
|
|
98
85
|
const cache = upstreamCacheRoot(ctx.root, env, cwd);
|
|
99
86
|
const cacheParent = dirname(cache);
|
|
@@ -112,37 +99,41 @@ async function gatherPrepare(ctx) {
|
|
|
112
99
|
throw prepareError("adapter returned an invalid preparation staging leaf");
|
|
113
100
|
}
|
|
114
101
|
const pluginRoot = location.destinationRoot;
|
|
115
|
-
const additionalValidator = env.SUPERPOWERS_VALIDATOR || "";
|
|
116
102
|
const executableValidator = env.SUPERPOWERS_VALIDATOR_EXECUTABLE || "";
|
|
117
103
|
const tmpParent = dirname(pluginRoot);
|
|
104
|
+
const outcomes = [];
|
|
105
|
+
const failed = (message) => ({
|
|
106
|
+
kind: "failed",
|
|
107
|
+
outcomes,
|
|
108
|
+
validator: NO_VALIDATOR_OUTPUT,
|
|
109
|
+
message,
|
|
110
|
+
});
|
|
111
|
+
const selection = ctx.selection ?? (await computeEffectiveSelection(ctx.root, env));
|
|
112
|
+
let prefetch;
|
|
113
|
+
try {
|
|
114
|
+
prefetch = await ctx.adapter.validatePreparationBeforeFetch(adapterContext);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return {
|
|
118
|
+
outcome: failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
|
|
119
|
+
.unexpected),
|
|
120
|
+
cleanupWarning: null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
outcomes.push(prefetch.outcome);
|
|
124
|
+
if (!prefetch.outcome.ok)
|
|
125
|
+
return { outcome: failed(null), cleanupWarning: null };
|
|
126
|
+
if (prefetch.status !== 0) {
|
|
127
|
+
return {
|
|
128
|
+
outcome: failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
|
|
129
|
+
.invalidStatus),
|
|
130
|
+
cleanupWarning: null,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
118
133
|
await owned(`cannot create directory: ${tmpParent}`, () => mkdir(tmpParent, { recursive: true }));
|
|
119
134
|
let cleanupWarning = null;
|
|
120
135
|
const outcome = await withWorkspace(tmpParent, ".superpowers.prepare.", async (workspace) => {
|
|
121
|
-
const outcomes = [];
|
|
122
|
-
const failed = (message) => ({
|
|
123
|
-
kind: "failed",
|
|
124
|
-
outcomes,
|
|
125
|
-
validator: NO_VALIDATOR_OUTPUT,
|
|
126
|
-
message,
|
|
127
|
-
});
|
|
128
136
|
const candidate = join(workspace, location.stagingLeaf);
|
|
129
|
-
const selection = ctx.selection ?? (await computeEffectiveSelection(ctx.root, env));
|
|
130
|
-
let prefetch;
|
|
131
|
-
try {
|
|
132
|
-
prefetch =
|
|
133
|
-
await ctx.adapter.validatePreparationBeforeFetch(adapterContext);
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
|
|
137
|
-
.unexpected);
|
|
138
|
-
}
|
|
139
|
-
outcomes.push(prefetch.outcome);
|
|
140
|
-
if (!prefetch.outcome.ok)
|
|
141
|
-
return failed(null);
|
|
142
|
-
if (prefetch.status !== 0) {
|
|
143
|
-
return failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
|
|
144
|
-
.invalidStatus);
|
|
145
|
-
}
|
|
146
137
|
await owned(`cannot create directory: ${cacheParent}`, () => mkdir(cacheParent, { recursive: true }));
|
|
147
138
|
if (selection.selectionMode === "pinned") {
|
|
148
139
|
await fetchExactCommit(selection.effectiveSource, selection.desiredCommit, cache, workspace);
|
|
@@ -214,37 +205,7 @@ async function gatherPrepare(ctx) {
|
|
|
214
205
|
return failed(prepared.outcome.result.compatibility.reason);
|
|
215
206
|
}
|
|
216
207
|
let validator = NO_VALIDATOR_OUTPUT;
|
|
217
|
-
if (
|
|
218
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:108::[ -f "$additional_validator` — `[ -f ]`.
|
|
219
|
-
if (!(await regularFileExists(additionalValidator))) {
|
|
220
|
-
return {
|
|
221
|
-
kind: "failed",
|
|
222
|
-
outcomes,
|
|
223
|
-
validator,
|
|
224
|
-
message: `additional plugin validator not found: ${additionalValidator}`,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
const ran = await runValidator(["python3", additionalValidator, candidate], UNBOUNDED_LEGACY, env, workspace);
|
|
228
|
-
if (ran.kind === "launchFailed") {
|
|
229
|
-
// PARITY, and it must stay a THROW. The legacy path rejects the workspace
|
|
230
|
-
// callback on a spawn failure; withWorkspace returns the callback error
|
|
231
|
-
// and the `outcomes` collected above are LOST. That loss is a recorded,
|
|
232
|
-
// deliberately unassigned defect. Returning a failed outcome here would
|
|
233
|
-
// replay those outcomes instead -- observably different control flow for
|
|
234
|
-
// CLI-ENV-VALIDATOR-01, which this PR is not authorized to change.
|
|
235
|
-
throw prepareError(`cannot execute additional plugin validator: ${additionalValidator}`, ran.cause);
|
|
236
|
-
}
|
|
237
|
-
validator = { stdout: ran.stdout.text, stderr: ran.stderr.text };
|
|
238
|
-
if (ran.kind !== "exited" || ran.code !== 0) {
|
|
239
|
-
return {
|
|
240
|
-
kind: "failed",
|
|
241
|
-
outcomes,
|
|
242
|
-
validator,
|
|
243
|
-
message: "additional plugin validation failed",
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
else if (executableValidator.length > 0) {
|
|
208
|
+
if (executableValidator.length > 0) {
|
|
248
209
|
const resolution = await resolveValidator(executableValidator);
|
|
249
210
|
const ran = await runValidator([executableValidator, candidate], BOUNDED_EXECUTABLE, env, workspace);
|
|
250
211
|
// The disclosure lands BEFORE any early return, so a launch failure still
|
|
@@ -287,7 +248,7 @@ async function gatherPrepare(ctx) {
|
|
|
287
248
|
// the workspace on return, and the candidate lives in it.
|
|
288
249
|
//
|
|
289
250
|
// atomicReplaceDir delegates to beginDirectoryPublication, whose outer
|
|
290
|
-
// catch (`src/atomic.ts:
|
|
251
|
+
// catch (`src/atomic.ts:319::if (cause`) wraps every non-SafetyError
|
|
291
252
|
// into a SafetyError, so the callee owns every failure on this path and
|
|
292
253
|
// re-emitting its own diagnostic is the sanctioned form of interpolation.
|
|
293
254
|
// The hand-written prefix carries the live root, which the callee's message
|
|
@@ -334,61 +295,10 @@ async function performPrepare(argv, ctx) {
|
|
|
334
295
|
run = await gatherPrepare(ctx);
|
|
335
296
|
}
|
|
336
297
|
catch (cause) {
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
// failure (its cause is the shared runner's (src/validator.ts) captured
|
|
342
|
-
// spawn error, which oneLine never reads -- it takes .message only);
|
|
343
|
-
// readManifest's three hookError messages
|
|
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
|
-
// errno; and SafetyErrors from gitSafeSource, writeProvenance, and
|
|
347
|
-
// withWorkspace.
|
|
348
|
-
//
|
|
349
|
-
// FOUR exceptions, all inherited and none a regression:
|
|
350
|
-
// 1. resolveRef splices git's combined stdout+stderr into its own text
|
|
351
|
-
// on the NON-PINNED path (src/upstream.ts), reached via
|
|
352
|
-
// computeEffectiveSelection (src/effective-selection.ts).
|
|
353
|
-
// This is the DEFAULT invocation -- plain `prepare`, `track-latest`,
|
|
354
|
-
// and any non-40-hex SUPERPOWERS_REF -- not an exotic corner. Pinned
|
|
355
|
-
// by
|
|
356
|
-
// `tests/unit/upstream.test.ts:453-462::void test("resolveRef reports a query failure for latest`,
|
|
357
|
-
// :471-481, and :483-501.
|
|
358
|
-
// 2. fetchExactCommit splices the same combined stdout+stderr into its
|
|
359
|
-
// own text on the PINNED path (both of its own splice sites in
|
|
360
|
-
// src/upstream.ts, and proveCommit's, which it calls). This is the
|
|
361
|
-
// rarer of the two raw-git-output paths, not the only one.
|
|
362
|
-
// 3. `src/selection-store.ts:120-124::cause.module === "selection") {`
|
|
363
|
-
// (same shape at :49, :86, :98) is the
|
|
364
|
-
// module AGENTS.md's `src/selection-store.ts` bullet grandfathers:
|
|
365
|
-
// it interpolates the caught error's own message, so Node errno
|
|
366
|
-
// prose can reach this stream -- sanctioned, nothing here needs
|
|
367
|
-
// fixing.
|
|
368
|
-
// 4. Every runGit call site in this module (fetch, clone, checkout) can
|
|
369
|
-
// reject instead of resolving: `src/git.ts:47-52::reject(new SafetyError` wraps every string
|
|
370
|
-
// errno other than ENOENT in
|
|
371
|
-
// `new SafetyError("git", \`cannot run git: ${failure.message}\`)`,
|
|
372
|
-
// and that Node spawn-level message reaches ctx.stderr through this
|
|
373
|
-
// catch. So the claim that the non-pinned clone/fetch/checkout
|
|
374
|
-
// branch names only the source is true for a non-zero *exit status*
|
|
375
|
-
// (handled explicitly below, in gatherPrepare) but false for a
|
|
376
|
-
// *spawn-level* failure -- runGit throws rather than returning a
|
|
377
|
-
// status in that case, and this outer catch is what stands between
|
|
378
|
-
// it and the stream.
|
|
379
|
-
//
|
|
380
|
-
// oneLine() at this catch collapses each of exceptions 1, 2, and 4 -- the
|
|
381
|
-
// three carrying git-derived text -- to a single line. It collapses CR/LF
|
|
382
|
-
// only, so it bounds how much of that text lands, not what it may
|
|
383
|
-
// contain.
|
|
384
|
-
//
|
|
385
|
-
// runAdapter's closing `throw cause` (src/harnesses/codex/adapter.ts) does NOT arrive
|
|
386
|
-
// here -- the call site catches it and converts it to a hand-written
|
|
387
|
-
// message per AGENTS.md's reader-diagnostics rule.
|
|
388
|
-
//
|
|
389
|
-
// gatherPrepare performs no writes of its own, so this catch cannot also be
|
|
390
|
-
// reached by an EPIPE from prepare's own output: every write below runs
|
|
391
|
-
// only after this try/catch has resolved.
|
|
298
|
+
// Reader wrappers supply their own diagnostics. The saved-selection read
|
|
299
|
+
// path retains its sanctioned interpolation at
|
|
300
|
+
// `src/selection-store.ts:116-121::if (cause instanceof SafetyError && cause.module === "selection") {`.
|
|
301
|
+
// oneLine() bounds any inherited git or filesystem diagnostic to one line.
|
|
392
302
|
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
393
303
|
return 1;
|
|
394
304
|
}
|
|
@@ -418,7 +328,7 @@ async function performPrepare(argv, ctx) {
|
|
|
418
328
|
// completed before cleanup ran, so it is not being reported as unverified
|
|
419
329
|
// -- but something did still go wrong, and AGENTS.md's fail-closed rule
|
|
420
330
|
// extends to it. Mirrors
|
|
421
|
-
// `src/commands/install.ts:
|
|
331
|
+
// `src/commands/install.ts:446::if (cleanupWarning`.
|
|
422
332
|
ctx.stderr.write(`error: ${cleanupWarning}\n`);
|
|
423
333
|
return 1;
|
|
424
334
|
}
|
package/dist/commands/probe.js
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import { assertFailureWritable, writeAdapterFailure, } from "../adapter-result.js";
|
|
2
2
|
import { oneLine } from "../cli-arguments.js";
|
|
3
3
|
import { computeEffectiveSelection } from "../effective-selection.js";
|
|
4
|
+
import { callAdapter } from "./adapter-call.js";
|
|
4
5
|
export const PROBE_USAGE = "error: usage: superpowers-manager probe [--porcelain]\n";
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// in array order, and a controlled failure printed `error:` plus one `hint:`
|
|
9
|
-
// per hint. DIAG-ADAPTER-01 retains that contract
|
|
10
|
-
// (docs/baseline/protocol-disposition.md); dropping it here would be a
|
|
11
|
-
// silent diagnostics regression, not a simplification.
|
|
12
|
-
//
|
|
13
|
-
// Interpolating error.message and each hint is the sanctioned form (AGENTS.md):
|
|
14
|
-
// src/harnesses/codex/adapter.ts has 52 `fail()` sites and none interpolates a caught error's
|
|
15
|
-
// message, so the callee owns every failure reachable on this path. Those two
|
|
16
|
-
// writes now live in writeAdapterFailure (src/adapter-result.ts), which
|
|
17
|
-
// validates all three strings before the first of them reaches the stream.
|
|
18
|
-
//
|
|
19
|
-
// This writes to ctx, so it MUST NOT be called from inside gatherProbe's try
|
|
20
|
-
// -- see the ProbeOutcome note below.
|
|
6
|
+
// Replay adapter messages in order on their declared streams. Adapter failures
|
|
7
|
+
// are emitted only after terminal-safety validation. This writes to ctx, so it
|
|
8
|
+
// must run after gathering, outside a gather function's failure handling.
|
|
21
9
|
export function replayOutcome(outcome, ctx) {
|
|
22
10
|
// Hoisted ABOVE the message loop, and that ordering is the point: a failure
|
|
23
11
|
// whose code, message, or a hint carries a terminal control character must
|
|
@@ -33,153 +21,14 @@ export function replayOutcome(outcome, ctx) {
|
|
|
33
21
|
// stored, so scanning it here would re-check a population that cannot fail.
|
|
34
22
|
writeAdapterFailure(ctx, outcome);
|
|
35
23
|
}
|
|
36
|
-
function
|
|
37
|
-
return value !==
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
function coherentControl(value) {
|
|
25
|
+
return (value.recoveryState !== "required" ||
|
|
26
|
+
(value.probeEligibility.kind === "blocked" &&
|
|
27
|
+
value.mutationEligibility.kind === "blocked"));
|
|
40
28
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return (candidate !== null &&
|
|
44
|
-
Array.isArray(candidate.stdout) &&
|
|
45
|
-
candidate.stdout.every((line) => typeof line === "string") &&
|
|
46
|
-
Array.isArray(candidate.stderr) &&
|
|
47
|
-
candidate.stderr.every((line) => typeof line === "string"));
|
|
48
|
-
}
|
|
49
|
-
function validDecision(value) {
|
|
50
|
-
const candidate = record(value);
|
|
51
|
-
return (candidate !== null &&
|
|
52
|
-
(candidate.kind === "allowed" ||
|
|
53
|
-
(candidate.kind === "blocked" && validOutput(candidate.output))));
|
|
54
|
-
}
|
|
55
|
-
function validCompatibility(value) {
|
|
56
|
-
const candidate = record(value);
|
|
57
|
-
if (candidate === null ||
|
|
58
|
-
typeof candidate.reason !== "string" ||
|
|
59
|
-
(candidate.kind !== "unknown" &&
|
|
60
|
-
candidate.kind !== "supported" &&
|
|
61
|
-
candidate.kind !== "experimental" &&
|
|
62
|
-
candidate.kind !== "unsupported"))
|
|
63
|
-
return false;
|
|
64
|
-
return ((candidate.kind !== "supported" && candidate.kind !== "experimental") ||
|
|
65
|
-
typeof candidate.generation === "string");
|
|
66
|
-
}
|
|
67
|
-
function validPreparedArtifact(value) {
|
|
68
|
-
const candidate = record(value);
|
|
69
|
-
return (candidate !== null &&
|
|
70
|
-
typeof candidate.root === "string" &&
|
|
71
|
-
typeof candidate.commit === "string" &&
|
|
72
|
-
typeof candidate.identity === "string" &&
|
|
73
|
-
validCompatibility(candidate.compatibility));
|
|
74
|
-
}
|
|
75
|
-
function validPrepared(value) {
|
|
76
|
-
const candidate = record(value);
|
|
77
|
-
return (candidate !== null &&
|
|
78
|
-
typeof candidate.observedIdentity === "string" &&
|
|
79
|
-
validCompatibility(candidate.compatibility) &&
|
|
80
|
-
(candidate.kind === "needs-prepare" ||
|
|
81
|
-
(candidate.kind === "current" &&
|
|
82
|
-
validPreparedArtifact(candidate.artifact))));
|
|
83
|
-
}
|
|
84
|
-
export function validInstalled(value) {
|
|
85
|
-
const candidate = record(value);
|
|
86
|
-
return (candidate !== null &&
|
|
87
|
-
(candidate.kind === "current" ||
|
|
88
|
-
candidate.kind === "mismatch" ||
|
|
89
|
-
candidate.kind === "absent") &&
|
|
90
|
-
typeof candidate.observedIdentity === "string" &&
|
|
91
|
-
(candidate.kind !== "absent" || candidate.observedIdentity === ""));
|
|
92
|
-
}
|
|
93
|
-
function validOwnership(value) {
|
|
94
|
-
const candidate = record(value);
|
|
95
|
-
return (candidate !== null &&
|
|
96
|
-
validDecision(candidate.installEligibility) &&
|
|
97
|
-
Object.prototype.hasOwnProperty.call(candidate, "removalInput") &&
|
|
98
|
-
validDecision(candidate.removalVerification) &&
|
|
99
|
-
validOutput(candidate.postRemovalOutput) &&
|
|
100
|
-
typeof candidate.presentationValue === "string" &&
|
|
101
|
-
(candidate.presentationConflicts === undefined ||
|
|
102
|
-
(Array.isArray(candidate.presentationConflicts) &&
|
|
103
|
-
candidate.presentationConflicts.every((conflict) => typeof conflict === "string"))));
|
|
104
|
-
}
|
|
105
|
-
function validControl(value) {
|
|
106
|
-
const candidate = record(value);
|
|
107
|
-
return (candidate !== null &&
|
|
108
|
-
validDecision(candidate.probeEligibility) &&
|
|
109
|
-
validDecision(candidate.mutationEligibility) &&
|
|
110
|
-
typeof candidate.presentationValue === "string" &&
|
|
111
|
-
(candidate.recoveryState === undefined ||
|
|
112
|
-
(candidate.recoveryState === "required" &&
|
|
113
|
-
candidate.probeEligibility.kind === "blocked" &&
|
|
114
|
-
candidate.mutationEligibility.kind === "blocked")));
|
|
115
|
-
}
|
|
116
|
-
// `runAdapter` reports a CONTROLLED failure by RETURN VALUE, not by throwing
|
|
117
|
-
// (`src/adapter-result.ts:32-35::export interface AdapterResult`). The shell got
|
|
118
|
-
// fail-closed behaviour for free: spw_invoke_adapter returned 1 and
|
|
119
|
-
// scripts/probe ran under `set -eu`.
|
|
120
|
-
// Omitting the status check here would read a failed inspection as absent
|
|
121
|
-
// evidence and report it as success.
|
|
122
|
-
//
|
|
123
|
-
// It does still THROW for a non-AdapterFailure cause (runAdapter's closing
|
|
124
|
-
// `throw cause`, src/harnesses/codex/adapter.ts). That is caught here rather than in runProbe's
|
|
125
|
-
// outer catch, because the two need different diagnostics -- see spec §3.3a.
|
|
126
|
-
//
|
|
127
|
-
// Reached through ctx.adapter, not a direct module-level dependency on the
|
|
128
|
-
// adapter module: src/commands/prepare.ts and this module are the two the
|
|
129
|
-
// injected double must observe, because install reaches the adapter through
|
|
130
|
-
// gatherProbe and runPrepare. Spec §4.5.
|
|
131
|
-
async function inspect(call, unexpected, invalidStatus, valid) {
|
|
132
|
-
let result;
|
|
133
|
-
try {
|
|
134
|
-
result = await call();
|
|
135
|
-
if (!result ||
|
|
136
|
-
!Number.isInteger(result.status) ||
|
|
137
|
-
!result.outcome ||
|
|
138
|
-
typeof result.outcome.ok !== "boolean" ||
|
|
139
|
-
!Array.isArray(result.outcome.messages))
|
|
140
|
-
return { ok: false, result: null, message: invalidStatus };
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return { ok: false, result: null, message: unexpected };
|
|
144
|
-
}
|
|
145
|
-
const outcome = result.outcome;
|
|
146
|
-
if (result.status !== 0 || !outcome.ok) {
|
|
147
|
-
// The `outcome.ok && status !== 0` combination cannot arise from
|
|
148
|
-
// successResult/failureResult, so it gets its own hand-written message
|
|
149
|
-
// rather than falling through to a replay that would print nothing.
|
|
150
|
-
return {
|
|
151
|
-
ok: false,
|
|
152
|
-
result,
|
|
153
|
-
message: outcome.ok ? invalidStatus : null,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
if (!valid(outcome.result)) {
|
|
158
|
-
return { ok: false, result, message: invalidStatus };
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
return { ok: false, result, message: invalidStatus };
|
|
163
|
-
}
|
|
164
|
-
return { ok: true, result: { status: result.status, outcome } };
|
|
165
|
-
}
|
|
166
|
-
// Runs every step that can throw or fail closed, returning the outcome as
|
|
167
|
-
// data. It performs no writes of its own, so nothing inside runProbe's try can
|
|
168
|
-
// raise EPIPE — the same shape as src/commands/unpin.ts's attemptUnpin.
|
|
169
|
-
//
|
|
170
|
-
// The outcomes are CARRIED OUT rather than replayed in place, for that same
|
|
171
|
-
// reason: replayOutcome writes to ctx, and a write inside this try could
|
|
172
|
-
// raise EPIPE and be caught and relabelled as a selection failure. runProbe
|
|
173
|
-
// replays them, in collection order, after the try/catch has resolved. The
|
|
174
|
-
// operator-visible result is identical -- probe emits nothing else until the
|
|
175
|
-
// very end -- and the EPIPE hazard never exists.
|
|
176
|
-
// Exported for src/commands/install.ts and src/commands/update.ts, which need
|
|
177
|
-
// probe's FACTS rather than its rendering. The historical
|
|
178
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:39-41::spw_probe_field`
|
|
179
|
-
// implementation awk-parsed the porcelain back into fields; that round trip is
|
|
180
|
-
// gone.
|
|
29
|
+
// Collect outcomes without writing so output failures cannot be classified as
|
|
30
|
+
// inspection failures. Replay collected outcomes after gathering completes.
|
|
181
31
|
export async function gatherProbe(ctx) {
|
|
182
|
-
// Order mirrors `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:24-40::spw_compute_effective_selection` exactly.
|
|
183
32
|
const selection = ctx.selection ?? (await computeEffectiveSelection(ctx.root, ctx.env));
|
|
184
33
|
const outcomes = [];
|
|
185
34
|
const adapterContext = { root: ctx.root, env: ctx.env };
|
|
@@ -196,19 +45,28 @@ export async function gatherProbe(ctx) {
|
|
|
196
45
|
const before = resources === null
|
|
197
46
|
? []
|
|
198
47
|
: await ctx.coordination.observeResources(resources);
|
|
199
|
-
const collect = async (call, unexpected, invalidStatus,
|
|
200
|
-
|
|
48
|
+
const collect = async (call, unexpected, invalidStatus, acceptValue, replaySuccess = true) => {
|
|
49
|
+
let result = await callAdapter(call, { unexpected, invalidStatus });
|
|
50
|
+
if (result.ok && acceptValue !== undefined) {
|
|
51
|
+
try {
|
|
52
|
+
if (!acceptValue(result.result.outcome.result))
|
|
53
|
+
result = { ok: false, result: result.result, message: invalidStatus };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
result = { ok: false, result: result.result, message: invalidStatus };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
201
59
|
if (result.result !== null && (replaySuccess || !result.result.outcome.ok))
|
|
202
60
|
outcomes.push(result.result.outcome);
|
|
203
61
|
return result;
|
|
204
62
|
};
|
|
205
|
-
const prepared = await collect(() => ctx.adapter.inspectPrepared(selection, adapterContext), "cannot inspect prepared harness state", "adapter reported a failure status for prepared harness inspection"
|
|
63
|
+
const prepared = await collect(() => ctx.adapter.inspectPrepared(selection, adapterContext), "cannot inspect prepared harness state", "adapter reported a failure status for prepared harness inspection");
|
|
206
64
|
if (!prepared.ok) {
|
|
207
65
|
return { status: 1, outcomes, message: prepared.message };
|
|
208
66
|
}
|
|
209
67
|
const failure = (site) => ctx.adapter.presentation.callFailure(site, adapterContext);
|
|
210
68
|
const installedFailure = failure("probe-installed");
|
|
211
|
-
const installed = await collect(() => ctx.adapter.inspectInstalled(selection, adapterContext), installedFailure.unexpected, installedFailure.invalidStatus
|
|
69
|
+
const installed = await collect(() => ctx.adapter.inspectInstalled(selection, adapterContext), installedFailure.unexpected, installedFailure.invalidStatus);
|
|
212
70
|
if (!installed.ok) {
|
|
213
71
|
return { status: 1, outcomes, message: installed.message };
|
|
214
72
|
}
|
|
@@ -219,22 +77,22 @@ export async function gatherProbe(ctx) {
|
|
|
219
77
|
message: "cannot determine harness observation resources",
|
|
220
78
|
};
|
|
221
79
|
const ownershipFailure = failure("probe-ownership");
|
|
222
|
-
const ownership = await collect(() => ctx.adapter.inspectOwnership(adapterContext), ownershipFailure.unexpected, ownershipFailure.invalidStatus
|
|
80
|
+
const ownership = await collect(() => ctx.adapter.inspectOwnership(adapterContext), ownershipFailure.unexpected, ownershipFailure.invalidStatus);
|
|
223
81
|
if (!ownership.ok) {
|
|
224
82
|
return { status: 1, outcomes, message: ownership.message };
|
|
225
83
|
}
|
|
226
84
|
const controlFailure = failure("probe-control");
|
|
227
|
-
const control = await collect(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure.unexpected, controlFailure.invalidStatus,
|
|
85
|
+
const control = await collect(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure.unexpected, controlFailure.invalidStatus, coherentControl);
|
|
228
86
|
if (!control.ok) {
|
|
229
87
|
return { status: 1, outcomes, message: control.message };
|
|
230
88
|
}
|
|
231
|
-
const preparedAfter = await collect(() => ctx.adapter.inspectPrepared(selection, adapterContext), "cannot inspect prepared harness state", "adapter reported a failure status for prepared harness inspection",
|
|
89
|
+
const preparedAfter = await collect(() => ctx.adapter.inspectPrepared(selection, adapterContext), "cannot inspect prepared harness state", "adapter reported a failure status for prepared harness inspection", undefined, false);
|
|
232
90
|
if (!preparedAfter.ok)
|
|
233
91
|
return { status: 1, outcomes, message: preparedAfter.message };
|
|
234
|
-
const installedAfter = await collect(() => ctx.adapter.inspectInstalled(selection, adapterContext), installedFailure.unexpected, installedFailure.invalidStatus,
|
|
92
|
+
const installedAfter = await collect(() => ctx.adapter.inspectInstalled(selection, adapterContext), installedFailure.unexpected, installedFailure.invalidStatus, undefined, false);
|
|
235
93
|
if (!installedAfter.ok)
|
|
236
94
|
return { status: 1, outcomes, message: installedAfter.message };
|
|
237
|
-
const controlAfter = await collect(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure.unexpected, controlFailure.invalidStatus,
|
|
95
|
+
const controlAfter = await collect(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure.unexpected, controlFailure.invalidStatus, coherentControl, false);
|
|
238
96
|
if (!controlAfter.ok)
|
|
239
97
|
return { status: 1, outcomes, message: controlAfter.message };
|
|
240
98
|
const after = await ctx.coordination.observeResources(resources);
|
|
@@ -270,7 +128,6 @@ export async function gatherProbe(ctx) {
|
|
|
270
128
|
ownership: ownership.result.outcome.result,
|
|
271
129
|
control: control.result.outcome.result,
|
|
272
130
|
compatibility: preparedState.compatibility,
|
|
273
|
-
resources: after,
|
|
274
131
|
resourceState: control.result.outcome.result.recoveryState === "required"
|
|
275
132
|
? "recovery-required"
|
|
276
133
|
: after.some((observation) => observation.state === "owned")
|
|
@@ -285,17 +142,8 @@ export async function gatherProbe(ctx) {
|
|
|
285
142
|
};
|
|
286
143
|
}
|
|
287
144
|
export async function runProbe(argv, ctx) {
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// narrowing, recorded as a port-only entry in
|
|
291
|
-
// tests/migration-inventory/probe.md.
|
|
292
|
-
//
|
|
293
|
-
// This guard is NOT the production path. src/cli.ts's parseArgs rejects the
|
|
294
|
-
// same inputs first, before preflight, with the usage block the CLI's other
|
|
295
|
-
// usage errors carry — exactly the arrangement track-latest and unpin have,
|
|
296
|
-
// where the in-module check is an unreachable-from-CLI duplicate. It stays
|
|
297
|
-
// because runProbe is also called directly by tests and by any future
|
|
298
|
-
// in-process caller that has not been through parseArgs.
|
|
145
|
+
// The CLI parser rejects these inputs first. Keep this guard for direct
|
|
146
|
+
// in-process callers, which do not necessarily pass through that parser.
|
|
299
147
|
const porcelain = argv.length === 1 && argv[0] === "--porcelain";
|
|
300
148
|
if (!porcelain && argv.length !== 0) {
|
|
301
149
|
ctx.stderr.write(PROBE_USAGE);
|
|
@@ -306,91 +154,14 @@ export async function runProbe(argv, ctx) {
|
|
|
306
154
|
outcome = await gatherProbe(ctx);
|
|
307
155
|
}
|
|
308
156
|
catch (cause) {
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
// computeEffectiveSelection's validateSource call
|
|
314
|
-
// (src/effective-selection.ts); the ones validateRecord raises
|
|
315
|
-
// (`src/selection.ts:198::integer`, :219, requireObject at :47, requireExactKeys at
|
|
316
|
-
// :64, validatePinnedRecord at :170-183), reached from
|
|
317
|
-
// `src/selection-store.ts:103::return validateRecord` on the read path; the
|
|
318
|
-
// selectionErrors requireAbsolute and selectionConfigDir
|
|
319
|
-
// (src/effective-selection.ts) raise
|
|
320
|
-
// for a non-absolute or missing config directory; readConfigRef's `cannot
|
|
321
|
-
// read packaged upstream ref <path>` (src/upstream.ts); and resolveRef's own
|
|
322
|
-
// two no-match diagnostics, `no stable semver tag found for latest-release`
|
|
323
|
-
// and `cannot resolve upstream ref: <ref>` (src/upstream.ts). normalizeSaved
|
|
324
|
-
// (`src/selection.ts:222-241::normalizeSaved`) throws nothing at all.
|
|
325
|
-
//
|
|
326
|
-
// THREE exceptions, all inherited and none a regression:
|
|
327
|
-
// 1. resolveRef splices git's combined stdout+stderr into its own text
|
|
328
|
-
// (its three `cannot query upstream ...` throws, src/upstream.ts),
|
|
329
|
-
// reached via computeEffectiveSelection's resolveRef call
|
|
330
|
-
// (src/effective-selection.ts). The packaged ref is `latest-release`,
|
|
331
|
-
// which resolveRef answers with an ls-remote, so an ordinary unpinned
|
|
332
|
-
// run reaches this rather than only an exotic corner -- though not
|
|
333
|
-
// every unpinned run does: resolveRef's COMMIT_INPUT_RE branch returns
|
|
334
|
-
// before any runGit call, so a 40-hex requested ref splices nothing.
|
|
335
|
-
// Pinned by
|
|
336
|
-
// `tests/unit/upstream.test.ts:453::void test("resolveRef reports a query failure for latest`,
|
|
337
|
-
// :471, and :483.
|
|
338
|
-
// 2. The
|
|
339
|
-
// `src/selection-store.ts:120-124::cause.module === "selection") {`
|
|
340
|
-
// site (same shape at :49, :86, :98) is the module AGENTS.md's
|
|
341
|
-
// `src/selection-store.ts` bullet grandfathers:
|
|
342
|
-
// it interpolates the caught error's own message, so Node errno
|
|
343
|
-
// prose can reach this stream. Reached on the READ path only, via
|
|
344
|
-
// loadSavedSelection (src/effective-selection.ts) ->
|
|
345
|
-
// readSelectionState
|
|
346
|
-
// (`src/selection-store.ts:149::export async function readSelectionState`).
|
|
347
|
-
// This module's four write-only interpolating sites -- ensureStateDirectory
|
|
348
|
-
// (:172), finalStateDiagnostic (:197), and the two in
|
|
349
|
-
// writeSelectionState's own catch (:225, :231) -- are all
|
|
350
|
-
// unreachable from probe, which never writes.
|
|
351
|
-
// 3. Every runGit call site inside resolveRef (src/upstream.ts) can
|
|
352
|
-
// reject instead of resolving. On the non-ENOENT arm of
|
|
353
|
-
// `src/git.ts:47-52::if (typeof`, runGit builds the message
|
|
354
|
-
// "cannot run git: " followed by the Node spawn error's own message
|
|
355
|
-
// (:51) and rejects with a SafetyError carrying it (:52), so that
|
|
356
|
-
// Node spawn-level text reaches ctx.stderr through this catch. The
|
|
357
|
-
// ENOENT arm (:50) is hand-written and carries nothing. A non-zero
|
|
358
|
-
// *exit status* is handled by exception 1 above; this is the
|
|
359
|
-
// *spawn-level* case, where runGit throws rather than returning a
|
|
360
|
-
// status.
|
|
361
|
-
//
|
|
362
|
-
// oneLine() at this catch collapses each of exceptions 1 and 3 -- the two
|
|
363
|
-
// carrying git-derived text -- to a single line. It collapses CR/LF only,
|
|
364
|
-
// so it bounds how much of that text lands, not what it may contain.
|
|
365
|
-
//
|
|
366
|
-
// fetchExactCommit is deliberately NOT in this list, unlike prepare's
|
|
367
|
-
// fetchExactCommit exception, which runPrepare's catch block documents.
|
|
368
|
-
// Its only callers are `src/upstream-cli.ts:83::await fetchExactCommit` and
|
|
369
|
-
// gatherPrepare's own call in src/commands/prepare.ts (git grep -n
|
|
370
|
-
// fetchExactCommit -- src/ is the check that keeps "only" true), so probe
|
|
371
|
-
// never reaches it and
|
|
372
|
-
// its splice sites cannot appear on this stream. Do not add it back by
|
|
373
|
-
// symmetry with prepare.
|
|
374
|
-
//
|
|
375
|
-
// generatedCommitOrEmpty is not in this list either: it cannot throw at
|
|
376
|
-
// all. It (src/provenance.ts) delegates to readGeneratedCommitLenient,
|
|
377
|
-
// which catches every failure and returns "".
|
|
378
|
-
//
|
|
379
|
-
// A non-AdapterFailure re-thrown by runAdapter's closing `throw cause`
|
|
380
|
-
// (src/harnesses/codex/adapter.ts) does NOT reach here: inspect() catches it and converts
|
|
381
|
-
// it to a hand-written message per AGENTS.md's reader-diagnostics rule -- a
|
|
382
|
-
// rethrown cause is exactly the failure src/harnesses/codex/adapter.ts declined to own, so
|
|
383
|
-
// its text must never reach this stream. See §3.3a.
|
|
384
|
-
//
|
|
385
|
-
// gatherProbe performs no writes of its own, so this catch cannot also be
|
|
386
|
-
// reached by an EPIPE from probe's own output — every write below runs
|
|
387
|
-
// only after this try/catch has resolved.
|
|
157
|
+
// Reader wrappers supply their own diagnostics. The saved-selection read
|
|
158
|
+
// path retains its sanctioned interpolation at
|
|
159
|
+
// `src/selection-store.ts:116-121::if (cause instanceof SafetyError && cause.module === "selection") {`.
|
|
160
|
+
// oneLine() bounds any inherited git or filesystem diagnostic to one line.
|
|
388
161
|
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
389
162
|
return 1;
|
|
390
163
|
}
|
|
391
|
-
// Replay
|
|
392
|
-
// messages whether or not that response was a failure at
|
|
393
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/validate-adapter-response.py:268::replay(messages)`.
|
|
164
|
+
// Replay before reporting a command-level result on either path.
|
|
394
165
|
for (const each of outcome.outcomes)
|
|
395
166
|
replayOutcome(each, ctx);
|
|
396
167
|
if (outcome.status === 1) {
|
|
@@ -15,7 +15,7 @@ export async function runTrackLatest(argv, ctx) {
|
|
|
15
15
|
// writing, under "Invalid existing state must block overwrite" —
|
|
16
16
|
// readSelectionState's own readOpenedRecord/parseRecordBytes is what
|
|
17
17
|
// validates an existing record, via validateRecord at
|
|
18
|
-
// `src/selection-store.ts:
|
|
18
|
+
// `src/selection-store.ts:99::return validateRecord`). Calling loadSavedSelection here preserves
|
|
19
19
|
// `git show 349fe2ed405b371ec2de1347bb3fc50c6bc15dc4:scripts/track-latest:20-21::read`'s read-then-write shape, so this command
|
|
20
20
|
// stays fail-closed on its own terms rather than solely by depending on
|
|
21
21
|
// the store's internals.
|