immune-brain 4.3.0 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,7 +39,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
39
39
  }
40
40
 
41
41
  // plugins/immune-brain/runtime/plugin_version.ts
42
- var PLUGIN_VERSION = "4.3.0";
42
+ var PLUGIN_VERSION = "4.4.1";
43
43
 
44
44
  // plugins/immune-brain/runtime/claude/interaction.ts
45
45
  import { createHash, randomUUID } from "node:crypto";
@@ -1362,6 +1362,88 @@ function reviewAdvisoryRecords(verdict) {
1362
1362
  evidence: finding.evidence ?? null
1363
1363
  }));
1364
1364
  }
1365
+ var QA_STAGES = ["resolution", "prepare", "check", "integrity"];
1366
+ var QA_OUTCOMES = [
1367
+ "running",
1368
+ "passed",
1369
+ "executable_or_cwd_unavailable",
1370
+ "process_launch_failed",
1371
+ "process_cleanup_failed",
1372
+ "protected_input_or_output_drift",
1373
+ "execution_metadata_limit_exceeded",
1374
+ "timeout",
1375
+ "output_limit",
1376
+ "nonzero_exit",
1377
+ "execution_failed",
1378
+ "command_identity_changed",
1379
+ "delivery_cleanup_failed",
1380
+ "delivery_unavailable"
1381
+ ];
1382
+ function qaDiagnosticMetadata(d) {
1383
+ if (typeof d.acceptance_id !== "string" || !d.acceptance_id || !/^acceptance\/\d{1,6}\/verification\/(command|environment\/prepare)$/.test(d.descriptor_ref) || !/^sha256:[a-f0-9]{64}$/.test(d.descriptor_digest) || !QA_STAGES.includes(d.stage) || !QA_OUTCOMES.includes(d.outcome) || !Number.isFinite(d.elapsed_ms) || d.elapsed_ms < 0 || [d.stdout_bytes, d.stderr_bytes].some((n) => n !== null && (!Number.isSafeInteger(n) || n < 0)) || d.exit_code !== null && !Number.isSafeInteger(d.exit_code))
1384
+ throw new Error("invalid QA diagnostic metadata");
1385
+ const safe = {
1386
+ acceptance_id: d.acceptance_id,
1387
+ descriptor_ref: d.descriptor_ref,
1388
+ descriptor_digest: d.descriptor_digest,
1389
+ stage: d.stage,
1390
+ outcome: d.outcome,
1391
+ elapsed_ms: d.elapsed_ms,
1392
+ exit_code: d.exit_code,
1393
+ stdout_bytes: d.stdout_bytes,
1394
+ stderr_bytes: d.stderr_bytes
1395
+ };
1396
+ if (Buffer.byteLength(JSON.stringify(safe)) > 16384)
1397
+ throw new QaPreparationError("prepare", [], "execution_metadata_limit_exceeded");
1398
+ return safe;
1399
+ }
1400
+
1401
+ class QaPreparationError extends Error {
1402
+ stage;
1403
+ acceptance_ids;
1404
+ reason;
1405
+ diagnostics;
1406
+ constructor(stage, acceptance_ids, reason, diagnostics = []) {
1407
+ if (!QA_STAGES.includes(stage) || !QA_OUTCOMES.includes(reason))
1408
+ throw new Error("invalid QA preparation classification");
1409
+ const safe = diagnostics.map(qaDiagnosticMetadata);
1410
+ if (Buffer.byteLength(JSON.stringify(safe)) > 16384)
1411
+ throw new QaPreparationError("prepare", [], "execution_metadata_limit_exceeded");
1412
+ super(`QA ${stage} failed (${reason}); affected checks=${acceptance_ids.length}`);
1413
+ this.stage = stage;
1414
+ this.acceptance_ids = acceptance_ids;
1415
+ this.reason = reason;
1416
+ this.diagnostics = diagnostics;
1417
+ this.diagnostics = safe;
1418
+ this.name = "QaPreparationError";
1419
+ }
1420
+ }
1421
+ function deriveAssuranceRecovery(taskId, fresh, result, findings = []) {
1422
+ if (fresh.error || fresh.claim?.task_id !== taskId || fresh.projection.lifecycle !== "active")
1423
+ return null;
1424
+ const p = fresh.projection;
1425
+ const findingIds = [...p.blocking_finding_ids, ...p.unresolved_user_decision_ids, ...p.replan_required_ids];
1426
+ const scope = result.reason?.includes("task delivery contains paths outside the authorization envelope");
1427
+ const migration = result.reason?.includes("verification_contract_migration_required");
1428
+ const unstaged = result.reason?.includes("task delivery has unstaged or untracked changes");
1429
+ const reviewPreparation = result.state === "review_preparation_failed" && p.next_obligation === "run_review";
1430
+ const authority = scope || migration || p.next_obligation === "resolve_user_decision" || p.next_obligation === "revise_intent" || p.unresolved_user_decision_ids.length > 0 || p.replan_required_ids.length > 0 || p.authorization.state !== "none";
1431
+ const repair = unstaged || p.next_obligation === "resolve_findings" || p.blocking_finding_ids.length > 0;
1432
+ if (!authority && !repair && !result.environment_failure && !reviewPreparation)
1433
+ return null;
1434
+ const affected = findings.filter((f) => f.status === "open" && f.id && findingIds.includes(f.id) && f.acceptance_id).map((f) => f.acceptance_id);
1435
+ const category = authority ? "authorization" : repair ? "repair" : "environment";
1436
+ return {
1437
+ category,
1438
+ task_id: taskId,
1439
+ run_id: p.run_id ?? null,
1440
+ record_revision: p.record_revision,
1441
+ next_obligation: p.next_obligation,
1442
+ finding_ids: findingIds,
1443
+ acceptance_ids: [...new Set(affected.length ? affected : result.diagnostics?.length ? result.diagnostics.map((d) => d.acceptance_id) : reviewPreparation ? p.fresh_acceptance_ids : p.missing_acceptance_ids)],
1444
+ next_action: scope ? "Reconcile the listed paths with the TaskIntent scope: unstage unrelated changes or submit the required native Intent revision." : migration ? "route Planner for the verification descriptor v2 Intent revision and native gate" : category === "authorization" ? p.authorization.blocked ? "inspect authority state" : p.next_obligation === "revise_intent" ? "route Planner for the required Intent revision and native gate" : "request_authorization" : unstaged ? "Stage only the scoped implementation changes, then call advance_assurance." : category === "repair" ? "Route foreground Executor to repair and verify the identified findings; dispose each by resolve_finding or evidence-bound refute_finding, then run fresh advance_assurance." : reviewPreparation ? "Repair the Review evidence environment, then call advance_assurance to resume run_review; retain fresh QA and do not re-confirm the batch." : "Repair the identified verification environment, then call advance_assurance; a local check is non-attesting."
1445
+ };
1446
+ }
1365
1447
  var QA_MIN_JOB_TIMEOUT_SECONDS = 15 * 60;
