superpowers-manager 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +42 -26
  2. package/dist/adapter-result.js +5 -9
  3. package/dist/atomic.js +4 -14
  4. package/dist/cli-arguments.js +1 -27
  5. package/dist/cli.js +25 -40
  6. package/dist/commands/adapter-call.js +28 -0
  7. package/dist/commands/install.js +25 -105
  8. package/dist/commands/pin.js +1 -1
  9. package/dist/commands/prepare.js +37 -81
  10. package/dist/commands/probe.js +35 -262
  11. package/dist/commands/track-latest.js +1 -1
  12. package/dist/commands/uninstall.js +15 -91
  13. package/dist/commands/update.js +5 -25
  14. package/dist/effective-selection.js +4 -6
  15. package/dist/git-tree.js +85 -0
  16. package/dist/harness-command-result.js +47 -0
  17. package/dist/harnesses/codex/adapter.js +12 -41
  18. package/dist/harnesses/codex/conflicts.js +2 -3
  19. package/dist/harnesses/codex/hooks.js +1 -1
  20. package/dist/harnesses/codex/json.js +1 -11
  21. package/dist/harnesses/codex/lifecycle.js +0 -48
  22. package/dist/harnesses/codex/manifest-overlay.js +1 -1
  23. package/dist/harnesses/codex/paths.js +14 -29
  24. package/dist/harnesses/codex/presentation.js +2 -23
  25. package/dist/harnesses/codex/publication.js +0 -6
  26. package/dist/harnesses/codex/recovery.js +5 -5
  27. package/dist/harnesses/codex/stored-state.js +1 -1
  28. package/dist/harnesses/opencode/config.js +256 -0
  29. package/dist/harnesses/opencode/discovery.js +587 -0
  30. package/dist/harnesses/opencode/harness.js +37 -0
  31. package/dist/harnesses/opencode/install.js +601 -0
  32. package/dist/harnesses/opencode/native.js +57 -0
  33. package/dist/harnesses/opencode/package.js +89 -0
  34. package/dist/harnesses/opencode/paths.js +38 -0
  35. package/dist/harnesses/opencode/prepare.js +30 -0
  36. package/dist/harnesses/opencode/presentation.js +28 -0
  37. package/dist/harnesses/opencode/state.js +174 -0
  38. package/dist/harnesses/pi/compatibility.js +5 -37
  39. package/dist/harnesses/pi/install.js +7 -5
  40. package/dist/harnesses/pi/native.js +3 -47
  41. package/dist/harnesses/pi/package.js +5 -107
  42. package/dist/harnesses/pi/prepare.js +13 -104
  43. package/dist/harnesses/pi/presentation.js +23 -66
  44. package/dist/harnesses/pi/shared-skills.js +28 -39
  45. package/dist/harnesses/pi/state.js +7 -27
  46. package/dist/provenance.js +2 -2
  47. package/dist/safe-path.js +2 -2
  48. package/dist/selection-store.js +2 -22
  49. package/dist/snapshot-package.js +78 -0
  50. package/dist/snapshot-prepare.js +99 -0
  51. package/dist/snapshot-presentation.js +44 -0
  52. package/dist/validator.js +4 -1
  53. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  54. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  55. package/node_modules/jsonc-parser/README.md +364 -0
  56. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  57. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  58. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  59. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  60. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  61. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  62. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  63. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  64. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  65. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  66. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  67. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  68. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  69. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  70. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  71. package/node_modules/jsonc-parser/package.json +37 -0
  72. package/package.json +8 -4
  73. package/dist/domain/fingerprint.js +0 -4
  74. package/dist/status.js +0 -24
  75. package/dist/validate-generated-plugin-cli.js +0 -118
@@ -78,10 +78,9 @@ function validStagingLeaf(value) {
78
78
  !value.includes("\\"));
79
79
  }
80
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.
81
83
  const env = ctx.env;
