audit-tools 0.30.49 → 0.30.51

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 (117) hide show
  1. package/dist/audit/cli/advanceAuditCommand.d.ts.map +1 -1
  2. package/dist/audit/cli/advanceAuditCommand.js +8 -4
  3. package/dist/audit/cli/advanceAuditCommand.js.map +1 -1
  4. package/dist/audit/cli/args.d.ts.map +1 -1
  5. package/dist/audit/cli/args.js +9 -2
  6. package/dist/audit/cli/args.js.map +1 -1
  7. package/dist/audit/cli/mergeAndIngestCommand.d.ts.map +1 -1
  8. package/dist/audit/cli/mergeAndIngestCommand.js +6 -22
  9. package/dist/audit/cli/mergeAndIngestCommand.js.map +1 -1
  10. package/dist/audit/cli/validateResultCommand.d.ts.map +1 -1
  11. package/dist/audit/cli/validateResultCommand.js +19 -6
  12. package/dist/audit/cli/validateResultCommand.js.map +1 -1
  13. package/dist/audit/cli/workerRunCommand.d.ts.map +1 -1
  14. package/dist/audit/cli/workerRunCommand.js +6 -21
  15. package/dist/audit/cli/workerRunCommand.js.map +1 -1
  16. package/dist/audit/contracts/workerSchemas.d.ts +27 -3
  17. package/dist/audit/contracts/workerSchemas.d.ts.map +1 -1
  18. package/dist/audit/contracts/workerSchemas.js +5 -1
  19. package/dist/audit/contracts/workerSchemas.js.map +1 -1
  20. package/dist/audit/extractors/analyzers/html.d.ts +1 -0
  21. package/dist/audit/extractors/analyzers/html.d.ts.map +1 -1
  22. package/dist/audit/extractors/analyzers/html.js +5 -3
  23. package/dist/audit/extractors/analyzers/html.js.map +1 -1
  24. package/dist/audit/extractors/browserExtension.d.ts.map +1 -1
  25. package/dist/audit/extractors/browserExtension.js +1 -8
  26. package/dist/audit/extractors/browserExtension.js.map +1 -1
  27. package/dist/audit/extractors/graphRoutes.d.ts.map +1 -1
  28. package/dist/audit/extractors/graphRoutes.js +4 -1
  29. package/dist/audit/extractors/graphRoutes.js.map +1 -1
  30. package/dist/audit/orchestrator/autoFixExecutor.d.ts +8 -0
  31. package/dist/audit/orchestrator/autoFixExecutor.d.ts.map +1 -1
  32. package/dist/audit/orchestrator/autoFixExecutor.js +68 -35
  33. package/dist/audit/orchestrator/autoFixExecutor.js.map +1 -1
  34. package/dist/audit/orchestrator/runtimeCommand.d.ts.map +1 -1
  35. package/dist/audit/orchestrator/runtimeCommand.js +7 -1
  36. package/dist/audit/orchestrator/runtimeCommand.js.map +1 -1
  37. package/dist/audit/orchestrator/selectiveDeepening/index.d.ts.map +1 -1
  38. package/dist/audit/orchestrator/selectiveDeepening/index.js +12 -8
  39. package/dist/audit/orchestrator/selectiveDeepening/index.js.map +1 -1
  40. package/dist/audit/orchestrator/taskBuilder.d.ts.map +1 -1
  41. package/dist/audit/orchestrator/taskBuilder.js +0 -32
  42. package/dist/audit/orchestrator/taskBuilder.js.map +1 -1
  43. package/dist/audit/quota/discoveredLimits.d.ts +12 -0
  44. package/dist/audit/quota/discoveredLimits.d.ts.map +1 -1
  45. package/dist/audit/quota/discoveredLimits.js +30 -25
  46. package/dist/audit/quota/discoveredLimits.js.map +1 -1
  47. package/dist/audit/validation/auditResults.d.ts +16 -0
  48. package/dist/audit/validation/auditResults.d.ts.map +1 -1
  49. package/dist/audit/validation/auditResults.js +35 -1
  50. package/dist/audit/validation/auditResults.js.map +1 -1
  51. package/dist/remediate/phases/close.d.ts.map +1 -1
  52. package/dist/remediate/phases/close.js +16 -6
  53. package/dist/remediate/phases/close.js.map +1 -1
  54. package/dist/remediate/steps/contractPipeline.d.ts +4 -1
  55. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  56. package/dist/remediate/steps/contractPipeline.js +63 -47
  57. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  58. package/dist/remediate/steps/contractPipelinePrompts.d.ts +12 -1
  59. package/dist/remediate/steps/contractPipelinePrompts.d.ts.map +1 -1
  60. package/dist/remediate/steps/contractPipelinePrompts.js +29 -18
  61. package/dist/remediate/steps/contractPipelinePrompts.js.map +1 -1
  62. package/dist/remediate/steps/dispatch.d.ts +20 -3
  63. package/dist/remediate/steps/dispatch.d.ts.map +1 -1
  64. package/dist/remediate/steps/dispatch.js +193 -116
  65. package/dist/remediate/steps/dispatch.js.map +1 -1
  66. package/dist/remediate/steps/finalGate.d.ts +88 -0
  67. package/dist/remediate/steps/finalGate.d.ts.map +1 -0
  68. package/dist/remediate/steps/finalGate.js +171 -0
  69. package/dist/remediate/steps/finalGate.js.map +1 -0
  70. package/dist/remediate/steps/leanFastPath.d.ts.map +1 -1
  71. package/dist/remediate/steps/leanFastPath.js +12 -0
  72. package/dist/remediate/steps/leanFastPath.js.map +1 -1
  73. package/dist/remediate/steps/nextStep.d.ts +3 -81
  74. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  75. package/dist/remediate/steps/nextStep.js +36 -169
  76. package/dist/remediate/steps/nextStep.js.map +1 -1
  77. package/dist/remediate/validation/contractPipeline.d.ts.map +1 -1
  78. package/dist/remediate/validation/contractPipeline.js +17 -3
  79. package/dist/remediate/validation/contractPipeline.js.map +1 -1
  80. package/dist/shared/index.d.ts +1 -0
  81. package/dist/shared/index.d.ts.map +1 -1
  82. package/dist/shared/index.js +1 -0
  83. package/dist/shared/index.js.map +1 -1
  84. package/dist/shared/io/frictionCapture.d.ts +14 -1
  85. package/dist/shared/io/frictionCapture.d.ts.map +1 -1
  86. package/dist/shared/io/frictionCapture.js +28 -3
  87. package/dist/shared/io/frictionCapture.js.map +1 -1
  88. package/dist/shared/observability/runLog.d.ts +1 -1
  89. package/dist/shared/observability/runLog.d.ts.map +1 -1
  90. package/dist/shared/observability/runLog.js.map +1 -1
  91. package/dist/shared/providers/providerConfirmation.d.ts +16 -1
  92. package/dist/shared/providers/providerConfirmation.d.ts.map +1 -1
  93. package/dist/shared/providers/providerConfirmation.js +13 -13
  94. package/dist/shared/providers/providerConfirmation.js.map +1 -1
  95. package/dist/shared/providers/providerFactory.d.ts.map +1 -1
  96. package/dist/shared/providers/providerFactory.js +10 -14
  97. package/dist/shared/providers/providerFactory.js.map +1 -1
  98. package/dist/shared/providers/providerPathGuard.d.ts +21 -0
  99. package/dist/shared/providers/providerPathGuard.d.ts.map +1 -0
  100. package/dist/shared/providers/providerPathGuard.js +69 -0
  101. package/dist/shared/providers/providerPathGuard.js.map +1 -0
  102. package/dist/shared/providers/sharedProviderConfirmation.d.ts +14 -2
  103. package/dist/shared/providers/sharedProviderConfirmation.d.ts.map +1 -1
  104. package/dist/shared/providers/sharedProviderConfirmation.js +26 -5
  105. package/dist/shared/providers/sharedProviderConfirmation.js.map +1 -1
  106. package/dist/shared/quota/apiPool.d.ts +6 -1
  107. package/dist/shared/quota/apiPool.d.ts.map +1 -1
  108. package/dist/shared/quota/apiPool.js +13 -2
  109. package/dist/shared/quota/apiPool.js.map +1 -1
  110. package/dist/shared/quota/fileLock.d.ts +17 -2
  111. package/dist/shared/quota/fileLock.d.ts.map +1 -1
  112. package/dist/shared/quota/fileLock.js +21 -19
  113. package/dist/shared/quota/fileLock.js.map +1 -1
  114. package/dist/shared/types/providerConfirmation.d.ts +9 -0
  115. package/dist/shared/types/providerConfirmation.d.ts.map +1 -1
  116. package/package.json +2 -2
  117. package/scripts/audit/postinstall.mjs +22 -14