1366
1448
  var QA_MAX_JOB_TIMEOUT_SECONDS = 60 * 60;
1367
1449
  var QA_JOB_OVERHEAD_SECONDS = 2 * 60;
@@ -1673,6 +1755,36 @@ class AssuranceCoordinator {
1673
1755
  return this.sessionGeneration;
1674
1756
  }
1675
1757
  async advance(taskId, ctx, signal, onUpdate) {
1758
+ const checks = new Map;
1759
+ const result = await this.advanceOnce(taskId, ctx, signal, (update) => {
1760
+ const diagnostic = update.details.diagnostic;
1761
+ if (diagnostic) {
1762
+ checks.set(diagnostic.descriptor_ref, diagnostic);
1763
+ if (Buffer.byteLength(JSON.stringify([...checks.values()])) > 16384)
1764
+ throw new QaPreparationError("prepare", [diagnostic.acceptance_id], "execution_metadata_limit_exceeded", [{ ...diagnostic, stage: "prepare", outcome: "execution_metadata_limit_exceeded" }]);
1765
+ }
1766
+ onUpdate?.(update);
1767
+ });
1768
+ const diagnostics = result.diagnostics ?? [...checks.values()];
1769
+ return this.withRecovery(taskId, ctx, diagnostics.length ? { ...result, diagnostics } : result);
1770
+ }
1771
+ async withRecovery(taskId, ctx, enriched) {
1772
+ if (enriched.recovery_error || !["failed", "blocked", "rework", "review_preparation_failed"].includes(enriched.state))
1773
+ return enriched;
1774
+ try {
1775
+ const fresh = await this.ports.projectTask(ctx.cwd, taskId);
1776
+ if (fresh.error || fresh.claim?.task_id !== taskId || fresh.projection.lifecycle !== "active")
1777
+ return { ...enriched, recovery_error: "fresh_kernel_projection_unavailable" };
1778
+ const record = await this.ports.readTaskRecord(ctx.cwd, taskId);
1779
+ if (!record.record || record.revision !== fresh.projection.record_revision)
1780
+ return { ...enriched, recovery_error: "fresh_kernel_projection_unavailable" };
1781
+ const recovery = deriveAssuranceRecovery(taskId, fresh, enriched, record.record?.findings);
1782
+ return recovery ? { ...enriched, recovery } : enriched;
1783
+ } catch {
1784
+ return { ...enriched, recovery_error: "fresh_kernel_projection_unavailable" };
1785
+ }
1786
+ }
1787
+ async advanceOnce(taskId, ctx, signal, onUpdate) {
1676
1788
  if (this.isInvocationOpen(taskId))
1677
1789
  return { state: "blocked", reason: "an authority invocation is already open" };
1678
1790
  const active = this.active(taskId);
@@ -1682,7 +1794,7 @@ class AssuranceCoordinator {
1682
1794
  try {
1683
1795
  projection = await this.ports.projectTask(ctx.cwd, taskId);
1684
1796
  } catch (error) {
1685
- return { state: "blocked", reason: `cannot validate Review reservation: ${boundedAssuranceError(error)}` };
1797
+ return { state: "blocked", reason: `cannot validate Review reservation: ${boundedAssuranceError(error)}`, recovery_error: "fresh_kernel_projection_unavailable" };
1686
1798
  }
1687
1799
  const matches = reservationStillValid(reservation, projection, taskId);
1688
1800
  if (matches)
@@ -1690,6 +1802,8 @@ class AssuranceCoordinator {
1690
1802
  if (reservation)
1691
1803
  this.releaseReviewReservation(taskId, reservation);
1692
1804
  this.rejectedReviewOperations.delete(taskId);
1805
+ if (projection.error || projection.projection.lifecycle !== "done" && projection.projection.lifecycle !== "stopped" && (!projection.claim || projection.claim.task_id !== taskId))
1806
+ return { state: "blocked", reason: projection.error ?? "no active backend claim for this task", recovery_error: "fresh_kernel_projection_unavailable" };
1693
1807
  }
1694
1808
  const refreshed = this.active(taskId);
1695
1809
  if (refreshed?.state === "running")
@@ -1708,6 +1822,7 @@ class AssuranceCoordinator {
1708
1822
  let authorityBoundaryStarted = false;
1709
1823
  let boundaryBaseline = null;
1710
1824
  let reviewPreparationStarted = false;
1825
+ let initialProjectionAvailable = false;
1711
1826
  let qaJobBudgetExceeded = false;
1712
1827
  let phase = "preparing";
1713
1828
  const operationLive = () => this.sessionActive && this.sessionGeneration === operationGeneration && this.activeOperations.get(taskId) === operationId && !operationController.signal.aborted;
@@ -1744,15 +1859,16 @@ class AssuranceCoordinator {
1744
1859
  }
1745
1860
  ensureOperationLive();
1746
1861
  if (projection.error)
1747
- return { state: "blocked", reason: projection.error };
1862
+ return { state: "blocked", reason: projection.error, recovery_error: "fresh_kernel_projection_unavailable" };
1748
1863
  if (projection.projection.lifecycle === "done" || projection.projection.lifecycle === "stopped") {
1749
1864
  this.unknownOperations.delete(taskId);
1750
1865
  return { state: projection.projection.lifecycle === "done" ? "completed" : "stopped" };
1751
1866
  }
1752
1867
  if (!projection.claim)
1753
- return { state: "blocked", reason: "no active backend claim" };
1868
+ return { state: "blocked", reason: "no active backend claim", recovery_error: "fresh_kernel_projection_unavailable" };
1754
1869
  if (projection.claim.task_id !== taskId)
1755
- return { state: "blocked", reason: `backend claim belongs to ${projection.claim.task_id}, not ${taskId}` };
1870
+ return { state: "blocked", reason: `backend claim belongs to ${projection.claim.task_id}, not ${taskId}`, recovery_error: "fresh_kernel_projection_unavailable" };
1871
+ initialProjectionAvailable = true;
1756
1872
  if (projection.projection.lifecycle === "active")
1757
1873
  this.unknownOperations.delete(taskId);
1758
1874
  const parked = await this.ports.readTaskRecord(ctx.cwd, taskId);
@@ -1824,6 +1940,7 @@ class AssuranceCoordinator {
1824
1940
  operationController.abort(new Error(`deterministic QA exceeded its declared ${Math.ceil(qaJobTimeoutMs / 60000)} minute job budget`));
1825
1941
  }, qaJobTimeoutMs);
1826
1942
  try {
1943
+ phase = "verifying";
1827
1944
  qaVerdict = await this.ports.runQa(assurance.snapshot, assurance.descriptors, {
1828
1945
  signal: operationController.signal,
1829
1946
  onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, {
@@ -1831,7 +1948,8 @@ class AssuranceCoordinator {
1831
1948
  total: item.total,
1832
1949
  acceptance_id: item.acceptance_id,
1833
1950
  acceptance_phase: item.phase,
1834
- elapsed_ms: item.elapsed_ms
1951
+ elapsed_ms: item.elapsed_ms,
1952
+ ...item.diagnostic ? { diagnostic: qaDiagnosticMetadata(item.diagnostic) } : {}
1835
1953
  })
1836
1954
  });
1837
1955
  } finally {
@@ -1964,6 +2082,31 @@ class AssuranceCoordinator {
1964
2082
  progress("review_ready", "QA passed; invoke the reserved foreground Agent, then call submit_review", { snapshot_digest: snapshotDigest(review.snapshot), review_bundle_digest: review.snapshot.review_bundle_digest ?? "", agent_params: hostReservation.dispatch });
1965
2083
  return { state: "review_ready", operation: "review", operation_id: operationId, snapshot_digest: snapshotDigest(review.snapshot), review_bundle_digest: review.snapshot.review_bundle_digest ?? "", agent_params: hostReservation.dispatch };
1966
2084
  } catch (error) {
2085
+ if (!initialProjectionAvailable && !aborted() && !(error instanceof VerificationAbortedError))
2086
+ return {
2087
+ state: "failed",
2088
+ operation: "qa",
2089
+ operation_id: operationId,
2090
+ reason: `${phase}: ${boundedAssuranceError(error)}`,
2091
+ recovery_error: "fresh_kernel_projection_unavailable"
2092
+ };
2093
+ if (error instanceof QaPreparationError && !authorityCommitted && !authorityBoundaryStarted && !aborted())
2094
+ return {
2095
+ state: "failed",
2096
+ operation: "qa",
2097
+ operation_id: operationId,
2098
+ reason: error.message,
2099
+ environment_failure: true,
2100
+ diagnostics: error.diagnostics.map(qaDiagnosticMetadata)
2101
+ };
2102
+ if (phase === "verifying" && !authorityCommitted && !authorityBoundaryStarted && !aborted() && !(error instanceof VerificationAbortedError))
2103
+ return {
2104
+ state: "failed",
2105
+ operation: "qa",
2106
+ operation_id: operationId,
2107
+ reason: "QA execution failed before attestation (execution_failed)",
2108
+ environment_failure: true
2109
+ };
1967
2110
  if (reviewPreparationStarted) {
1968
2111
  const reason = aborted() || error instanceof VerificationAbortedError ? `${phase}: host cancellation` : `${phase}: ${boundedAssuranceError(error)}`;
1969
2112
  return this.reviewPreparationFailed(taskId, operationId, reason);
@@ -1989,6 +2132,9 @@ class AssuranceCoordinator {
1989
2132
  }
1990
2133
  }
1991
2134
  async submitReview(taskId, ctx, verdictInput) {
2135
+ return this.withRecovery(taskId, ctx, await this.submitReviewOnce(taskId, ctx, verdictInput));
2136
+ }
2137
+ async submitReviewOnce(taskId, ctx, verdictInput) {
1992
2138
  const unknown = this.unknownOperations.get(taskId);
1993
2139
  if (unknown)
1994
2140
  return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason };
@@ -2004,12 +2150,12 @@ class AssuranceCoordinator {
2004
2150
  } catch (error) {
2005
2151
  const reason = boundedAssuranceError(error);
2006
2152
  this.releaseReviewReservation(taskId, reservation, reason);
2007
- return { state: "blocked", reason };
2153
+ return { state: "blocked", reason, recovery_error: "fresh_kernel_projection_unavailable" };
2008
2154
  }
2009
2155
  if (fresh.error || !fresh.claim || fresh.claim.task_id !== taskId || compareReservationSnapshot(reservation.snapshot, fresh.projection).length > 0 || fresh.projection.lifecycle !== reservation.snapshot.lifecycle || fresh.projection.artifact_state !== reservation.snapshot.artifact_state) {
2010
2156
  const reason = fresh.error ?? "assurance snapshot changed before Review submission";
2011
2157
  this.releaseReviewReservation(taskId, reservation, reason);
2012
- return { state: "blocked", reason };
2158
+ return { state: "blocked", reason, ...fresh.error || !fresh.claim || fresh.claim.task_id !== taskId ? { recovery_error: "fresh_kernel_projection_unavailable" } : {} };
2013
2159
  }
2014
2160
  if (reservation.snapshot.review_revision) {
2015
2161
  try {
@@ -7857,7 +8003,6 @@ function initializeEnrollmentGitBase(root, input, previous, base, signal) {
7857
8003
 
7858
8004
  // plugins/immune-brain/runtime/assurance/qa.ts
7859
8005
  import { createHash as createHash17 } from "node:crypto";
7860
-
7861
8006
  // plugins/immune-brain/runtime/assurance/delivery_workspace.ts
7862
8007
  import { execFileSync as execFileSync5 } from "node:child_process";
7863
8008
  import { createHash as createHash16 } from "node:crypto";
@@ -8117,19 +8262,6 @@ function deliveryTreeForSnapshot(snapshot, writeTree) {
8117
8262
  throw new Error("QA delivery identity does not match the frozen snapshot");
8118
8263
  return writeDeliveryTree(snapshot.root, captured);
8119
8264
  }
8120
-
8121
- class QaPreparationError extends Error {
8122
- stage;
8123
- acceptance_ids;
8124
- reason;
8125
- constructor(stage, acceptance_ids, reason) {
8126
- super(`QA ${stage} failed (${reason}); affected checks=${acceptance_ids.join(",")}`);
8127
- this.stage = stage;
8128
- this.acceptance_ids = acceptance_ids;
8129
- this.reason = reason;
8130
- this.name = "QaPreparationError";
8131
- }
8132
- }
8133
8265
  async function runDeterministicQa(snapshot, descriptors, options = {}) {
8134
8266
  if (snapshot.role !== "qa")
8135
8267
  throw new Error("deterministic QA requires qa role");
@@ -8157,46 +8289,80 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
8157
8289
  live();
8158
8290
  const ids = group.map((item) => item.id);
8159
8291
  const environment = group[0].descriptor.environment;
8160
- const delivery = (options._materializeDeliveryWorkspace ?? materializeDeliveryWorkspace)(snapshot.root, tree);
8161
- const home = mkdtempSync3(join9(tmpdir4(), "imm-qa-home-"));
8292
+ const diagnostics = (stage, outcome, affected = group, commandStage = "check", elapsed = 0, result) => affected.map((item) => ({
8293
+ acceptance_id: item.id,
8294
+ descriptor_ref: `acceptance/${item.index}/verification/${commandStage === "prepare" ? "environment/prepare" : "command"}`,
8295
+ descriptor_digest: `sha256:${createHash17("sha256").update(JSON.stringify(item.descriptor)).digest("hex")}`,
8296
+ stage,
8297
+ outcome,
8298
+ elapsed_ms: elapsed,
8299
+ exit_code: result?.exit_code ?? null,
8300
+ stdout_bytes: result ? Buffer.byteLength(result.stdout) : null,
8301
+ stderr_bytes: result ? Buffer.byteLength(result.stderr) : null
8302
+ }));
8303
+ const { delivery, home, deliveryPrefix } = (() => {
8304
+ let delivery, home;
8305
+ try {
8306
+ delivery = (options._materializeDeliveryWorkspace ?? materializeDeliveryWorkspace)(snapshot.root, tree);
8307
+ home = mkdtempSync3(join9(tmpdir4(), "imm-qa-home-"));
8308
+ return { delivery, home, deliveryPrefix: `${realpathSync10(delivery.root)}${sep6}` };
8309
+ } catch {
8310
+ let reason = "delivery_unavailable";
8311
+ try {
8312
+ try {
8313
+ delivery?.cleanup();
8314
+ } finally {
8315
+ if (home)
8316
+ removeTaskOwnedTree(home);
8317
+ }
8318
+ } catch {
8319
+ reason = "delivery_cleanup_failed";
8320
+ }
8321
+ throw new QaPreparationError("prepare", ids, reason, diagnostics("prepare", reason));
8322
+ }
8323
+ })();
8162
8324
  const clean = () => {
8163
8325
  if (options._materializeDeliveryWorkspace)
8164
8326
  return;
8165
8327
  try {
8166
8328
  assertDeliveryClean(delivery.root, tree, delivery.seal, environment.writable_paths);
8167
8329
  } catch {
8168
- throw new QaPreparationError("integrity", ids, "protected_input_or_output_drift");
8330
+ throw new QaPreparationError("integrity", ids, "protected_input_or_output_drift", diagnostics("integrity", "protected_input_or_output_drift"));
8169
8331
  }
8170
8332
  };
8171
8333
  const groupCommands = [];
8172
- const deliveryPrefix = `${realpathSync10(delivery.root)}${sep6}`;
8173
- const execute = async (command, stage) => {
8334
+ const execute = async (command, stage, affected = group) => {
8335
+ const started = performance.now();
8336
+ const fail = (errorStage, reason) => new QaPreparationError(errorStage, affected.map((item) => item.id), reason, diagnostics(errorStage, reason, affected, stage, Math.round(performance.now() - started)));
8174
8337
  live();
8175
8338
  clean();
8176
8339
  let frozen;
8177
8340
  try {
8178
8341
  frozen = (options._resolveVerificationCommand ?? resolveVerificationCommand)(delivery.root, command, path);
8179
8342
  } catch {
8180
- throw new QaPreparationError("resolution", ids, "executable_or_cwd_unavailable");
8343
+ throw fail("resolution", "executable_or_cwd_unavailable");
8181
8344
  }
8182
8345
  let result;
8183
8346
  try {
8184
8347
  result = await (options._runFixedVerification ?? runFixedVerification)(delivery.root, command, frozen, { signal: options.signal, home, path });
8185
8348
  } catch (error) {
8349
+ if (error instanceof VerificationAbortedError)
8350
+ throw error;
8186
8351
  if (error instanceof VerificationLaunchError)
8187
- throw new QaPreparationError("resolution", ids, "process_launch_failed");
8352
+ throw fail("resolution", "process_launch_failed");
8188
8353
  if (error instanceof VerificationCleanupError)
8189
- throw new QaPreparationError("resolution", ids, "process_cleanup_failed");
8190
- throw error;
8354
+ throw fail("resolution", "process_cleanup_failed");
8355
+ throw fail(stage, "execution_failed");
8191
8356
  }
8192
8357
  live();
8193
8358
  clean();
8194
8359
  groupCommands.push(frozen);
8195
8360
  if (frozen?.entry) {
8361
+ const identityDiagnostics = diagnostics("integrity", "command_identity_changed", affected, stage, Math.round(performance.now() - started), result);
8196
8362
  if (!command.executable.startsWith("./"))
8197
- allCommands.push(frozen);
8363
+ allCommands.push({ frozen, diagnostics: identityDiagnostics });
8198
8364
  else if (frozen.interpreter && !frozen.interpreter.path.startsWith(deliveryPrefix))
8199
- allCommands.push({ entry: frozen.interpreter, interpreter: null, interpreter_args: [] });
8365
+ allCommands.push({ frozen: { entry: frozen.interpreter, interpreter: null, interpreter_args: [] }, diagnostics: identityDiagnostics });
8200
8366
  }
8201
8367
  evidence.push({
8202
8368
  stage,
@@ -8209,29 +8375,31 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
8209
8375
  output_limited: result.output_limited
8210
8376
  });
8211
8377
  if (Buffer.byteLength(JSON.stringify(evidence)) > 16384)
8212
- throw new QaPreparationError("prepare", ids, "execution_metadata_limit_exceeded");
8378
+ throw fail("prepare", "execution_metadata_limit_exceeded");
8213
8379
  return result;
8214
8380
  };
8215
8381
  try {
8216
8382
  clean();
8217
8383
  if (environment.prepare) {
8384
+ const prepareStarted = performance.now();
8218
8385
  const result = await execute(environment.prepare, "prepare");
8219
8386
  if (result.exit_code !== 0 || result.timed_out || result.output_limited)
8220
- throw new QaPreparationError("prepare", ids, result.timed_out ? "timeout" : result.output_limited ? "output_limit" : "nonzero_exit");
8387
+ throw new QaPreparationError("prepare", ids, result.timed_out ? "timeout" : result.output_limited ? "output_limit" : "nonzero_exit", diagnostics("prepare", result.timed_out ? "timeout" : result.output_limited ? "output_limit" : "nonzero_exit", group, "prepare", Math.round(performance.now() - prepareStarted), result));
8221
8388
  }
8222
8389
  for (const item of group) {
8223
- const started = Date.now();
8224
- const progress = (phase) => options.onProgress?.({
8390
+ const started = performance.now();
8391
+ const progress = (phase, outcome, result) => options.onProgress?.({
8225
8392
  index: item.index + 1,
8226
8393
  total: snapshot.acceptance.length,
8227
8394
  acceptance_id: item.id,
8228
8395
  phase,
8229
- elapsed_ms: Date.now() - started
8396
+ elapsed_ms: Math.round(performance.now() - started),
8397
+ diagnostic: diagnostics("check", outcome, [item], "check", Math.round(performance.now() - started), result)[0]
8230
8398
  });
8231
- progress("running");
8232
- const result = await execute(item.descriptor.command, "check");
8399
+ progress("running", "running");
8400
+ const result = await execute(item.descriptor.command, "check", [item]);
8233
8401
  const failed = result.exit_code !== 0 || result.timed_out || result.output_limited;
8234
- progress(failed ? "failed" : "passed");
8402
+ progress(failed ? "failed" : "passed", result.timed_out ? "timeout" : result.output_limited ? "output_limit" : failed ? "nonzero_exit" : "passed", result);
8235
8403
  if (failed)
8236
8404
  findings.push({
8237
8405
  id: qaFindingId(item.id, snapshotDigest(snapshot)),
@@ -8241,22 +8409,33 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
8241
8409
  findings_digest: ""
8242
8410
  });
8243
8411
  }
8244
- for (const frozen of groupCommands) {
8245
- if (frozen?.entry)
8246
- assertCommandIdentity(frozen);
8412
+ try {
8413
+ for (const frozen of groupCommands)
8414
+ if (frozen?.entry)
8415
+ assertCommandIdentity(frozen);
8416
+ } catch {
8417
+ throw new QaPreparationError("integrity", ids, "command_identity_changed", diagnostics("integrity", "command_identity_changed"));
8247
8418
  }
8248
8419
  } finally {
8249
8420
  try {
8250
- delivery.cleanup();
8251
- } finally {
8252
- removeTaskOwnedTree(home);
8421
+ try {
8422
+ delivery.cleanup();
8423
+ } finally {
8424
+ removeTaskOwnedTree(home);
8425
+ }
8426
+ } catch {
8427
+ throw new QaPreparationError("integrity", ids, "delivery_cleanup_failed", diagnostics("integrity", "delivery_cleanup_failed"));
8253
8428
  }
8254
8429
  }
8255
8430
  }
8256
8431
  live();
8257
- for (const frozen of allCommands) {
8258
- if (frozen?.entry)
8259
- assertCommandIdentity(frozen);
8432
+ for (const entry of allCommands) {
8433
+ try {
8434
+ if (entry.frozen?.entry)
8435
+ assertCommandIdentity(entry.frozen);
8436
+ } catch {
8437
+ throw new QaPreparationError("integrity", entry.diagnostics.map((item) => item.acceptance_id), "command_identity_changed", entry.diagnostics);
8438
+ }
8260
8439
  }
8261
8440
  const base = {
8262
8441
  contract: "assurance_kernel/assurance_verdict/v2",
@@ -8296,6 +8475,11 @@ var BATCH_REASONS = Object.freeze({
8296
8475
  reason: (detail) => `an active workspace claim already exists for task: ${detail}`,
8297
8476
  recovery_action: "resolve or stop the active task before starting a batch in the current Host"
8298
8477
  },
8478
+ kernel_authority_conflict: {
8479
+ state: "rejected",
8480
+ reason: (detail) => `Kernel authority conflict: ${detail}`,
8481
+ recovery_action: "resolve the Kernel storage conflict before resuming in the current Host"
8482
+ },
8299
8483
  git_head_unreadable: {
8300
8484
  state: "rejected",
8301
8485
  reason: (detail) => detail,
@@ -10509,7 +10693,12 @@ function findExistingActiveBatch(root, initiativeSlug) {
10509
10693
  }
10510
10694
  if (isTerminalBatchState(candidate.batch_state))
10511
10695
  continue;
10512
- return { corrupt: false, record: candidate };
10696
+ try {
10697
+ const record = readBatchRunState(root, file.slice(0, -5));
10698
+ if (record)
10699
+ return { corrupt: false, record };
10700
+ } catch {}
10701
+ return { corrupt: true, path: file };
10513
10702
  }
10514
10703
  return null;
10515
10704
  }
@@ -10635,7 +10824,9 @@ async function projectPlanSurface(input) {
10635
10824
  const riskByTask = new Map;
10636
10825
  let budget;
10637
10826
  if (isResuming && existingBatch) {
10638
- budget = existingBatch.budget;
10827
+ budget = { ...existingBatch.budget };
10828
+ if (Date.parse(budget.deadline_at) <= Date.parse(now))
10829
+ budget.deadline_at = new Date(Date.parse(now) + DEFAULT_DEADLINE_MS).toISOString();
10639
10830
  try {
10640
10831
  recoveryChildren = existingBatch.children.map((c) => {
10641
10832
  const intentPath = `docs/plans/${c.task_id}.intent.json`;
@@ -10738,6 +10929,11 @@ async function projectBatchPreflight(options) {
10738
10929
  const isResuming = activeRecord !== null;
10739
10930
  const batchBranch = `imm/${initiativeSlug}`;
10740
10931
  const activeTaskId = readActiveClaimTaskId(root);
10932
+ if (activeTaskId) {
10933
+ const authority = reconcileKernelAuthority(root, activeTaskId);
10934
+ if (authority.state === "authority_conflict")
10935
+ return reject("kernel_authority_conflict", authority.diagnostic ?? "unverifiable workspace authority");
10936
+ }
10741
10937
  const ownClaim = isResuming && activeTaskId !== null && isOwnBatchClaim(root, activeRecord, activeTaskId, batchBranch);
10742
10938
  if (activeTaskId && !ownClaim)
10743
10939
  return reject("claim_already_active", activeTaskId);
@@ -10851,11 +11047,15 @@ async function authorizeBatch(options) {
10851
11047
  } = projection;
10852
11048
  const existingBatch = projection.existing_batch;
10853
11049
  const isExistingExpired = isResuming && Date.parse(existingBatch.authorization_expires_at) <= Date.now();
10854
- const expiresAt = isResuming && !isExistingExpired && existingBatch.batch_state === "running" ? existingBatch.authorization_expires_at : budget.deadline_at;
11050
+ const branchBefore = spawnSync5("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11051
+ if (branchBefore.status !== 0)
11052
+ return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
10855
11053
  const reuseBlockers = [];
10856
11054
  if (isResuming && existingBatch) {
10857
11055
  if (isExistingExpired)
10858
11056
  reuseBlockers.push("batch_authorization_expired");
11057
+ if (Date.parse(existingBatch.budget.deadline_at) <= Date.now())
11058
+ reuseBlockers.push("batch_budget_expired");
10859
11059
  if (existingBatch.batch_state !== "running")
10860
11060
  reuseBlockers.push("batch_not_running");
10861
11061
  if (existingBatch.plan_digest !== planDigest)
@@ -10866,6 +11066,7 @@ async function authorizeBatch(options) {
10866
11066
  reuseBlockers.push("batch_head_lineage_moved");
10867
11067
  }
10868
11068
  const reuseAuthorization = isResuming && reuseBlockers.length === 0;
11069
+ const expiresAt = reuseAuthorization ? existingBatch.authorization_expires_at : budget.deadline_at;
10869
11070
  const batchId = isResuming && existingBatch ? existingBatch.batch_id : `batch-${initiativeSlug}-${randomUUID7()}`;
10870
11071
  const facts = {
10871
11072
  initiative_slug: initiativeSlug,
@@ -10909,6 +11110,16 @@ async function authorizeBatch(options) {
10909
11110
  const finalOwnClaim = isResuming && finalClaimTaskId !== null && isOwnBatchClaim(root, existingBatch, finalClaimTaskId, batchBranch);
10910
11111
  if (finalClaimTaskId && !finalOwnClaim)
10911
11112
  return { outcome: "rejected", rejection: batchRejection("claim_appeared_during_confirmation", finalClaimTaskId) };
11113
+ if (isResuming) {
11114
+ const current = findExistingActiveBatch(root, initiativeSlug);
11115
+ if (!current || current.corrupt || JSON.stringify(current.record) !== JSON.stringify(existingBatch))
11116
+ return { outcome: "rejected", rejection: batchRejection("plan_changed") };
11117
+ }
11118
+ const branchAfter = spawnSync5("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11119
+ if (branchAfter.status !== 0 || branchAfter.stdout !== branchBefore.stdout)
11120
+ return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "Git branch moved after native confirmation") };
11121
+ if (Date.parse(expiresAt) <= Date.now())
11122
+ return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "confirmed batch deadline expired before authorization issuance") };
10912
11123
  const binding = {
10913
11124
  batch_id: batchId,
10914
11125
  initiative_slug: initiativeSlug,
@@ -11474,6 +11685,14 @@ async function lookupBatchCommit(input) {
11474
11685
  }
11475
11686
 
11476
11687
  // plugins/immune-brain/runtime/unattended/batch_runner.ts
11688
+ function batchQaFailureFacts(record) {
11689
+ const failed = record.history.filter((event) => event.type === "request_rework" && event.authority?.authority_kind === "qa");
11690
+ return {
11691
+ qa_failure_count: failed.length,
11692
+ last_qa_failure_at: failed.at(-1)?.at ?? null,
11693
+ recovery_findings: record.findings.filter((f) => f.status === "open").map((f) => ({ id: f.id, kind: f.kind, status: f.status, acceptance_id: f.acceptance_id }))
11694
+ };
11695
+ }
11477
11696
  function dependentsOf(record, taskId) {
11478
11697
  return record.children.filter((child) => child.blocked_by.includes(taskId));
11479
11698
  }
@@ -11533,8 +11752,23 @@ function reportFor(record, reason, nextAction) {
11533
11752
  created_at: record.updated_at
11534
11753
  };
11535
11754
  }
11536
- function finalize(root, record, reason, nextAction) {
11537
- const report = reportFor(record, reason, nextAction);
11755
+ function executorHandoff(record, taskId, fresh, failure = {}) {
11756
+ const recovery = deriveAssuranceRecovery(taskId, fresh, failure, fresh.recovery_findings);
11757
+ return {
11758
+ ...reportFor(record, null, recovery?.next_action ?? (fresh.projection.artifact_state === "frozen" ? "The child is frozen with pending QA. Call advance_assurance once; follow its diagnostics before retrying, then submit any reserved Review before batch continuation." : "Route the enrolled child to foreground Executor, implement and stage its scoped work, then call Kernel advance_assurance. Submit any reserved Review verdict before continuing start_unattended_batch with the same Initiative.")),
11759
+ ...recovery ? { recovery } : {},
11760
+ ...failure.diagnostics?.length ? { diagnostics: failure.diagnostics } : {},
11761
+ handoff: recovery?.category === "authorization" ? undefined : {
11762
+ role: "executor",
11763
+ task_id: taskId,
11764
+ run_id: fresh.projection.run_id ?? null,
11765
+ record_revision: fresh.projection.record_revision,
11766
+ next_obligation: fresh.projection.next_obligation
11767
+ }
11768
+ };
11769
+ }
11770
+ function finalize(root, record, reason, nextAction, details = {}) {
11771
+ const report = { ...reportFor(record, reason, nextAction), ...details };
11538
11772
  if (isTerminalBatchState(record.batch_state) || record.batch_state === "needs_human") {
11539
11773
  writeBatchRunReport(root, report);
11540
11774
  }
@@ -11724,7 +11958,6 @@ async function startBatchLocked(input) {
11724
11958
  authorization_expires_at: input.authorization_expires_at,
11725
11959
  budget: input.budget,
11726
11960
  batch_state: "running",
11727
- consecutive_qa_failures: 0,
11728
11961
  children: resumedChildren
11729
11962
  });
11730
11963
  }
@@ -11882,67 +12115,8 @@ async function startBatchLocked(input) {
11882
12115
  break;
11883
12116
  }
11884
12117
  }
11885
- let childTerminal = false;
11886
- while (!childTerminal) {
11887
- const terminal = await input.kernel.advanceTask(input.root, child.task_id);
11888
- if (terminal.state === "completed") {
11889
- childTerminal = true;
11890
- record.consecutive_qa_failures = 0;
11891
- record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null } : c);
11892
- persist();
11893
- try {
11894
- const planChild = input.children.find((c) => c.task_id === child.task_id);
11895
- const intentPath = planChild?.intent_path ?? undefined;
11896
- const { commit } = input.git?.commitChild ? await input.git.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath) : input.kernel.commitChild ? await input.kernel.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath) : await commitBatchChild({
11897
- root: input.root,
11898
- taskId: child.task_id,
11899
- batchId: input.batch_id,
11900
- expectedHead: head,
11901
- branch: record.branch,
11902
- intentPath
11903
- });
11904
- record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "committed", commit } : c);
11905
- record.commits.push(commit);
11906
- head = commit;
11907
- persist();
11908
- } catch (error) {
11909
- const message = error instanceof Error ? error.message : String(error);
11910
- record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
11911
- skipDependents(record, child.task_id, `dependency ${child.task_id} failed to commit`);
11912
- record.batch_state = "failed";
11913
- persist();
11914
- break;
11915
- }
11916
- } else if (terminal.state === "stopped") {
11917
- record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: "Kernel reported the child stopped" } : c);
11918
- childTerminal = true;
11919
- skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
11920
- record.batch_state = "needs_human";
11921
- persist();
11922
- } else if (terminal.state === "rework" && terminal.operation === "qa") {
11923
- record.consecutive_qa_failures += 1;
11924
- if (record.consecutive_qa_failures >= record.budget.qa_failure_limit) {
11925
- record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: `QA failure limit reached: ${terminal.summary}` } : c);
11926
- record.batch_state = "needs_human";
11927
- childTerminal = true;
11928
- skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
11929
- persist();
11930
- } else {
11931
- persist();
11932
- }
11933
- } else if (terminal.state === "failed" || terminal.state === "blocked" || terminal.state === "rework") {
11934
- const reason = terminal.state === "rework" ? terminal.summary : terminal.reason;
11935
- childTerminal = true;
11936
- record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
11937
- skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
11938
- record.batch_state = "needs_human";
11939
- persist();
11940
- } else if (terminal.state === "review_ready") {
11941
- record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "enrolled", reason: `review reservation ${terminal.operation_id} open` } : c);
11942
- persist();
11943
- return reportFor(record, `child ${child.task_id} holds an open Review reservation`, "Submit the reserved foreground Review verdict, then call startBatch again to continue.");
11944
- }
11945
- }
12118
+ const enrolled = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
12119
+ return executorHandoff(record, child.task_id, enrolled);
11946
12120
  }
11947
12121
  return finalize(input.root, record, stopReasonFor(record), "");
11948
12122
  }
@@ -12001,11 +12175,30 @@ async function resumeBatch(input, projection) {
12001
12175
  });
12002
12176
  return finalize(input.root, existing, "claim held by another batch", "needs-human-attention");
12003
12177
  }
12178
+ if (holdsClaim) {
12179
+ const failures = Math.max(existing.consecutive_qa_failures, fresh.qa_failure_count ?? 0);
12180
+ if (failures >= existing.budget.qa_failure_limit && fresh.last_qa_failure_at && Date.parse(fresh.last_qa_failure_at) > Date.parse(existing.confirmation_time)) {
12181
+ const reason = "QA failure limit reached; repair and settle the own child through Kernel before resuming the batch";
12182
+ skipDependents(existing, driven.task_id, `dependency ${driven.task_id} parked`);
12183
+ existing = writeBatchRunState(input.root, {
12184
+ ...existing,
12185
+ batch_state: "needs_human",
12186
+ consecutive_qa_failures: failures,
12187
+ children: existing.children.map((c) => c.task_id === driven.task_id ? { ...c, state: "needs_human", reason } : c)
12188
+ });
12189
+ return finalize(input.root, existing, reason, "Repair and settle the own child through Kernel, then re-confirm the batch.", { recovery: deriveAssuranceRecovery(driven.task_id, fresh, {}, fresh.recovery_findings) ?? undefined });
12190
+ }
12191
+ if (failures !== existing.consecutive_qa_failures)
12192
+ existing = writeBatchRunState(input.root, { ...existing, consecutive_qa_failures: failures });
12193
+ if (fresh.projection.next_obligation === "run_qa" || fresh.projection.artifact_state !== "frozen")
12194
+ return executorHandoff(existing, driven.task_id, fresh);
12195
+ }
12004
12196
  if (!holdsClaim) {
12005
12197
  const settledRemotely = fresh.projection.lifecycle === "done" && fresh.projection.completion_ready === true;
12006
12198
  if (settledRemotely) {
12007
12199
  existing = writeBatchRunState(input.root, {
12008
12200
  ...existing,
12201
+ consecutive_qa_failures: 0,
12009
12202
  children: existing.children.map((c) => c.task_id === driven.task_id ? { ...c, state: "settled", reason: "crash after settlement; resuming at commit" } : c)
12010
12203
  });
12011
12204
  } else {
@@ -12108,30 +12301,8 @@ async function driveInterruptedChild(input, child) {
12108
12301
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null } : c);
12109
12302
  record.consecutive_qa_failures = 0;
12110
12303
  persist();
12111
- try {
12112
- const planChild = input.children.find((c) => c.task_id === child.task_id);
12113
- const intentPath = planChild?.intent_path ?? undefined;
12114
- const { commit } = input.git?.commitChild ? await input.git.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath) : input.kernel.commitChild ? await input.kernel.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath) : await commitBatchChild({
12115
- root: input.root,
12116
- taskId: child.task_id,
12117
- batchId: input.batch_id,
12118
- expectedHead: head,
12119
- branch: record.branch,
12120
- intentPath
12121
- });
12122
- record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "committed", commit } : c);
12123
- record.commits.push(commit);
12124
- head = commit;
12125
- persist();
12126
- child = { ...child, state: "committed", commit };
12127
- } catch (error) {
12128
- const message = error instanceof Error ? error.message : String(error);
12129
- record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
12130
- skipDependents(record, child.task_id, `dependency ${child.task_id} failed to commit`);
12131
- record.batch_state = "failed";
12132
- persist();
12133
- return finalize(input.root, record, message, "");
12134
- }
12304
+ child = { ...child, state: "settled", reason: null };
12305
+ continue;
12135
12306
  } else if (terminal.state === "review_ready") {
12136
12307
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, reason: `review reservation ${terminal.operation_id} open` } : c);
12137
12308
  persist();