82
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:16::invocation_root=` — captured before the two case statements below.
83
- // getcwd(3) returns the physical path, so this matches `pwd -P` without a
84
- // realpath call.
85
84
  const cwd = process.cwd();
86
85
  const cache = upstreamCacheRoot(ctx.root, env, cwd);
87
86
  const cacheParent = dirname(cache);
@@ -102,34 +101,39 @@ async function gatherPrepare(ctx) {
102
101
  const pluginRoot = location.destinationRoot;
103
102
  const executableValidator = env.SUPERPOWERS_VALIDATOR_EXECUTABLE || "";
104
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
+ }
105
133
  await owned(`cannot create directory: ${tmpParent}`, () => mkdir(tmpParent, { recursive: true }));
106
134
  let cleanupWarning = null;
107
135
  const outcome = await withWorkspace(tmpParent, ".superpowers.prepare.", async (workspace) => {
108
- const outcomes = [];
109
- const failed = (message) => ({
110
- kind: "failed",
111
- outcomes,
112
- validator: NO_VALIDATOR_OUTPUT,
113
- message,
114
- });
115
136
  const candidate = join(workspace, location.stagingLeaf);
116
- const selection = ctx.selection ?? (await computeEffectiveSelection(ctx.root, env));
117
- let prefetch;
118
- try {
119
- prefetch =
120
- await ctx.adapter.validatePreparationBeforeFetch(adapterContext);
121
- }
122
- catch {
123
- return failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
124
- .unexpected);
125
- }
126
- outcomes.push(prefetch.outcome);
127
- if (!prefetch.outcome.ok)
128
- return failed(null);
129
- if (prefetch.status !== 0) {
130
- return failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
131
- .invalidStatus);
132
- }
133
137
  await owned(`cannot create directory: ${cacheParent}`, () => mkdir(cacheParent, { recursive: true }));
134
138
  if (selection.selectionMode === "pinned") {
135
139
  await fetchExactCommit(selection.effectiveSource, selection.desiredCommit, cache, workspace);
@@ -244,7 +248,7 @@ async function gatherPrepare(ctx) {
244
248
  // the workspace on return, and the candidate lives in it.
245
249
  //
246
250
  // atomicReplaceDir delegates to beginDirectoryPublication, whose outer
247
- // catch (`src/atomic.ts:341-348::if (cause`) wraps every non-SafetyError
251
+ // catch (`src/atomic.ts:319::if (cause`) wraps every non-SafetyError
248
252
  // into a SafetyError, so the callee owns every failure on this path and
249
253
  // re-emitting its own diagnostic is the sanctioned form of interpolation.
250
254
  // The hand-written prefix carries the live root, which the callee's message
@@ -291,58 +295,10 @@ async function performPrepare(argv, ctx) {
291
295
  run = await gatherPrepare(ctx);
292
296
  }
293
297
  catch (cause) {
294
- // Hand-written messages, per AGENTS.md's reader-diagnostics rule.
295
- // Reachable here: prepareError(), from this module's owned() wrappers,
296
- // its two manifest-version checks, and asResolutionKind;
297
- // readManifest's three hookError messages
298
- // (`src/harnesses/codex/hooks.ts:113-138::readManifest`), pinned by
299
- // `tests/unit/harnesses/codex/hooks.test.ts:95::void test("readManifest diagnostics` as carrying no reader vocabulary or
300
- // errno; and SafetyErrors from gitSafeSource, writeProvenance, and
301
- // withWorkspace.
302
- //
303
- // FOUR exceptions, all inherited and none a regression:
304
- // 1. resolveRef splices git's combined stdout+stderr into its own text
305
- // on the NON-PINNED path (src/upstream.ts), reached via
306
- // computeEffectiveSelection (src/effective-selection.ts).
307
- // This is the DEFAULT invocation -- plain `prepare`, `track-latest`,
308
- // and any non-40-hex SUPERPOWERS_REF -- not an exotic corner. Pinned
309
- // by
310
- // `tests/unit/upstream.test.ts:453-462::void test("resolveRef reports a query failure for latest`,
311
- // :471-481, and :483-501.
312
- // 2. fetchExactCommit splices the same combined stdout+stderr into its
313
- // own text on the PINNED path (both of its own splice sites in
314
- // src/upstream.ts, and proveCommit's, which it calls). This is the
315
- // rarer of the two raw-git-output paths, not the only one.
316
- // 3. `src/selection-store.ts:120-124::cause.module === "selection") {`
317
- // (same shape at :49, :86, :98) is the
318
- // module AGENTS.md's `src/selection-store.ts` bullet grandfathers:
319
- // it interpolates the caught error's own message, so Node errno
320
- // prose can reach this stream -- sanctioned, nothing here needs
321
- // fixing.
322
- // 4. Every runGit call site in this module (fetch, clone, checkout) can
323
- // reject instead of resolving: `src/git.ts:47-52::reject(new SafetyError` wraps every string
324
- // errno other than ENOENT in
325
- // `new SafetyError("git", \`cannot run git: ${failure.message}\`)`,
326
- // and that Node spawn-level message reaches ctx.stderr through this
327
- // catch. So the claim that the non-pinned clone/fetch/checkout
328
- // branch names only the source is true for a non-zero *exit status*
329
- // (handled explicitly below, in gatherPrepare) but false for a
330
- // *spawn-level* failure -- runGit throws rather than returning a
331
- // status in that case, and this outer catch is what stands between
332
- // it and the stream.
333
- //
334
- // oneLine() at this catch collapses each of exceptions 1, 2, and 4 -- the
335
- // three carrying git-derived text -- to a single line. It collapses CR/LF
336
- // only, so it bounds how much of that text lands, not what it may
337
- // contain.
338
- //
339
- // runCodexOperation's closing `throw cause` (src/harnesses/codex/adapter.ts) does NOT arrive
340
- // here -- the call site catches it and converts it to a hand-written
341
- // message per AGENTS.md's reader-diagnostics rule.
342
- //
343
- // gatherPrepare performs no writes of its own, so this catch cannot also be
344
- // reached by an EPIPE from prepare's own output: every write below runs
345
- // 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.
346
302
  ctx.stderr.write(`error: ${oneLine(cause)}\n`);
347
303
  return 1;
348
304
  }
@@ -372,7 +328,7 @@ async function performPrepare(argv, ctx) {
372
328
  // completed before cleanup ran, so it is not being reported as unverified
373
329
  // -- but something did still go wrong, and AGENTS.md's fail-closed rule
374
330
  // extends to it. Mirrors
375
- // `src/commands/install.ts:561-570::if (cleanupWarning`.
331
+ // `src/commands/install.ts:446::if (cleanupWarning`.
376
332
  ctx.stderr.write(`error: ${cleanupWarning}\n`);
377
333
  return 1;
378
334
  }
@@ -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
- // Ports scripts/core/validate-adapter-response.py's replay (:235-238) and its
6
- // error/hint block (:269-272). The shell ran that validator on EVERY adapter
7
- // response, so adapter messages reached the operator on their declared streams
8
- // in array order, and a controlled failure printed `error:` plus one `hint:`
9
- // per hint. DIAG-ADAPTER-01 retains that contract
10
- // (docs/adapter-result-contract.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 record(value) {
37
- return value !== null && typeof value === "object"
38
- ? value
39
- : null;
24
+ function coherentControl(value) {
25
+ return (value.recoveryState !== "required" ||
26
+ (value.probeEligibility.kind === "blocked" &&
27
+ value.mutationEligibility.kind === "blocked"));
40
28
  }
41
- export function validOutput(value) {
42
- const candidate = record(value);
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
- // `runCodexOperation` 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 (runCodexOperation'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, valid, replaySuccess = true) => {
200
- const result = await inspect(call, unexpected, invalidStatus, valid);
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", validPrepared);
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, validInstalled);
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, validOwnership);
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, validControl);
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", validPrepared, false);
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, validInstalled, false);
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, validControl, false);
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,16 +142,8 @@ export async function gatherProbe(ctx) {
285
142
  };