@@ -69,6 +69,92 @@ function _capacityPoolSummary(poolId, slots, schedule) {
69
69
  function sortRosterMostCapableFirst(roster) {
70
70
  return [...roster].sort((a, b) => compareTier(b.rank, a.rank));
71
71
  }
72
+ async function buildHostPoolPreamble(input) {
73
+ const sessionConfig = input.sessionConfig ?? {};
74
+ const providerName = input.providerName ??
75
+ sessionConfig.provider ??
76
+ "claude-code";
77
+ const hostModel = input.hostModel ??
78
+ sessionConfig.block_quota
79
+ ?.host_model ??
80
+ null;
81
+ // Quota-key identity: resolved model name, else the host's opaque id, else
82
+ // null → `provider/*`. Per-roster-rank `model_id` overrides per pool below.
83
+ const quotaModelKeySegment = hostModel ?? input.hostModelId ?? null;
84
+ const roster = input.hostModels?.length
85
+ ? sortRosterMostCapableFirst(input.hostModels)
86
+ : null;
87
+ const hostLimit = resolveHostConcurrencyLimit({
88
+ hostMaxConcurrent: input.hostMaxConcurrent,
89
+ sessionConfig,
90
+ env: input.env,
91
+ });
92
+ // The capability handshake: the host reported its dispatch model's real
93
+ // context/output window this session (the roster's most capable entry under
94
+ // the multi-rank handshake). Carried into the pool's discoveredLimits so the
95
+ // shared discovered_capability rung sizes the budget to the real window
96
+ // instead of the conservative 32k floor. RPM/TPM stay null and fill from the
97
+ // learned quota state.
98
+ const hostContextTokens = input.hostContextTokens ?? roster?.[0]?.context_tokens ?? null;
99
+ const hostOutputTokens = input.hostOutputTokens ?? roster?.[0]?.output_tokens ?? null;
100
+ const hostCapabilityLimits = hostContextTokens != null || hostOutputTokens != null
101
+ ? { context_tokens: hostContextTokens, output_tokens: hostOutputTokens }
102
+ : null;
103
+ let quotaEntries = {};
104
+ try {
105
+ const state = await readQuotaState();
106
+ quotaEntries = state.entries;
107
+ }
108
+ catch (err) {
109
+ process.stderr.write(`[waveScheduler] readQuotaState failed; degrading to no learned quota entry. ${err instanceof Error ? err.message : String(err)}\n`);
110
+ }
111
+ // The proactive quota snapshot (Claude OAuth source, then learned) so the
112
+ // scheduler can throttle/cooldown from live remaining quota — mirrors
113
+ // audit-code's buildDispatchPool. PREPEND the host-session source keyed on
114
+ // this host pool's own (provider, model) key — first-class PRE-WALL source
115
+ // (graduated remaining_pct → LOW/CRITICAL throttle before a 429), gating on the
116
+ // exact key so it never masks the proactive/learned sources.
117
+ const quotaSource = buildQuotaSource({
118
+ halfLifeHours: sessionConfig.quota
119
+ ?.empirical_half_life_hours,
120
+ hostSession: input.hostSession ??
121
+ new HostSessionQuotaSource({
122
+ providerModelKey: buildProviderModelKey(providerName, quotaModelKeySegment),
123
+ }),
124
+ });
125
+ // One capacity pool per reported roster rank (most capable first), each with
126
+ // its own discovered window and quota key; a single pool for the scalar/absent
127
+ // handshake. Built via the shared host-pool-from-roster core so the pool shape
128
+ // + account-keyed pool ids can't drift across the two consumers.
129
+ const primaryPools = await buildHostModelPools({
130
+ providerName,
131
+ hostModel,
132
+ hostConcurrencyLimit: hostLimit,
133
+ quotaSource,
134
+ quotaEntries,
135
+ roster,
136
+ resolve: (entry) => ({
137
+ poolKey: buildProviderModelKey(providerName, entry?.model_id ?? quotaModelKeySegment),
138
+ discoveredLimits: entry
139
+ ? { context_tokens: entry.context_tokens, output_tokens: entry.output_tokens }
140
+ : hostCapabilityLimits,
141
+ }),
142
+ });
143
+ return {
144
+ sessionConfig,
145
+ providerName,
146
+ hostModel,
147
+ quotaModelKeySegment,
148
+ roster,
149
+ hostLimit,
150
+ hostContextTokens,
151
+ hostOutputTokens,
152
+ hostCapabilityLimits,
153
+ quotaEntries,
154
+ quotaSource,
155
+ primaryPools,
156
+ };
157
+ }
72
158
  export async function scheduleWave(input) {
73
159
  const sessionConfig = input.sessionConfig ?? {};
74
160
  const providerName = input.providerName ?? sessionConfig.provider ?? "claude-code";
@@ -92,12 +178,6 @@ export async function scheduleWave(input) {
92
178
  // learned quota state.
93
179
  const hostContextTokens = input.hostContextTokens ?? roster?.[0]?.context_tokens ?? null;
94
180
  const hostOutputTokens = input.hostOutputTokens ?? roster?.[0]?.output_tokens ?? null;
95
- const hostCapabilityLimits = hostContextTokens != null || hostOutputTokens != null
96
- ? {
97
- context_tokens: hostContextTokens,
98
- output_tokens: hostOutputTokens,
99
- }
100
- : null;
101
181
  const quota = sessionConfig.quota;
102
182
  if (!quota || quota.enabled === false) {
103
183
  const cap = hostLimit?.active_subagents ?? DEFAULT_WAVE_SIZE;
@@ -127,48 +207,19 @@ export async function scheduleWave(input) {
127
207
  capacity_pools: [_capacityPoolSummary(buildProviderModelKey(providerName, quotaModelKeySegment), waveSize, schedule)],
128
208
  };
129
209
  }
130
- let quotaEntries = {};
131
- try {
132
- const state = await readQuotaState();
133
- quotaEntries = state.entries;
134
- }
135
- catch (err) {
136
- process.stderr.write(`[waveScheduler] readQuotaState failed; falling back to default wave size. ${err instanceof Error ? err.message : String(err)}\n`);
137
- }
138
- // The proactive quota snapshot (Claude OAuth source, then learned) so the
139
- // scheduler can throttle/cooldown from live remaining quota — mirrors
140
- // audit-code's buildDispatchPool. Cached per key, so one probe per burst.
141
- // PREPEND the host-session source keyed on this host pool's own (provider,
142
- // model) key — first-class PRE-WALL source (graduated remaining_pct →
143
- // LOW/CRITICAL throttle before a 429), gating on the exact key so it never masks
144
- // the proactive/learned sources. Mirrors audit-code's buildDispatchPool.
145
- const quotaSource = buildQuotaSource({
146
- halfLifeHours: sessionConfig.quota
147
- ?.empirical_half_life_hours,
148
- hostSession: new HostSessionQuotaSource({
149
- providerModelKey: buildProviderModelKey(providerName, quotaModelKeySegment),
150
- }),
151
- });
152
- // One capacity pool per reported roster rank (most capable first), each with its
153
- // own discovered window and quota key; a single pool for the scalar/absent
154
- // handshake. Built via the shared host-pool-from-roster core (same as
155
- // buildConfirmedPools) so the pool shape + account-keyed pool ids can't drift.
156
- const pools = await buildHostModelPools({
157
- providerName,
158
- hostModel,
159
- hostConcurrencyLimit: hostLimit,
160
- quotaSource,
161
- quotaEntries,
162
- roster,
163
- resolve: (entry) => ({
164
- poolKey: buildProviderModelKey(providerName, entry?.model_id ?? quotaModelKeySegment),
165
- discoveredLimits: entry
166
- ? { context_tokens: entry.context_tokens, output_tokens: entry.output_tokens }
167
- : hostCapabilityLimits,
168
- }),
210
+ const preamble = await buildHostPoolPreamble({
211
+ sessionConfig: input.sessionConfig,
212
+ providerName: input.providerName,
213
+ hostModel: input.hostModel,
214
+ hostMaxConcurrent: input.hostMaxConcurrent,
215
+ hostContextTokens: input.hostContextTokens,
216
+ hostOutputTokens: input.hostOutputTokens,
217
+ hostModels: input.hostModels,
218
+ hostModelId: input.hostModelId,
219
+ env: input.env,
169
220
  });
170
221
  const capacity = computeDispatchCapacity({
171
- pools,
222
+ pools: preamble.primaryPools,
172
223
  sessionConfig,
173
224
  pendingItemTokens: normalizeSlotTokens(input.estimatedSlotTokens, input.itemCount),
174
225
  });
@@ -187,55 +238,17 @@ export async function scheduleWave(input) {
187
238
  * `driveRollingImplementDispatch`.
188
239
  */
189
240
  export async function buildConfirmedPools(input) {
190
- const sessionConfig = input.sessionConfig ?? {};
191
- const providerName = sessionConfig.provider ?? "claude-code";
192
- const hostModel = sessionConfig.block_quota
193
- ?.host_model ?? null;
194
- const quotaModelKeySegment = hostModel ?? input.hostModelId ?? null;
195
- const roster = input.hostModels?.length
196
- ? sortRosterMostCapableFirst(input.hostModels)
197
- : null;
198
- const hostLimit = resolveHostConcurrencyLimit({
241
+ // Resolve identity/limits and build the per-rank host-model pools via the
242
+ // SAME preamble `scheduleWave` uses — the two no longer keep parallel copies.
243
+ const { sessionConfig, providerName, quotaSource, quotaEntries, primaryPools } = await buildHostPoolPreamble({
244
+ sessionConfig: input.sessionConfig,
199
245
  hostMaxConcurrent: input.hostMaxConcurrent,
200
- sessionConfig,
246
+ hostContextTokens: input.hostContextTokens,
247
+ hostOutputTokens: input.hostOutputTokens,
248
+ hostModels: input.hostModels,
249
+ hostModelId: input.hostModelId,
201
250
  env: input.env,
202
- });
203
- const hostContextTokens = input.hostContextTokens ?? roster?.[0]?.context_tokens ?? null;
204
- const hostOutputTokens = input.hostOutputTokens ?? roster?.[0]?.output_tokens ?? null;
205
- const hostCapabilityLimits = hostContextTokens != null || hostOutputTokens != null
206
- ? { context_tokens: hostContextTokens, output_tokens: hostOutputTokens }
207
- : null;
208
- let quotaEntries = {};
209
- try {
210
- const state = await readQuotaState();
211
- quotaEntries = state.entries;
212
- }
213
- catch {
214
- // Non-fatal: a missing/locked quota state degrades to no learned entry.
215
- }
216
- const quotaSource = buildQuotaSource({
217
- halfLifeHours: sessionConfig.quota
218
- ?.empirical_half_life_hours,
219
- hostSession: input.hostSession ??
220
- new HostSessionQuotaSource({
221
- providerModelKey: buildProviderModelKey(providerName, quotaModelKeySegment),
222
- }),
223
- });
224
- // Host-model pools via the shared host-pool-from-roster core (identical shape across
225
- // audit + remediate). The per-tool part is just the key + the roster/capability limits.
226
- const primaryPools = await buildHostModelPools({
227
- providerName,
228
- hostModel,
229
- hostConcurrencyLimit: hostLimit,
230
- quotaSource,
231
- quotaEntries,
232
- roster,
233
- resolve: (entry) => ({
234
- poolKey: buildProviderModelKey(providerName, entry?.model_id ?? quotaModelKeySegment),
235
- discoveredLimits: entry
236
- ? { context_tokens: entry.context_tokens, output_tokens: entry.output_tokens }
237
- : hostCapabilityLimits,
238
- }),
251
+ hostSession: input.hostSession,
239
252
  });
240
253
  // Every configured dispatchable backend source (any non-IDE source: NIM/vLLM API,
241
254
  // a CLI pool, …) becomes a CapacityPool alongside the primary, so the scheduler's
@@ -1329,17 +1342,48 @@ export function normalizeNodeTestCommand(cmd) {
1329
1342
  return cmd;
1330
1343
  return trimmed.replace(/^node\b/, "node --import tsx/esm");
1331
1344
  }
1345
+ /**
1346
+ * True when `cmd` is a test-runner invocation whose target is a whole directory
1347
+ * or the entire suite rather than specific test files — e.g. `npx vitest run
1348
+ * tests/remediate`, `vitest run` (no path), `node --test tests/audit/`. Such a
1349
+ * command, run as an *additional* per-node verify alongside the scoped derive,
1350
+ * re-enters the FULL suite inside a per-node worktree. That is the structural
1351
+ * deadlock proven 2026-06-30: a source node's whole-suite verify fails on a
1352
+ * stale test owned by a DIFFERENT node, and concurrent worktrees race shared
1353
+ * test temp dirs. The derived verify already runs this node's OWN touched
1354
+ * tests, so a host/DAG-authored whole-suite command adds only risk — the tool
1355
+ * drops it rather than relying on the author to scope it (enforce-in-tooling,
1356
+ * never host discretion). A command naming at least one concrete `.test.<ext>`
1357
+ * file is scoped and kept.
1358
+ */
1359
+ export function isWholeSuiteTestCommand(cmd) {
1360
+ const c = cmd.trim();
1361
+ const isVitest = /\bvitest\b\s+run\b/.test(c) || /\bvitest\b(?!\s+run)/.test(c);
1362
+ const isNodeTest = /^node\b/.test(c) && /\s--test\b/.test(c);
1363
+ if (!isVitest && !isNodeTest)
1364
+ return false;
1365
+ // Tokenise; a concrete test file makes it scoped. Anything that is a runner
1366
+ // with no concrete test-file target (bare runner, or a directory/glob target)
1367
+ // is whole-suite.
1368
+ const namesConcreteTestFile = /(^|\s)[^\s]*\.test\.(ts|tsx|mjs|cjs|js)(\s|$)/.test(c);
1369
+ return !namesConcreteTestFile;
1370
+ }
1332
1371
  /**
1333
1372
  * Filter a node's `targeted_commands` to the build-free subset for the per-node
1334
1373
  * verify section. Build-prepending or build commands are dropped (the host runs
1335
- * the build centrally) rather than emitted into the prompt. Surviving
1336
- * `node --test` commands are normalized to carry the tsx loader.
1374
+ * the build centrally) rather than emitted into the prompt. Whole-suite /
1375
+ * whole-directory test runs are dropped too they re-enter the full suite in a
1376
+ * per-node worktree and re-create the cross-node verify deadlock (the scoped
1377
+ * derive already covers this node's own tests). Surviving `node --test`
1378
+ * commands are normalized to carry the tsx loader.
1337
1379
  */
1338
1380
  function buildFreeVerifyCommands(commands) {
1339
1381
  if (!Array.isArray(commands))
1340
1382
  return [];
1341
1383
  return commands
1342
- .filter((c) => typeof c === "string" && isBuildFreeVerifyCommand(c))
1384
+ .filter((c) => typeof c === "string" &&
1385
+ isBuildFreeVerifyCommand(c) &&
1386
+ !isWholeSuiteTestCommand(c))
1343
1387
  .map(normalizeNodeTestCommand);
1344
1388
  }
1345
1389
  /** A repo-relative test path → the runner that executes that file directly. */
@@ -1419,6 +1463,18 @@ function markTerminal(item) {
1419
1463
  item.started_at ??= now;
1420
1464
  item.completed_at = now;
1421
1465
  }
1466
+ /**
1467
+ * Load a worker's already-written implement result, distinguishing an ABSENT
1468
+ * file (the worker hasn't run yet → re-dispatch from scratch) from a PRESENT but
1469
+ * INVALID one (the worker ran but emitted malformed/unparseable JSON or a result
1470
+ * that fails the contract). A bare `catch → undefined` conflated the two: a
1471
+ * written-but-invalid file looked identical to "never produced", so the merge
1472
+ * loop silently `continue`d past the block (the missing-file branch never fires
1473
+ * because the file DOES exist) and the node could neither converge nor surface
1474
+ * the corruption. We now archive the invalid file (so a clean re-dispatch can
1475
+ * write a fresh one) and report it loudly, returning `undefined` only for the
1476
+ * genuinely-absent case.
1477
+ */
1422
1478
  async function tryLoadExistingImplementResult(resultPath) {
1423
1479
  if (!existsSync(resultPath))
1424
1480
  return undefined;
@@ -1427,7 +1483,13 @@ async function tryLoadExistingImplementResult(resultPath) {
1427
1483
  assertImplementWorkerResult(result, resultPath);
1428
1484
  return result;
1429
1485
  }
1430
- catch {
1486
+ catch (err) {
1487
+ // Present but invalid: do NOT treat it as absent (which would let the block
1488
+ // be silently dropped from the merge). Archive the corrupt file and surface
1489
+ // the reason so a clean re-dispatch produces a valid result.
1490
+ process.stderr.write(`[remediate-code] dispatch: existing implement result ${resultPath} is present but ` +
1491
+ `invalid (${err instanceof Error ? err.message : String(err)}); archiving and re-dispatching\n`);
1492
+ await archiveIncompleteImplementResult(resultPath);
1431
1493
  return undefined;
1432
1494
  }
1433
1495
  }
@@ -1773,32 +1835,47 @@ allowed in addition to the file access above.
1773
1835
  // Infra-modifying block detection
1774
1836
  // ---------------------------------------------------------------------------
1775
1837
  /**
1776
- * Canonical set of infra files whose modification can break the live dispatcher
1777
- * while the run is in progress. Paths are repo-relative forward-slash strings.
1838
+ * The live dispatch/orchestration modules whose modification can break the
1839
+ * running engine mid-run. Derived from the REAL post-A12 source layout
1840
+ * (`src/remediate/...`) — this module IS one of them (`steps/dispatch.ts`), so
1841
+ * the list is anchored to the actual files on disk, not a hand-typed monorepo
1842
+ * path that drifts when the tree is reorganised (the pre-A12
1843
+ * `packages/remediate-code/...` list silently matched NOTHING after the
1844
+ * collapse, so every infra block rendered as non-infra). Each entry is the
1845
+ * module's path relative to the `src/remediate` area, forward-slash form.
1778
1846
  */
1779
- const INFRA_FILE_PATHS = new Set([
1780
- "packages/remediate-code/src/steps/nextStep.ts",
1781
- "packages/remediate-code/src/steps/dispatch.ts",
1782
- "packages/remediate-code/src/state/store.ts",
1783
- "packages/remediate-code/src/steps/contractPipeline.ts",
1784
- "packages/remediate-code/src/steps/waveScheduler.ts",
1785
- "packages/remediate-code/src/steps/stepWriter.ts",
1786
- ]);
1847
+ const INFRA_MODULE_SUBPATHS = [
1848
+ "steps/nextStep.ts",
1849
+ "steps/dispatch.ts",
1850
+ "state/store.ts",
1851
+ "steps/contractPipeline.ts",
1852
+ "steps/stepWriter.ts",
1853
+ ];
1787
1854
  /**
1788
- * Returns true when any path in `writePaths` matches the set of infra files
1789
- * (normalised to repo-relative forward-slash strings). Used to gate the
1790
- * live-surface verification section in the implement prompt.
1855
+ * The infra module sub-paths anchored under `src/remediate/` the canonical
1856
+ * repo-relative form for the current (post-A12) single-package layout. A write
1857
+ * path matches when its normalised (forward-slash) form ends with one of these
1858
+ * segments, so an absolute worktree path
1859
+ * (`.../worktrees/foo/src/remediate/steps/dispatch.ts`), a repo-relative path
1860
+ * (`src/remediate/steps/dispatch.ts`), or a Windows backslash path all match,
1861
+ * while a same-basename file in another area (`src/audit/steps/dispatch.ts`)
1862
+ * does not.
1863
+ */
1864
+ const INFRA_FILE_SEGMENTS = INFRA_MODULE_SUBPATHS.map((sub) => `src/remediate/${sub}`);
1865
+ /**
1866
+ * Returns true when any path in `writePaths` is one of the live infra modules.
1867
+ * Paths are normalised to forward-slash form (win32 backslash → `/`) and matched
1868
+ * by trailing repo-relative segment so absolute/worktree/relative spellings all
1869
+ * resolve identically. Used to gate the live-surface verification section in the
1870
+ * implement prompt.
1791
1871
  */
1792
1872
  export function isInfraModifyingBlock(writePaths) {
1793
1873
  for (const p of writePaths) {
1794
1874
  const normalized = p.replace(/\\/g, "/");
1795
- if (INFRA_FILE_PATHS.has(normalized))
1796
- return true;
1797
- // Also match if the path ends with the infra file's relative segment,
1798
- // e.g. an absolute path like /abs/root/packages/remediate-code/src/steps/dispatch.ts
1799
- for (const infraPath of INFRA_FILE_PATHS) {
1800
- if (normalized.endsWith("/" + infraPath))
1875
+ for (const segment of INFRA_FILE_SEGMENTS) {
1876
+ if (normalized === segment || normalized.endsWith("/" + segment)) {
1801
1877
  return true;
1878
+ }
1802
1879
  }
1803
1880
  }
1804
1881
  return false;