@@ -12139,16 +12310,22 @@ async function driveInterruptedChild(input, child) {
12139
12310
  } else if (terminal.state === "rework" && terminal.operation === "qa") {
12140
12311
  record.consecutive_qa_failures += 1;
12141
12312
  if (record.consecutive_qa_failures >= record.budget.qa_failure_limit) {
12142
- const reason = `QA failure limit reached: ${terminal.summary}`;
12313
+ const reason = "QA failure limit reached";
12143
12314
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
12144
12315
  record.batch_state = "needs_human";
12145
12316
  skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
12146
12317
  persist();
12147
- return finalize(input.root, record, reason, "");
12318
+ const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
12319
+ return finalize(input.root, record, reason, "Repair and settle the own child through Kernel, then re-confirm the batch.", { recovery: deriveAssuranceRecovery(child.task_id, fresh, terminal, fresh.recovery_findings) ?? undefined, diagnostics: terminal.diagnostics });
12148
12320
  }
12149
12321
  persist();
12322
+ const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
12323
+ return executorHandoff(record, child.task_id, fresh, terminal);
12324
+ } else if (terminal.state === "rework" || terminal.environment_failure || terminal.state === "review_preparation_failed" || terminal.recovery?.category === "repair") {
12325
+ const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
12326
+ return executorHandoff(record, child.task_id, fresh, terminal);
12150
12327
  } else {
12151
- const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.state === "rework" ? terminal.summary : terminal.reason;
12328
+ const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.reason;
12152
12329
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
12153
12330
  record.batch_state = "needs_human";
12154
12331
  skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
@@ -12564,6 +12741,12 @@ class ClaudeRuntime {
12564
12741
  return { ...result, tracker: TRACKER_PROJECTION_FAILURE };
12565
12742
  }
12566
12743
  }
