@kylecheng3146/agent-ops 0.10.0 → 0.11.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 (76) hide show
  1. package/README.md +3 -2
  2. package/dist/packages/cli/src/args.js +18 -53
  3. package/dist/packages/cli/src/bin.js +28 -29
  4. package/dist/packages/cli/src/cli.js +25 -17
  5. package/dist/packages/cli/src/codex-loop-process.js +0 -6
  6. package/dist/packages/cli/src/commands/advance.js +2 -1
  7. package/dist/packages/cli/src/commands/parallel.js +1 -1
  8. package/dist/packages/cli/src/commands/review.js +8 -11
  9. package/dist/packages/cli/src/commands/run.js +12 -3
  10. package/dist/packages/cli/src/commands/task.js +6 -50
  11. package/dist/packages/cli/src/commands/verify.js +7 -3
  12. package/dist/packages/cli/src/context.js +22 -4
  13. package/dist/packages/cli/src/default-base.js +26 -0
  14. package/dist/packages/cli/src/hook-process.js +19 -83
  15. package/dist/packages/cli/src/office-entry.js +8 -4
  16. package/dist/packages/cli/src/owned-run-step.js +4 -4
  17. package/dist/packages/cli/src/parallel-deps.js +0 -2
  18. package/dist/packages/cli/src/run-deps.js +12 -9
  19. package/dist/packages/cli/src/run-entry.js +123 -19
  20. package/dist/packages/cli/src/run-setup-entry.js +2 -2
  21. package/dist/runtime/src/adapters/agy/config.js +2 -3
  22. package/dist/runtime/src/adapters/claude/config.js +2 -2
  23. package/dist/runtime/src/adapters/codex/config.js +2 -5
  24. package/dist/runtime/src/hooks/completion-gate.js +0 -15
  25. package/dist/runtime/src/install/harness.js +7 -4
  26. package/dist/runtime/src/install/plan.js +3 -3
  27. package/dist/runtime/src/install/profiles.js +0 -2
  28. package/dist/runtime/src/office/scene.js +1 -1
  29. package/dist/runtime/src/office/server.js +43 -16
  30. package/dist/runtime/src/office/snapshot.js +5 -2
  31. package/dist/runtime/src/parallel/finish.js +139 -65
  32. package/dist/runtime/src/parallel/manage.js +0 -3
  33. package/dist/runtime/src/parallel/service.js +5 -1
  34. package/dist/runtime/src/review/execute.js +77 -11
  35. package/dist/runtime/src/review/packet.js +17 -6
  36. package/dist/runtime/src/review/runner.js +2 -0
  37. package/dist/runtime/src/run/controls.js +99 -1
  38. package/dist/runtime/src/run/hosts/codex.js +28 -1
  39. package/dist/runtime/src/run/hosts/protocol.js +5 -2
  40. package/dist/runtime/src/run/integration.js +35 -13
  41. package/dist/runtime/src/run/policy.js +65 -10
  42. package/dist/runtime/src/run/scheduler.js +10 -5
  43. package/dist/runtime/src/run/service.js +68 -21
  44. package/dist/runtime/src/run/supervisor.js +11 -2
  45. package/dist/runtime/src/run/transport.js +1 -0
  46. package/dist/runtime/src/run/verification.js +2 -2
  47. package/dist/runtime/src/schema/validate.js +10 -5
  48. package/dist/runtime/src/security/permissions.js +85 -0
  49. package/dist/runtime/src/task/completion.js +4 -42
  50. package/dist/runtime/src/task/contract.js +11 -0
  51. package/dist/runtime/src/task/service.js +34 -57
  52. package/dist/runtime/src/task/store.js +3 -7
  53. package/dist/runtime/src/verify/acceptance-coverage.js +2 -3
  54. package/dist/runtime/src/verify/acceptance-protocol.js +4 -3
  55. package/dist/runtime/src/verify/acceptance-replay.js +4 -7
  56. package/dist/runtime/src/verify/service.js +6 -23
  57. package/dist/runtime/src/verify/source-fingerprint.js +17 -4
  58. package/docs/en/guides/configuration.md +8 -4
  59. package/docs/en/guides/task-debug.md +39 -121
  60. package/docs/en/spec/acceptance-and-evidence.md +31 -19
  61. package/docs/en/spec/harness-adapters.md +35 -12
  62. package/docs/en/spec/loop-engineering.md +40 -6
  63. package/docs/en/spec/review.md +6 -0
  64. package/docs/zh-TW/guides/configuration.md +6 -3
  65. package/docs/zh-TW/guides/task-debug.md +27 -66
  66. package/docs/zh-TW/spec/acceptance-and-evidence.md +19 -18
  67. package/docs/zh-TW/spec/harness-adapters.md +28 -9
  68. package/docs/zh-TW/spec/loop-engineering.md +14 -3
  69. package/docs/zh-TW/spec/review.md +5 -0
  70. package/package.json +1 -1
  71. package/schemas/config.schema.json +1 -21
  72. package/dist/packages/cli/src/debug-intake.js +0 -245
  73. package/dist/runtime/src/task/debug-intake.js +0 -15
  74. package/dist/runtime/src/task/debug-state.js +0 -129
  75. package/dist/runtime/src/task/debug-user-event.js +0 -95
  76. package/dist/runtime/src/task/debug.js +0 -612
@@ -1,4 +1,3 @@
1
- import { validDebugSettings, validDebugIntake } from "../task/debug-state.js";
2
1
  import { CONFIG_SCHEMA_VERSION, EVIDENCE_SCHEMA_VERSION, MANIFEST_SCHEMA_VERSION, TASK_SCHEMA_VERSION } from "../contracts.js";
3
2
  import { acceptanceError, runnerError, commitIdentity } from "./acceptance.js";
4
3
  import { parseSurface } from "../surface/schema.js";