286
143
  }
287
144
  export async function runProbe(argv, ctx) {
288
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/probe:42::porcelain` tested only `[ "${1:-}" = "--porcelain" ]`, so a typo'd
289
- // flag silently produced human output. Rejecting it is a deliberate
290
- // narrowing from the historical shell behavior.
291
- //
292
- // This guard is NOT the production path. src/cli.ts's parseArgs rejects the
293
- // same inputs first, before preflight, with the usage block the CLI's other
294
- // usage errors carry — exactly the arrangement track-latest and unpin have,
295
- // where the in-module check is an unreachable-from-CLI duplicate. It stays
296
- // because runProbe is also called directly by tests and by any future
297
- // 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.
298
147
  const porcelain = argv.length === 1 && argv[0] === "--porcelain";
299
148
  if (!porcelain && argv.length !== 0) {
300
149
  ctx.stderr.write(PROBE_USAGE);
@@ -305,90 +154,14 @@ export async function runProbe(argv, ctx) {
305
154
  outcome = await gatherProbe(ctx);
306
155
  }
307
156
  catch (cause) {
308
- // Hand-written messages, per AGENTS.md's reader-diagnostics rule. Reachable
309
- // here: the selectionErrors validateSource raises
310
- // (`src/selection.ts:137::malformed`, :140, requireSingleLineString at :77),
311
- // reached from
312
- // computeEffectiveSelection's validateSource call
313
- // (src/effective-selection.ts); the ones validateRecord raises
314
- // (`src/selection.ts:198::integer`, :219, requireObject at :47, requireExactKeys at
315
- // :64, validatePinnedRecord at :170-183), reached from
316
- // `src/selection-store.ts:103::return validateRecord` on the read path; the
317
- // selectionErrors requireAbsolute and selectionConfigDir
318
- // (src/effective-selection.ts) raise
319
- // for a non-absolute or missing config directory; readConfigRef's `cannot
320
- // read packaged upstream ref <path>` (src/upstream.ts); and resolveRef's own
321
- // two no-match diagnostics, `no stable semver tag found for latest-release`
322
- // and `cannot resolve upstream ref: <ref>` (src/upstream.ts). normalizeSaved
323
- // (`src/selection.ts:222-241::normalizeSaved`) throws nothing at all.
324
- //
325
- // THREE exceptions, all inherited and none a regression:
326
- // 1. resolveRef splices git's combined stdout+stderr into its own text
327
- // (its three `cannot query upstream ...` throws, src/upstream.ts),
328
- // reached via computeEffectiveSelection's resolveRef call
329
- // (src/effective-selection.ts). The packaged ref is `latest-release`,
330
- // which resolveRef answers with an ls-remote, so an ordinary unpinned
331
- // run reaches this rather than only an exotic corner -- though not
332
- // every unpinned run does: resolveRef's COMMIT_INPUT_RE branch returns
333
- // before any runGit call, so a 40-hex requested ref splices nothing.
334
- // Pinned by
335
- // `tests/unit/upstream.test.ts:453::void test("resolveRef reports a query failure for latest`,
336
- // :471, and :483.
337
- // 2. The
338
- // `src/selection-store.ts:120-124::cause.module === "selection") {`
339
- // site (same shape at :49, :86, :98) is the module AGENTS.md's
340
- // `src/selection-store.ts` bullet grandfathers:
341
- // it interpolates the caught error's own message, so Node errno
342
- // prose can reach this stream. Reached on the READ path only, via
343
- // loadSavedSelection (src/effective-selection.ts) ->
344
- // readSelectionState
345
- // (`src/selection-store.ts:149::export async function readSelectionState`).
346
- // This module's four write-only interpolating sites -- ensureStateDirectory
347
- // (:172), finalStateDiagnostic (:197), and the two in
348
- // writeSelectionState's own catch (:225, :231) -- are all
349
- // unreachable from probe, which never writes.
350
- // 3. Every runGit call site inside resolveRef (src/upstream.ts) can
351
- // reject instead of resolving. On the non-ENOENT arm of
352
- // `src/git.ts:47-52::if (typeof`, runGit builds the message
353
- // "cannot run git: " followed by the Node spawn error's own message
354
- // (:51) and rejects with a SafetyError carrying it (:52), so that
355
- // Node spawn-level text reaches ctx.stderr through this catch. The
356
- // ENOENT arm (:50) is hand-written and carries nothing. A non-zero
357
- // *exit status* is handled by exception 1 above; this is the
358
- // *spawn-level* case, where runGit throws rather than returning a
359
- // status.
360
- //
361
- // oneLine() at this catch collapses each of exceptions 1 and 3 -- the two
362
- // carrying git-derived text -- to a single line. It collapses CR/LF only,
363
- // so it bounds how much of that text lands, not what it may contain.
364
- //
365
- // fetchExactCommit is deliberately NOT in this list, unlike prepare's
366
- // fetchExactCommit exception, which runPrepare's catch block documents.
367
- // Its only remaining caller is gatherPrepare's call in
368
- // src/commands/prepare.ts (git grep -n fetchExactCommit -- src/ is the
369
- // check that keeps "only" true), so probe never reaches it and
370
- // its splice sites cannot appear on this stream. Do not add it back by
371
- // symmetry with prepare.
372
- //
373
- // generatedCommitOrEmpty is not in this list either: it cannot throw at
374
- // all. It (src/provenance.ts) delegates to readGeneratedCommitLenient,
375
- // which catches every failure and returns "".
376
- //
377
- // A non-AdapterFailure re-thrown by runCodexOperation's closing `throw cause`
378
- // (src/harnesses/codex/adapter.ts) does NOT reach here: inspect() catches it and converts
379
- // it to a hand-written message per AGENTS.md's reader-diagnostics rule -- a
380
- // rethrown cause is exactly the failure src/harnesses/codex/adapter.ts declined to own, so
381
- // its text must never reach this stream. See §3.3a.
382
- //
383
- // gatherProbe performs no writes of its own, so this catch cannot also be
384
- // reached by an EPIPE from probe's own output — every write below runs
385
- // 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.
386
161
  ctx.stderr.write(`error: ${oneLine(cause)}\n`);
387
162
  return 1;
388
163
  }
389
- // Replay first, on both paths: the shell validator replayed every response's
390
- // messages whether or not that response was a failure at
391
- // `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/validate-adapter-response.py:268::replay(messages)`.
164
+ // Replay before reporting a command-level result on either path.
392
165
  for (const each of outcome.outcomes)
393
166
  replayOutcome(each, ctx);
394
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:103::return validateRecord`). Calling loadSavedSelection here preserves
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.