12744
+ async reviseIntent(taskId, nextIntent) {
12745
+ return this.executeOrdinary({ cwd: this.cwd }, {
12746
+ taskId,
12747
+ operation: { op: "revise_intent", next_intent: nextIntent, actor_id: "executor" }
12748
+ });
12749
+ }
12567
12750
  async resolveFinding(taskId, findingId) {
12568
12751
  return this.executeOrdinary({ cwd: this.cwd }, {
12569
12752
  taskId,
@@ -12849,7 +13032,7 @@ class ClaudeRuntime {
12849
13032
  throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
12850
13033
  if (!this.requestConfirmation)
12851
13034
  throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
12852
- const now = new Date().toISOString();
13035
+ let now = new Date().toISOString();
12853
13036
  const preflight = await projectBatchPreflight({
12854
13037
  root: this.cwd,
12855
13038
  initiative_slug: initiativeSlug,
@@ -12963,6 +13146,9 @@ class ClaudeRuntime {
12963
13146
  const { binding, batch_id: batchId, expires_at: expiresAt } = authorization;
12964
13147
  if (meta.signal?.aborted)
12965
13148
  return batchReason("cancelled_before_execution");
13149
+ now = new Date().toISOString();
13150
+ if (Date.parse(expiresAt) <= Date.parse(now))
13151
+ return batchReason("confirmation_failed", "confirmed batch deadline expired before authorization issuance");
12966
13152
  const capability = this.batchRegistry.issue(binding, recoveryChildren, now);
12967
13153
  const basePort = this.createBatchKernelPort(this.batchRegistry, capability, binding);
12968
13154
  const kernelPort = {
@@ -13035,19 +13221,30 @@ class ClaudeRuntime {
13035
13221
  },
13036
13222
  advanceTask: async (root, taskId) => {
13037
13223
  const result = await this.coordinator.advance(taskId, { cwd: root });
13224
+ const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
13038
13225
  if (result.state === "completed")
13039
- return { state: "completed" };
13226
+ return { state: "completed", ...facts };
13040
13227
  if (result.state === "stopped")
13041
- return { state: "stopped" };
13228
+ return { state: "stopped", ...facts };
13042
13229
  if (result.state === "rework")
13043
- return { state: "rework", operation: result.operation, summary: result.summary };
13230
+ return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
13044
13231
  if (result.state === "review_ready")
13045
- return { state: "review_ready", operation_id: result.operation_id };
13232
+ return { state: "review_ready", operation_id: result.operation_id, ...facts };
13233
+ if (result.state === "review_preparation_failed")
13234
+ return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
13046
13235
  if (result.state === "blocked")
13047
- return { state: "blocked", reason: result.reason };
13048
- return { state: "failed", reason: result.reason ?? "advance failed" };
13236
+ return { state: "blocked", reason: result.reason, ...facts };
13237
+ return { state: "failed", reason: result.reason ?? "advance failed", ...facts };
13238
+ },
13239
+ projectTask: async (root, taskId) => {
13240
+ const fresh = await projectAssurance(root, taskId, diffSnapshotOf);
13241
+ if (!fresh.claim || fresh.projection.lifecycle !== "active" || fresh.error !== null)
13242
+ return fresh;
13243
+ const { record, revision } = readTaskRecordRaw(root, taskId);
13244
+ if (!record || revision !== fresh.projection.record_revision)
13245
+ throw new Error(`Kernel recovery projection changed for ${taskId}`);
13246
+ return { ...fresh, ...batchQaFailureFacts(record) };
13049
13247
  },
13050
- projectTask: async (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
13051
13248
  ownsTaskClaim: (taskId) => {
13052
13249
  return registry.isChildConsumed(capability, taskId);
13053
13250
  },
@@ -13072,6 +13269,7 @@ var TOOLS = [
13072
13269
  { name: "advance_assurance", description: "Advance frozen Assurance through deterministic QA and Review reservation.", privileged: false },
13073
13270
  { name: "submit_review", description: "Submit the Parent-mediated Review verdict bound to the correlated receipt.", privileged: false },
13074
13271
  { name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
13272
+ { name: "revise_intent", description: "Apply a compatible TaskIntent revision.", privileged: false },
13075
13273
  { name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
13076
13274
  { name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
13077
13275
  { name: "start_unattended_batch", description: "Start an unattended serial batch run for an Initiative after native confirmation.", privileged: true },
@@ -13088,14 +13286,14 @@ function listMcpTools() {
13088
13286
  properties: {
13089
13287
  ...tool.name === "start_unattended_batch" ? { initiative_slug: { type: "string" } } : {
13090
13288
  task_id: { type: "string" },
13091
- ...tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {},
13289
+ ...tool.name === "approve_breaking_intent_revision" || tool.name === "revise_intent" ? { next_intent: { type: "object" } } : {},
13092
13290
  ...tool.name === "stop" ? { reason: { type: "string" } } : {},
13093
13291
  ...tool.name === "submit_review" ? { verdict: { type: "object" } } : {},
13094
13292
  ...tool.name === "resolve_finding" ? { finding_id: { type: "string" } } : {},
13095
13293
  ...tool.name === "refute_finding" ? { finding_id: { type: "string" }, attestation_id: { type: "string" } } : {}
13096
13294
  }
13097
13295
  },
13098
- required: tool.name === "start_unattended_batch" ? ["initiative_slug"] : tool.name === "submit_review" ? ["task_id", "verdict"] : tool.name === "resolve_finding" ? ["task_id", "finding_id"] : tool.name === "refute_finding" ? ["task_id", "finding_id", "attestation_id"] : ["task_id"]
13296
+ required: tool.name === "start_unattended_batch" ? ["initiative_slug"] : tool.name === "submit_review" ? ["task_id", "verdict"] : tool.name === "resolve_finding" ? ["task_id", "finding_id"] : tool.name === "refute_finding" ? ["task_id", "finding_id", "attestation_id"] : tool.name === "revise_intent" ? ["task_id", "next_intent"] : ["task_id"]
13099
13297
  },
13100
13298
  annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" }
13101
13299
  }));
@@ -13189,6 +13387,11 @@ function createMcpRuntime(options = {}) {
13189
13387
  throw new Error("verdict is required");
13190
13388
  return runtime.submitReview(taskId, args.verdict);
13191
13389
  }
13390
+ if (name === "revise_intent") {
13391
+ if (!Object.hasOwn(args, "next_intent"))
13392
+ throw new Error("next_intent is required");
13393
+ return runtime.reviseIntent(taskId, args.next_intent);
13394
+ }
13192
13395
  if (name === "resolve_finding") {
13193
13396
  if (typeof args.finding_id !== "string" || !args.finding_id)
13194
13397
  throw new Error("finding_id is required");