@@ -339,8 +338,9 @@ export function validateConfig(value) {
339
338
  if (featuresUnknown !== undefined) {
340
339
  return featuresUnknown;
341
340
  }
342
- if (root.features.debug !== undefined && !validDebugSettings(root.features.debug))
343
- return failure("INVALID_FEATURE", "$.features.debug", "Debug requires enabled, bounded budgetMs and maxRounds.");
341
+ // Pre-0.11 DEBUG budget settings: still accepted, now ignored.
342
+ if (root.features.debug !== undefined && !isRecord(root.features.debug))
343
+ return failure("INVALID_FEATURE", "$.features.debug", "features.debug must be an object.");
344
344
  if (root.features.office !== undefined) {
345
345
  if (!isRecord(root.features.office)) {
346
346
  return failure("INVALID_TYPE", "$.features.office", "office must be an object.");
@@ -571,12 +571,17 @@ export function validateCriterion(value, path) {
571
571
  }
572
572
  return success(value);
573
573
  }
574
+ /** The DEBUG tag a task keeps: its root and the issue's recorded facts and unknowns. */
575
+ function validDebugTag(value) {
576
+ return isRecord(value) && Object.keys(value).every(k => ["rootTaskId", "facts", "unknowns"].includes(k)) &&
577
+ typeof value.rootTaskId === "string" && value.rootTaskId.length > 0 && isStringArray(value.facts) && isStringArray(value.unknowns);
578
+ }
574
579
  export function validateTask(value) {
575
580
  const root = validateRoot(value, ["criteria", "id", "intent", "goal", "contractRevision", "parentTaskId", "schemaVersion", "title", "debug", "surface", "surfaceRequired", "origin", "originRequired"], isRecord(value) && value.schemaVersion === 1 ? 1 : TASK_SCHEMA_VERSION);
576
581
  if (isFailure(root)) {
577
582
  return root;
578
583
  }
579
- if (root.debug !== undefined && (!validDebugIntake(root.debug) || root.schemaVersion !== TASK_SCHEMA_VERSION || root.goal === undefined))
584
+ if (root.debug !== undefined && (!validDebugTag(root.debug) || root.schemaVersion !== TASK_SCHEMA_VERSION || root.goal === undefined))
580
585
  return failure("INVALID_DEBUG_TASK", "$.debug", "Debug tasks require immutable goal and bounded root intake.");
581
586
  if (root.surfaceRequired !== undefined && root.surfaceRequired !== true)
582
587
  return failure("INVALID_SURFACE", "$.surfaceRequired", "surfaceRequired is either absent or true.");
@@ -591,7 +596,7 @@ export function validateTask(value) {
591
596
  if (root.origin !== undefined && !validOrigin(root.origin))
592
597
  return failure("INVALID_ORIGIN", "$.origin", "origin is escape-of {commit, label, receipt?} or pre-existing/external {reason}.");
593
598
  if (root.originRequired !== undefined && (root.originRequired !== true || root.debug === undefined))
594
- return failure("INVALID_ORIGIN", "$.originRequired", "originRequired is either absent or true on a DEBUG task.");
599
+ return failure("INVALID_ORIGIN", "$.originRequired", "originRequired (pre-0.11, now ignored) is either absent or true on a DEBUG task.");
595
600
  if (root.contractRevision !== undefined && (!Number.isSafeInteger(root.contractRevision) || root.contractRevision < 0))
596
601
  return failure("INVALID_CONTRACT_REVISION", "$.contractRevision", "Invalid contract revision.");
597
602
  if (root.goal !== undefined && (!isNonEmptyString(root.goal) || Buffer.byteLength(root.goal, "utf8") > 65536))
@@ -37,6 +37,7 @@ const execFile = promisify(execFileCallback);
37
37
  const lockQueues = new Map();
38
38
  const heartbeatFailures = new Map();
39
39
  const heartbeatPaths = new Set();
40
+ const heartbeatReleases = new Map();
40
41
  // A heartbeat lives as long as the process. Its handle is held here, not only
41
42
  // by the interval below, so a reset or mocked timer can never let garbage
42
43
  // collection close it (an uncaught error since Node 26).
@@ -172,6 +173,14 @@ async function ensureProcessHeartbeat(parent, anchorDirectory) {
172
173
  });
173
174
  });
174
175
  }, HEARTBEAT_INTERVAL_MS);
176
+ heartbeatReleases.set(heartbeatPath, async () => {
177
+ clearInterval(timer);
178
+ await handle.close();
179
+ heartbeatHandles.delete(handle);
180
+ heartbeatPaths.delete(heartbeatPath);
181
+ processHeartbeats.delete(cacheKey);
182
+ heartbeatReleases.delete(heartbeatPath);
183
+ });
175
184
  timer.unref();
176
185
  return heartbeat;
177
186
  })();
@@ -184,6 +193,82 @@ async function ensureProcessHeartbeat(parent, anchorDirectory) {
184
193
  throw error;
185
194
  }
186
195
  }
196
+ async function fileIdentity(path) {
197
+ try {
198
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
199
+ try {
200
+ const { dev, ino } = await handle.stat({ bigint: true });
201
+ return { dev, ino };
202
+ }
203
+ finally {
204
+ await handle.close();
205
+ }
206
+ }
207
+ catch {
208
+ return undefined;
209
+ }
210
+ }
211
+ /** Retire authenticated runtime markers without weakening Git's dirty-file guard. */
212
+ export async function cleanupWorktreeHeartbeats(root, paths) {
213
+ const candidates = [];
214
+ for (const relativePath of paths) {
215
+ const match = /^\.(?:claude|codex)\/\.agent-ops-process-([1-9]\d*)-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/u.exec(relativePath);
216
+ if (match === null)
217
+ continue;
218
+ const path = join(root, relativePath), pid = Number(match[1]);
219
+ const unsafe = () => new AgentOpsError("WORKTREE_HEARTBEAT_UNSAFE", `Preserve unverified heartbeat ${relativePath} (PID ${pid}).`);
220
+ const blocked = () => new AgentOpsError("WORKTREE_HEARTBEAT_ACTIVE", `Preserve heartbeat ${relativePath}: PID ${pid} is active or its death cannot be confirmed.`);
221
+ if (!Number.isSafeInteger(pid) || pid > 2147483647)
222
+ throw unsafe();
223
+ const parent = await lstat(dirname(path));
224
+ if (parent.isSymbolicLink() || !parent.isDirectory())
225
+ throw unsafe();
226
+ let handle;
227
+ try {
228
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
229
+ const status = await handle.stat({ bigint: true });
230
+ if (!status.isFile() || status.nlink !== 1n || status.size > 128n ||
231
+ await handle.readFile("utf8") !== `runtime:${match[2]}\n`)
232
+ throw unsafe();
233
+ const own = pid === process.pid && match[2] === PROCESS_INSTANCE_TOKEN && heartbeatReleases.has(path);
234
+ if (own) {
235
+ if ([...lockQueues.keys()].some(key => key.startsWith(`${resolve(root)}\0`)))
236
+ throw blocked();
237
+ }
238
+ else if (isProcessActive(pid))
239
+ throw blocked();
240
+ candidates.push({ path, relative: relativePath, pid, own, dev: status.dev, ino: status.ino });
241
+ }
242
+ catch (error) {
243
+ if (isMissing(error))
244
+ continue;
245
+ if (error instanceof AgentOpsError)
246
+ throw error;
247
+ throw unsafe();
248
+ }
249
+ finally {
250
+ await handle?.close();
251
+ }
252
+ }
253
+ // Validate the complete set before removing anything; foreign owners fail closed.
254
+ for (const candidate of candidates) {
255
+ const status = await lstat(candidate.path, { bigint: true });
256
+ // Identity is compared handle to handle, as it was captured: on Windows, Node 22.14.0
257
+ // reports different dev/ino for lstat and fstat of the same file.
258
+ const identity = await fileIdentity(candidate.path);
259
+ if (!status.isFile() || status.isSymbolicLink() || identity === undefined || identity.dev !== candidate.dev || identity.ino !== candidate.ino)
260
+ throw new AgentOpsError("WORKTREE_HEARTBEAT_UNSAFE", `Heartbeat changed before cleanup: ${candidate.relative}.`);
261
+ if (!candidate.own && isProcessActive(candidate.pid))
262
+ throw new AgentOpsError("WORKTREE_HEARTBEAT_ACTIVE", `Preserve heartbeat ${candidate.relative}: PID ${candidate.pid} is active or its death cannot be confirmed.`);
263
+ if (candidate.own) {
264
+ const release = heartbeatReleases.get(candidate.path);
265
+ if (release === undefined || [...lockQueues.keys()].some(key => key.startsWith(`${resolve(root)}\0`)))
266
+ throw new AgentOpsError("WORKTREE_HEARTBEAT_ACTIVE", `Current-process locks still protect ${candidate.relative} (PID ${candidate.pid}).`);
267
+ await release();
268
+ }
269
+ await rm(candidate.path);
270
+ }
271
+ }
187
272
  async function assertProcessHeartbeat(parent, heartbeat) {
188
273
  const heartbeatPath = join(parent, heartbeat.file);
189
274
  try {
@@ -2,52 +2,14 @@ import { calculateConfigHash } from "../config/hash.js";
2
2
  import { findReviewAttestation } from "../review/attestation.js";
3
3
  import { validateEvidence, validateTaskAgainstConfig } from "../schema/validate.js";
4
4
  import { isPassingVerificationEvidence } from "../verify/evidence.js";
5
- import { taskContractHash } from "./contract.js";
6
- import { acceptanceCoverage, coverageDigest } from "../verify/acceptance-coverage.js";
7
- import { loadDebugProof, debugProofDigest, debugRemainingMs } from "./debug-state.js";
8
- import { escapeCompletionProblem } from "../escape/link.js";
9
- import { RULES_DIRECTORY } from "../escape/rules.js";
5
+ import { evidenceMatchesContract, taskContractHash } from "./contract.js";
6
+ import { acceptanceCoverage } from "../verify/acceptance-coverage.js";
10
7
  export async function checkTaskCompletionEvidence(stored, options) {
11
- const debug = stored.task.debug === undefined ? undefined : await loadDebugProof(options.root, stored.task.debug.rootTaskId);
12
- if (debug?.control?.paused === true || debug?.control?.mode === "diagnose")
13
- return { status: "FAIL", code: "TASK_DEBUG_SCOPE_RESTRICTED", remedy: "The human restricted this issue to pause or diagnosis. Preserve the active task; only a native human resume repair event authorizes repair completion." };
14
8
  const problem = await checkTaskVerificationEvidence(stored, options);
15
9
  if (problem !== null)
16
10
  return problem;
17
11
  const attestation = await findReviewAttestation(options.root, options.sourceFingerprint, stored.task.id);
18
- if (debug !== undefined) {
19
- const coverage = await acceptanceCoverage(stored, options.config, options.sourceFingerprint, options.evidenceStore);
20
- const { FileTaskStore } = await import("./store.js");
21
- const { join } = await import("node:path");
22
- const reviewedRecords = (await new FileTaskStore(join(options.root, ".agent-ops/tasks/state.json"), options.root).read()).tasks.filter(r => attestation?.taskContracts?.[r.task.id] !== undefined);
23
- const currentCoverage = [];
24
- for (const record of reviewedRecords) {
25
- const rows = await acceptanceCoverage(record, options.config, options.sourceFingerprint, options.evidenceStore);
26
- currentCoverage.push(...rows.map(row => ({ ...row, criterionId: attestation?.tree === undefined ? row.criterionId : record.task.id + ":" + row.criterionId })));
27
- }
28
- const refs = new Set(coverage.filter(c => c.mode === "behavioral" && c.status === "proven").flatMap(c => c.evidenceReferences));
29
- if (attestation?.coverageDigest !== coverageDigest(currentCoverage) || debug.operation !== null || debugRemainingMs(debug) === 0 || !debug.hypotheses.some(h => h.status === "confirmed" && h.supporting.some(ref => refs.has(ref))) ||
30
- attestation?.debugProofs?.[stored.task.id] !== debugProofDigest(debug))
31
- return { status: "FAIL", code: "TASK_DEBUG_PROOF_REQUIRED", remedy: "Confirm the original cause with current discriminating regression proof, then obtain two fresh independent reviews bound to that debug proof." };
32
- }
33
- // Fails closed: the Escape Loop needs every DEBUG fix to say where the bug came from.
34
- if (stored.task.originRequired === true && stored.task.origin === undefined)
35
- return { status: "FAIL", code: "ESCAPE_ORIGIN_MISSING", remedy: "Declare the issue origin with task debug --debug-action origin --payload-file <json> (escape-of <commit> + label, pre-existing or external + reason) after the hypothesis is confirmed, then re-verify, re-record the hypothesis and obtain review." };
36
- // Escape Loop R2: a declared escape finishes only with its proof (rule replay + backtest, or a promoted red-to-green check).
37
- const origin = stored.task.origin, base = debug?.baselineCommit ?? stored.createdSourceCommit;
38
- if (origin?.kind === "escape-of" && base !== undefined) {
39
- const escape = await escapeCompletionProblem({ root: options.root, taskId: stored.task.id, origin, base });
40
- if (escape !== null && "code" in escape)
41
- return { status: "FAIL", code: escape.code, remedy: `${escape.reason} Add or fix a tracked ${RULES_DIRECTORY}/<id>.json rule, re-declare the origin and commit the record.` };
42
- if (escape !== null) {
43
- const [runnerId, checkId] = escape.check.split(":");
44
- const proven = new Set((await acceptanceCoverage(stored, options.config, options.sourceFingerprint, options.evidenceStore))
45
- .filter(row => row.status === "proven").map(row => row.criterionId));
46
- if (!stored.task.criteria.some(c => proven.has(c.id) && c.acceptance?.mode === "behavioral" &&
47
- c.acceptance.bindings.some(b => b.runnerId === runnerId && (b.redCheckIds ?? []).includes(checkId))))
48
- return { status: "FAIL", code: "ESCAPE_RULE_UNPROVEN", remedy: `The promoted check ${escape.check} must be a redCheckId of a proven behavioral criterion of this task (red baseline, green candidate).` };
49
- }
50
- }
12
+ // Escape origin and its rule proof are advisory: task create/revise --escape-of report them, completion does not wait on them.
51
13
  if (attestation === null || attestation.taskId !== stored.task.id ||
52
14
  ((stored.task.criteria.some(c => c.acceptance !== undefined) || (stored.task.contractRevision ?? 0) > 0 || stored.task.goal !== undefined) &&
53
15
  (attestation.schemaVersion !== 3 || attestation.taskContracts?.[stored.task.id] !== taskContractHash(stored.task))) ||
@@ -86,7 +48,7 @@ export async function checkTaskVerificationEvidence(stored, options) {
86
48
  if (evidence.taskId === stored.task.id && evidence.criterionId === criterion.id &&
87
49
  evidence.commandId === command.id && evidence.configHash === configHash &&
88
50
  evidence.sourceFingerprint === sourceFingerprint &&
89
- ((stored.task.goal === undefined && (stored.task.contractRevision ?? 0) === 0 && stored.task.criteria.every(c => c.acceptance === undefined)) || evidence.taskContractHash === taskContractHash(stored.task)) &&
51
+ ((stored.task.goal === undefined && (stored.task.contractRevision ?? 0) === 0 && stored.task.criteria.every(c => c.acceptance === undefined)) || evidenceMatchesContract(evidence.taskContractHash, stored.task)) &&
90
52
  (latest === undefined || Date.parse(evidence.finishedAt) > Date.parse(latest.finishedAt) ||
91
53
  (Date.parse(evidence.finishedAt) === Date.parse(latest.finishedAt) && !isPassingVerificationEvidence(command, evidence)))) {
92
54
  latest = evidence;
@@ -15,6 +15,17 @@ export function taskContractHash(task) {
15
15
  ...(task.origin === undefined ? {} : { origin: task.origin }),
16
16
  ...(task.originRequired === undefined ? {} : { originRequired: true }) }));
17
17
  }
18
+ /**
19
+ * What verification actually ran: the criteria. Surface, origin, goal and revision
20
+ * changes leave it alone, so they need a fresh review but not a fresh verify.
21
+ */
22
+ export function executionContractHash(task) {
23
+ return sha256(canonicalJson({ domain: "agent-ops-execution-v1", criteria: task.criteria.map(criterionContractHash) }));
24
+ }
25
+ /** Verification evidence binds the execution hash; evidence from before 0.11 bound the full contract. */
26
+ export function evidenceMatchesContract(hash, task) {
27
+ return hash !== undefined && (hash === executionContractHash(task) || hash === taskContractHash(task));
28
+ }
18
29
  export function treeContractHash(tasks) {
19
30
  return sha256(canonicalJson(tasks.map(task => ({ id: task.id, hash: taskContractHash(task) }))
20
31
  .sort((a, b) => a.id.localeCompare(b.id))));
@@ -6,18 +6,14 @@ import { validateEvidence, validateTask, validateTaskAgainstConfig } from "../sc
6
6
  import { renderTaskMarkdown } from "./render.js";
7
7
  import { checkTaskCompletionEvidence, findIncompleteSubtask } from "./completion.js";
8
8
  import { calculateConfigHash, FileEvidenceStore, isPassingVerificationEvidence } from "../verify/evidence.js";
9
- import { collectChangeSurface } from "../verify/change-surface.js";
10
9
  import { resolveReviewScope } from "../review/scope.js";
11
10
  import { calculateSourceFingerprint } from "../verify/source-fingerprint.js";
12
11
  import { acceptanceCoverage } from "../verify/acceptance-coverage.js";
13
12
  import { loadFindingPin } from "./pin-finding.js";
14
13
  import { findReviewAttestation } from "../review/attestation.js";
15
14
  import { advanceFailureFingerprint } from "../verify/fingerprint.js";
16
- import { taskContractHash, treeContractHash } from "./contract.js";
15
+ import { evidenceMatchesContract, executionContractHash, taskContractHash, treeContractHash } from "./contract.js";
17
16
  import { commitIdentity } from "../schema/acceptance.js";
18
- import { createDebugState, DEBUG_DEFAULTS } from "./debug-state.js";
19
- import { DebugService } from "./debug.js";
20
- import { sameDebugText } from "./debug-intake.js";
21
17
  import { isCovers, parseSurface } from "../surface/schema.js";
22
18
  import { declareEscape, resolveOrigin } from "../escape/link.js";
23
19
  import { configProposal } from "../escape/rules.js";
@@ -92,15 +88,6 @@ export class TaskService {
92
88
  this.#now = options.now ?? (() => new Date().toISOString());
93
89
  this.#completion = options.completion;
94
90
  }
95
- debugService() {
96
- if (this.#completion === undefined)
97
- throw taskError("TASK_DEBUG_CONTEXT_REQUIRED", "Debug operations require repository context.");
98
- return new DebugService({ store: this.#store, tasks: this, ...this.#completion });
99
- }
100
- async assertDebugBaseline() {
101
- if (this.#completion === undefined || (await collectChangeSurface(this.#completion.gitRunner)).paths.length > 0)
102
- throw taskError("TASK_DEBUG_BASELINE_DIRTY", "Preserve user edits; debug intake needs a clean committed baseline before worktree allocation.");
103
- }
104
91
  async create(input) {
105
92
  if (input.policyConfigHash !== undefined &&
106
93
  !/^[a-f0-9]{64}$/u.test(input.policyConfigHash)) {
@@ -109,12 +96,6 @@ export class TaskService {
109
96
  if (input.sessionId !== undefined) {
110
97
  assertSessionId(input.sessionId);
111
98
  }
112
- // A new session must not restart an active issue's budget and rounds.
113
- if (input.debug !== undefined && input.parentTaskId === undefined && input.goal !== undefined) {
114
- const existing = await this.activeDebugRoot(input.goal);
115
- if (existing !== null)
116
- return input.sessionId === undefined ? existing : await this.attach(input.sessionId, existing.task.id);
117
- }
118
99
  let createdSourceCommit = input.createdSourceCommit;
119
100
  if (createdSourceCommit === undefined && this.#completion !== undefined) {
120
101
  const head = await this.#completion.gitRunner.run(["rev-parse", "--verify", "HEAD"]);
@@ -142,21 +123,11 @@ export class TaskService {
142
123
  ? {}
143
124
  : { parentTaskId: input.parentTaskId })
144
125
  };
145
- let debugSettings;
146
- if (input.debug !== undefined) {
147
- if (input.goal === undefined || createdSourceCommit === undefined || this.#completion === undefined)
148
- throw taskError("TASK_DEBUG_CONTEXT_REQUIRED", "Debug intake requires the original issue and a committed baseline.");
149
- if ((await collectChangeSurface(this.#completion.gitRunner)).paths.length > 0)
150
- throw taskError("TASK_DEBUG_BASELINE_DIRTY", "Preserve original user edits; investigate read-only or ask for an explicit committed baseline.");
151
- const config = await this.#completion.loadConfig();
152
- if (config.features.debug?.enabled === false)
153
- throw taskError("TASK_DEBUG_DISABLED", "Task debug routing is explicitly disabled.");
154
- debugSettings = config.features.debug ?? DEBUG_DEFAULTS;
126
+ // DEBUG = an ordinary task whose original regression is proven red at the baseline and green on the candidate.
127
+ if (input.debug !== undefined && input.parentTaskId === undefined) {
128
+ if (!criteria.some(c => c.acceptance?.mode === "behavioral" && c.acceptance.bindings.some(b => (b.redCheckIds ?? []).length > 0)))
129
+ throw taskError("TASK_DEBUG_RED_CHECK_REQUIRED", "A DEBUG task needs a behavioral criterion whose redCheckIds fail at the baseline and pass on the candidate.");
155
130
  task.debug = { rootTaskId: task.id, facts: [...(input.debug.facts ?? [])], unknowns: [...(input.debug.unknowns ?? [])] };
156
- if (input.origin !== undefined)
157
- throw taskError("ESCAPE_ORIGIN_DEBUG_ACTION", "A DEBUG issue declares its origin with task debug --debug-action origin after a confirmed hypothesis.");
158
- if (input.parentTaskId === undefined)
159
- task.originRequired = true;
160
131
  }
161
132
  const validation = validateTask(task);
162
133
  if (!validation.ok) {
@@ -192,13 +163,10 @@ export class TaskService {
192
163
  throw taskError("TASK_GOAL_IMMUTABLE", "Subtasks must retain their parent original goal.");
193
164
  validation.value.goal = parent.task.goal;
194
165
  }
195
- if (parent.task.debug !== undefined) {
196
- if (input.debug !== undefined)
197
- throw taskError("TASK_DEBUG_ROOT_IMMUTABLE", "A child cannot create a new issue budget.");
166
+ if (parent.task.debug !== undefined)
198
167
  validation.value.debug = structuredClone(parent.task.debug);
199
- }
200
168
  }
201
- if (input.requireSurface === true && validation.value.debug === undefined) {
169
+ if (input.requireSurface === true) {
202
170
  const parentSurface = state.tasks.find(record => record.task.id === input.parentTaskId)?.task.surface;
203
171
  const surfacedParent = parentSurface !== undefined && !isCovers(parentSurface);
204
172
  if (surface === undefined || isCovers(surface) !== surfacedParent)
@@ -208,7 +176,6 @@ export class TaskService {
208
176
  }
209
177
  const record = {
210
178
  task: validation.value,
211
- ...(input.debug === undefined ? {} : { debug: createDebugState(task.id, createdSourceCommit, debugSettings, this.#completion.root) }),
212
179
  ...(createdSourceCommit === undefined ? {} : { createdSourceCommit }),
213
180
  status: "active",
214
181
  evidence: {},
@@ -271,26 +238,28 @@ export class TaskService {
271
238
  if (taskContractHash(previous.task) !== input.expectedContractHash)
272
239
  throw taskError("TASK_CONTRACT_CHANGED", "Task contract changed.");
273
240
  // With none of them, a revision only rebinds the task to the current configuration (checked below).
274
- const rebindOnly = input.criteria === undefined && input.surface === undefined && input.origin === undefined;
241
+ const rebindOnly = input.criteria === undefined && input.surface === undefined && input.origin === undefined && input.baseline === undefined;
275
242
  if (rebindOnly && this.#completion === undefined)
276
243
  throw taskError("TASK_REVISION_INVALID", "Revision requires criteria, a surface, an origin, or a combination.");
277
- if (input.origin !== undefined && previous.task.debug !== undefined && input.debugOrigin !== true)
278
- throw taskError("ESCAPE_ORIGIN_DEBUG_ACTION", "A DEBUG issue declares its origin with task debug --debug-action origin after a confirmed hypothesis.");
279
244
  // Re-declaring the same origin only refreshes its ledger record; the contract and its proof stand.
280
245
  if (input.criteria === undefined && input.surface === undefined && input.origin !== undefined &&
281
246
  canonicalJson(input.origin) === canonicalJson(previous.task.origin ?? null))
282
247
  return previous;
283
248
  const surface = input.surface === undefined ? previous.task.surface : parseSurface(input.surface);
284
- const baseline = input.baseline ?? previous.createdSourceCommit;
285
- const criteria = input.criteria === undefined ? structuredClone(previous.task.criteria) : structuredClone(input.criteria).map(criterion => {
249
+ // An explicit --baseline moves every typed criterion (e.g. a task opened on the wrong branch); its evidence is then void.
250
+ const moved = input.baseline === undefined ? undefined : await this.#resolveCommit(input.baseline);
251
+ const baseline = moved ?? previous.createdSourceCommit;
252
+ const criteria = (input.criteria === undefined ? structuredClone(previous.task.criteria) : structuredClone(input.criteria)).map(criterion => {
286
253
  if (criterion.acceptance === undefined)
287
254
  return criterion;
288
- const sha = criterion.acceptance.baselineCommit ?? baseline;
255
+ if (input.criteria === undefined && moved === undefined)
256
+ return criterion;
257
+ const sha = moved ?? criterion.acceptance.baselineCommit ?? baseline;
289
258
  if (sha === undefined)
290
259
  throw taskError("BASELINE_REQUIRED", "Legacy tasks need an explicit baseline before typed acceptance.");
291
260
  const old = previous.task.criteria.find(c => c.id === criterion.id)?.acceptance;
292
- if (old !== undefined && sha !== old.baselineCommit)
293
- throw taskError("BASELINE_IMMUTABLE", "Create a replacement criterion instead of changing its baseline.");
261
+ if (moved === undefined && old !== undefined && sha !== old.baselineCommit)
262
+ throw taskError("BASELINE_IMMUTABLE", "Create a replacement criterion, or move every baseline with --baseline.");
294
263
  return { ...criterion, acceptance: { ...criterion.acceptance, baselineCommit: sha } };
295
264
  });
296
265
  const next = { ...previous.task, schemaVersion: TASK_SCHEMA_VERSION,
@@ -319,7 +288,13 @@ export class TaskService {
319
288
  const current = findTask(state, taskId);
320
289
  if (current.status !== "active" || JSON.stringify(current) !== JSON.stringify(previous))
321
290
  throw taskError("TASK_CONTRACT_CHANGED", "Concurrent revision preserved; retry from current task.");
322
- const result = { ...current, task: next, evidence: {}, failureFingerprint: null,
291
+ // Surface, origin or goal-only revisions keep what verification ran; review is still voided by the new contract hash.
292
+ const sameExecution = executionContractHash(next) === executionContractHash(current.task) && policyConfigHash === current.policyConfigHash;
293
+ // Review references never carry over: the new contract needs fresh review.
294
+ const verified = Object.fromEntries(Object.entries(current.evidence)
295
+ .map(([id, refs]) => [id, refs.filter(ref => !ref.startsWith("review:"))]).filter(([, refs]) => refs.length > 0));
296
+ const result = { ...current, task: next, evidence: sameExecution ? verified : {},
297
+ failureFingerprint: sameExecution ? current.failureFingerprint : null, ...(moved === undefined ? {} : { createdSourceCommit: moved }),
323
298
  policyConfigHash, completionBase: null, updatedAt: now,
324
299
  revisions: [...(current.revisions ?? []), { previousTask: current.task, previousEvidence: current.evidence,
325
300
  previousFailure: current.failureFingerprint, previousHash: input.expectedContractHash,
@@ -329,6 +304,15 @@ export class TaskService {
329
304
  return cloneRecord(withoutReview);
330
305
  });
331
306
  }
307
+ /** A moved baseline must be a real commit in the candidate's history, so red-to-green still proves this change. */
308
+ async #resolveCommit(ref) {
309
+ const git = this.#completion?.gitRunner;
310
+ const resolved = git === undefined ? undefined : await git.run(["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`]);
311
+ const sha = resolved?.exitCode === 0 ? resolved.stdout.toString().trim() : "";
312
+ if (!commitIdentity(sha) || (await git.run(["merge-base", "--is-ancestor", sha, "HEAD"])).exitCode !== 0)
313
+ throw taskError("TASK_BASELINE_INVALID", "--baseline must resolve to a commit that is an ancestor of HEAD.");
314
+ return sha;
315
+ }
332
316
  async replan(taskId, input) {
333
317
  if (input.reason.trim() === "" || input.reason.includes("\0") || input.reason.length > 8192 ||
334
318
  input.tasks.length === 0 || input.tasks.length > 128)
@@ -461,8 +445,6 @@ export class TaskService {
461
445
  if (record.policyConfigHash !== parent.policyConfigHash || !validateTask(task).ok) {
462
446
  throw taskError("TASK_DELIVERY_INVALID", `Task ${task.id} has an incompatible policy or definition.`);
463
447
  }
464
- if (parent.task.debug !== undefined && (task.goal !== parent.task.goal || JSON.stringify(task.debug) !== JSON.stringify(parent.task.debug) || record.debug !== undefined))
465
- throw taskError("TASK_DEBUG_ROOT_MISMATCH", "A delivery must retain the original issue identity and cannot import an independent debug budget.");
466
448
  const existing = state.tasks.find((item) => item.task.id === task.id);
467
449
  if (existing !== undefined) {
468
450
  if (JSON.stringify(existing.task) !== JSON.stringify(task)) {
@@ -488,11 +470,6 @@ export class TaskService {
488
470
  }
489
471
  });
490
472
  }
491
- /** The active DEBUG root already holding this original issue, if any. */
492
- async activeDebugRoot(goal) {
493
- return (await this.list()).find((record) => record.status === "active" && record.task.debug?.rootTaskId === record.task.id &&
494
- record.task.goal !== undefined && sameDebugText(record.task.goal, goal)) ?? null;
495
- }
496
473
  async list(filter = {}) {
497
474
  const state = await this.#store.read();
498
475
  return state.tasks
@@ -721,7 +698,7 @@ export class TaskService {
721
698
  validation.value.criterionId === criterionId && validation.value.commandId === commandId &&
722
699
  validation.value.configHash === configHash && validation.value.sourceFingerprint === sourceFingerprint &&
723
700
  (!(snapshot.task.goal !== undefined || (snapshot.task.contractRevision ?? 0) > 0 || snapshot.task.criteria.some(c => c.acceptance !== undefined)) ||
724
- validation.value.taskContractHash === taskContractHash(snapshot.task))
701
+ evidenceMatchesContract(validation.value.taskContractHash, snapshot.task))
725
702
  ? validation.value : null;
726
703
  };
727
704
  for (const criterion of snapshot.task.criteria) {
@@ -4,7 +4,6 @@ import { AgentOpsError } from "../fs/paths.js";
4
4
  import { validateTask } from "../schema/validate.js";
5
5
  import { readPrivateFile, withPrivateFileLock, writePrivateFile } from "../security/permissions.js";
6
6
  import { taskContractHash } from "./contract.js";
7
- import { assertDebugState } from "./debug-state.js";
8
7
  import { commitIdentity, acceptanceSourcePath } from "../schema/acceptance.js";
9
8
  const DEFAULT_MAX_BYTES = 1024 * 1024;
10
9
  const SESSION_ID_PATTERN = /^[^\0\r\n]{1,256}$/u;
@@ -78,11 +77,9 @@ function parseTaskRecord(value) {
78
77
  if (!task.ok) {
79
78
  return invalidState("Task state contains an invalid task.");
80
79
  }
81
- if (value.debug !== undefined) {
82
- assertDebugState(value.debug);
83
- if (value.debug.rootTaskId !== task.value.id || task.value.debug?.rootTaskId !== task.value.id)
84
- return invalidState("Debug counters belong only to the original issue root.");
85
- }
80
+ // Pre-0.11 DEBUG budget/hypothesis state: accepted, then dropped on load.
81
+ if (value.debug !== undefined && !isRecord(value.debug))
82
+ return invalidState("Task state contains an invalid task record.");
86
83
  if (!["active", "archived", "complete"].includes(String(value.status)) ||
87
84
  !isTimestamp(value.createdAt) ||
88
85
  !isTimestamp(value.updatedAt) ||
@@ -188,7 +185,6 @@ function parseTaskRecord(value) {
188
185
  }
189
186
  return {
190
187
  task: task.value,
191
- ...(value.debug === undefined ? {} : { debug: value.debug }),
192
188
  ...(value.noChangePaths === undefined ? {} : { noChangePaths: value.noChangePaths }),
193
189
  ...(value.createdSourceCommit === undefined ? {} : { createdSourceCommit: value.createdSourceCommit }),
194
190
  ...(value.revisions === undefined ? {} : { revisions: value.revisions }),
@@ -2,10 +2,9 @@ import { calculateConfigHash, canonicalJson } from "../config/hash.js";
2
2
  import { readPrivateFile } from "../security/permissions.js";
3
3
  import { join } from "node:path";
4
4
  import { sha256 } from "../fs/hash.js";
5
- import { criterionContractHash, taskContractHash } from "../task/contract.js";
5
+ import { criterionContractHash, evidenceMatchesContract } from "../task/contract.js";
6
6
  /** Only current, paired check evidence discharges a mechanical contract. */
7
7
  export async function acceptanceCoverage(record, config, sourceFingerprint, store) {
8
- const contract = taskContractHash(record.task);
9
8
  const configHash = calculateConfigHash(config);
10
9
  const rows = [];
11
10
  for (const criterion of record.task.criteria) {
@@ -18,7 +17,7 @@ export async function acceptanceCoverage(record, config, sourceFingerprint, stor
18
17
  const evidence = await store.load(reference);
19
18
  if (evidence?.schemaVersion === 4 && evidence.taskId === record.task.id &&
20
19
  evidence.criterionId === criterion.id && evidence.configHash === configHash &&
21
- evidence.sourceFingerprint === sourceFingerprint && evidence.taskContractHash === contract &&
20
+ evidence.sourceFingerprint === sourceFingerprint && evidenceMatchesContract(evidence.taskContractHash, record.task) &&
22
21
  evidence.acceptance?.criterionContractHash === criterionContractHash(criterion)) {
23
22
  matching.push({ reference, evidence });
24
23
  }
@@ -167,9 +167,10 @@ export function aggregateAcceptanceRun(run, requestedCheckIds) {
167
167
  const duplicateCheckIds = sortedUnique([...counts.entries()].filter(([, count]) => count > 1).map(([id]) => id));
168
168
  const unexpectedCheckIds = sortedUnique(run.results.filter((result) => !expected.has(result.checkId)).map((result) => result.checkId));
169
169
  const missingCheckIds = requested.filter((id) => !counts.has(id));
170
- if (duplicateCheckIds.length > 0 || unexpectedCheckIds.length > 0 || missingCheckIds.length > 0) {
171
- return unknown(run, run.results, duplicateCheckIds.length > 0 ? ACCEPTANCE_FAILURE.duplicate :
172
- missingCheckIds.length > 0 ? ACCEPTANCE_FAILURE.missing : ACCEPTANCE_FAILURE.unexpected, ["The observed checks do not match the requested checks exactly."], missingCheckIds, duplicateCheckIds, unexpectedCheckIds);
170
+ // Checks the runner emits beyond the requested ones are ignored: a binding may list only its red checks.
171
+ // An unrequested failure still surfaces through the runner's exit code, which replay reconciles.
172
+ if (duplicateCheckIds.length > 0 || missingCheckIds.length > 0) {
173
+ return unknown(run, run.results, duplicateCheckIds.length > 0 ? ACCEPTANCE_FAILURE.duplicate : ACCEPTANCE_FAILURE.missing, ["The observed checks do not include every requested check exactly once."], missingCheckIds, duplicateCheckIds, unexpectedCheckIds);
173
174
  }
174
175
  const requestedResults = requested.map((id) => run.results.find((result) => result.checkId === id));
175
176
  if (run.diagnostics.length > 0) {
@@ -8,7 +8,7 @@ import { canonicalJson } from "../config/hash.js";
8
8
  import { sha256 } from "../fs/hash.js";
9
9
  import { removeTemporary } from "../fs/temporary.js";
10
10
  import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
11
- import { criterionContractHash, taskContractHash } from "../task/contract.js";
11
+ import { criterionContractHash, executionContractHash } from "../task/contract.js";
12
12
  import { aggregateAcceptanceRun, makeAcceptanceCheck, makeAcceptanceRun } from "./acceptance-protocol.js";
13
13
  import { collectNodeAcceptance, collectJestAcceptance, collectVitestAcceptance, collectPytestAcceptance, collectRustAcceptance } from "./adapters/index.js";
14
14
  import { buildVerificationEvidence } from "./evidence.js";
@@ -118,12 +118,9 @@ export async function replayAcceptance(options) {
118
118
  const env = replayEnvironment(checkout, phase, executionId);
119
119
  let timedOut;
120
120
  const command = async (executable, args, cwd, stage, own) => {
121
- const remaining = await options.remainingMs?.();
122
- if (remaining === 0)
123
- throw new AgentOpsError("TASK_DEBUG_BUDGET_EXHAUSTED", "The original issue's active budget is exhausted.");
124
121
  const limit = own ?? ACCEPTANCE_TIMEOUTS[stage];
125
122
  const result = await runVerificationCommand({ id: group.runner.id, command: executable, args: [...args], cwd: ".", required: true,
126
- evidence: { kind: "exit-code" }, timeoutMs: Math.min(limit, remaining ?? Infinity) }, { cwd, runner: options.processRunner, env, replaceEnv: true,
123
+ evidence: { kind: "exit-code" }, timeoutMs: limit }, { cwd, runner: options.processRunner, env, replaceEnv: true,
127
124
  outputLimitBytes: 1024 * 1024, ...(options.signal === undefined ? {} : { signal: options.signal }) });
128
125
  if (result.failureClass === "timeout" && timedOut === undefined)
129
126
  timedOut = stage === "git"
@@ -217,7 +214,7 @@ export async function replayAcceptance(options) {
217
214
  failure = aggregate?.failureClass ?? output.failureClass;
218
215
  }
219
216
  catch (error) {
220
- failure = error instanceof AgentOpsError && error.code === "TASK_DEBUG_BUDGET_EXHAUSTED" ? "debug-budget-exhausted" : error instanceof Error && /^[a-z-]+$/u.test(error.message) ? error.message : "replay-unavailable";
217
+ failure = error instanceof Error && /^[a-z-]+$/u.test(error.message) ? error.message : "replay-unavailable";
221
218
  }
222
219
  finally {
223
220
  await removeTemporary(temp);
@@ -245,7 +242,7 @@ export async function replayAcceptance(options) {
245
242
  scope: options.scope, startedAt, finishedAt, exitCode: output?.exitCode ?? null, testCount: checks.length,
246
243
  status, failureClass: status === "PASS" ? "none" : phase === "baseline" && aggregate?.status === "PASS" ? "non-discriminating" : failure,
247
244
  sourceFingerprint: options.sourceFingerprint, toolVersions: { node: process.version }, config: options.config,
248
- taskContractHash: taskContractHash(options.task), acceptance: { criterionContractHash: criterionContractHash(criterion),
245
+ taskContractHash: executionContractHash(options.task), acceptance: { criterionContractHash: criterionContractHash(criterion),
249
246
  bindingHash: sha256(canonicalJson({ runnerId: group.runner.id, materials: group.binding.materials })), phase, commit, pairedCommit: phase === "baseline" ? candidate : group.baseline, materialDigest, executionDigest, executionArtifact, checks } });
250
247
  const reference = await options.evidenceStore.save(evidence);
251
248
  reports.push({ criterionId: criterion.id, runnerId: group.runner.id, phase, status, failureClass: evidence.failureClass, reference,
@@ -1,4 +1,4 @@
1
- import { taskContractHash } from "../task/contract.js";
1
+ import { executionContractHash, taskContractHash } from "../task/contract.js";
2
2
  import { replayAcceptance } from "./acceptance-replay.js";
3
3
  import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
4
4
  import { validateTaskAgainstConfig } from "../schema/validate.js";
@@ -61,7 +61,9 @@ export class VerificationService {
61
61
  failureClass: result.failureClass,
62
62
  sourceFingerprint,
63
63
  toolVersions: this.#options.toolVersions ?? {},
64
- ...(task.goal === undefined && (task.contractRevision ?? 0) === 0 && task.criteria.every(c => c.acceptance === undefined) ? {} : { taskContractHash: taskContractHash(task) }),
64
+ // Surfaced tasks are always bound, so a later surface-only revision of a goal-less one keeps it; legacy tasks stay unbound.
65
+ ...(task.goal === undefined && (task.contractRevision ?? 0) === 0 && task.criteria.every(c => c.acceptance === undefined) &&
66
+ task.surfaceRequired === undefined ? {} : { taskContractHash: executionContractHash(task) }),
65
67
  config: this.#options.config
66
68
  });
67
69
  references.push(await this.#options.evidenceStore.save(evidence));
@@ -70,10 +72,7 @@ export class VerificationService {
70
72
  }
71
73
  async #runCommand(task, command) {
72
74
  const startedAt = (this.#options.now ?? (() => new Date().toISOString()))();
73
- const remaining = task.debug === undefined ? undefined : await this.#options.taskService.debugService().remaining(task.id);
74
- if (remaining === 0)
75
- throw verificationError("TASK_DEBUG_BUDGET_EXHAUSTED", "The original issue's active budget is exhausted.");
76
- const result = await executeConfiguredCommand(remaining === undefined ? command : { ...command, timeoutMs: Math.min(command.timeoutMs ?? 300_000, remaining) }, {
75
+ const result = await executeConfiguredCommand(command, {
77
76
  cwd: await this.#commandCwd(command),
78
77
  runner: this.#options.processRunner,
79
78
  trusted: this.#options.trusted
@@ -110,11 +109,6 @@ export class VerificationService {
110
109
  };
111
110
  }
112
111
  async verify(taskId) {
113
- const stored = await this.#options.taskService.status({ taskId });
114
- return stored.task.debug === undefined ? await this.#verify(taskId) :
115
- await this.#options.taskService.debugService().verification(taskId, () => this.#verify(taskId));
116
- }
117
- async #verify(taskId) {
118
112
  const stored = await this.#options.taskService.status({ taskId });
119
113
  if (stored.status === "archived") {
120
114
  throw verificationError("TASK_NOT_ACTIVE", "An archived task cannot be verified.");
@@ -153,8 +147,7 @@ export class VerificationService {
153
147
  results.push(await this.#runCommand(validation.value, commandById(this.#options.config, commandId)));
154
148
  }
155
149
  const acceptance = validation.value.criteria.some(c => c.acceptance !== undefined && c.acceptance.mode !== "review-only")
156
- ? await replayAcceptance({ ...this.#options, task: validation.value, sourceFingerprint,
157
- ...(stored.task.debug === undefined ? {} : { remainingMs: () => this.#options.taskService.debugService().remaining(taskId) }) }) : [];
150
+ ? await replayAcceptance({ ...this.#options, task: validation.value, sourceFingerprint }) : [];
158
151
  let status = aggregateVerificationStatus([...results, ...acceptance.map(a => ({ required: true, status: a.status }))]);
159
152
  let sourceChanged = false;
160
153
  const postflightScope = await resolveReviewScope({
@@ -197,16 +190,6 @@ export class VerificationService {
197
190
  }
198
191
  }
199
192
  else {
200
- if (stored.task.debug !== undefined && !sourceChanged) {
201
- const failures = {};
202
- for (const result of results) {
203
- const references = await this.#persistEvidence(validation.value, commandById(this.#options.config, result.commandId), result.startedAt, result.finishedAt, result, sourceFingerprint);
204
- relevantCriteria(validation.value, result.commandId).forEach((criterion, index) => { const reference = references[index]; if (reference !== undefined)
205
- failures[criterion.id] = [...(failures[criterion.id] ?? []), reference]; });
206
- }
207
- if (Object.keys(failures).length > 0)
208
- await this.#options.taskService.recordEvidence(taskId, failures);
209
- }
210
193
  const required = [...results.filter((result) => result.required), ...acceptance.map(a => ({ commandId: a.runnerId, required: true,
211
194
  status: a.status, failureClass: a.failureClass, exitCode: null, timedOut: false, testCount: null,
212
195
  diagnostic: a.criterionId + ":" + a.phase + ":" + a.failureClass, evidenceReferences: [a.reference], startedAt: "", finishedAt: "" }))];