@tea-agent/loop-agent 0.33.6-beta.0 → 0.33.6

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 (64) hide show
  1. package/CHANGELOG.md +29 -4
  2. package/dist/application/task-lifecycle/advance.js +254 -4
  3. package/dist/application/task-lifecycle/gates.js +50 -0
  4. package/dist/application/task-lifecycle/observe.js +11 -2
  5. package/dist/commands/init-upgrade.js +32 -1
  6. package/dist/commands/init.js +94 -3
  7. package/dist/executors/shell-write-guard.js +26 -8
  8. package/dist/shared/operator/capabilities.js +72 -42
  9. package/dist/shared/resilient-git.js +133 -0
  10. package/dist/task/source-prepare/artifact-meta.js +137 -0
  11. package/dist/task/source-prepare/index.js +2 -0
  12. package/dist/task/source-prepare/parse-intent.js +58 -10
  13. package/dist/task/source-prepare/prepare.js +180 -16
  14. package/dist/task/source-prepare/reference-integrity.js +18 -2
  15. package/dist/task/source-prepare/semantic-intake.js +404 -0
  16. package/dist/worker/console/app-data.js +2 -0
  17. package/dist/worker/console/chat/chat-event-store.js +190 -25
  18. package/dist/worker/console/chat/pi-console-config.js +250 -32
  19. package/dist/worker/console/chat/pi-runtime.js +625 -71
  20. package/dist/worker/console/chat/resource-loader.js +5 -4
  21. package/dist/worker/console/chat/routes.js +324 -146
  22. package/dist/worker/console/chat/runtime-context.js +48 -12
  23. package/dist/worker/console/chat/runtime-selection.js +59 -0
  24. package/dist/worker/console/chat/shortcuts.js +1 -0
  25. package/dist/worker/console/chat/tool-adapter.js +9 -3
  26. package/dist/worker/console/chat/tools.js +5 -1
  27. package/dist/worker/console/dag-execution-receipt.js +380 -0
  28. package/dist/worker/console/operator-actions.js +559 -68
  29. package/dist/worker/console/server.js +8 -15
  30. package/dist/worker/console/static/assets/index-BUOLppPr.js +28 -0
  31. package/dist/worker/console/static/assets/index-C1KzazY5.css +1 -0
  32. package/dist/worker/console/static/index.html +2 -2
  33. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
  34. package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
  35. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
  36. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +16 -0
  37. package/dist/worker/console/static-src/operator-chat/useChatStream.js +210 -184
  38. package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
  39. package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
  40. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
  41. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
  42. package/dist/worker/delivery/final-verification.js +13 -5
  43. package/dist/worker/delivery/package.js +31 -19
  44. package/dist/worker/delivery/verification-bundle.js +6 -4
  45. package/dist/worker/observe/static/operator-chrome.css +5 -2
  46. package/dist/worker/observe/static/operator-chrome.js +6 -1
  47. package/dist/worker/observe/static/styles.css +39 -9
  48. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +2 -27
  49. package/dist/workflows/dag/backend-test-module-stem.js +0 -5
  50. package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
  51. package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
  52. package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
  53. package/dist/workflows/dag/init-hybrid.js +12 -20
  54. package/dist/workflows/dag/types.js +0 -7
  55. package/dist/workflows/dag/workspace-checkpoint.js +8 -27
  56. package/docs/templates/backend-test-dag.json +10 -10
  57. package/harness.json +1 -1
  58. package/package.json +1 -1
  59. package/skills/loop-agent/references/command-reference.md +3 -1
  60. package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
  61. package/skills/loop-agent/references/task-workflow.md +4 -0
  62. package/dist/worker/console/chat/instruction-skills.js +0 -217
  63. package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
  64. package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
@@ -1,7 +1,7 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import { buildOperatorCapabilitiesDocument } from "../../shared/operator/capabilities.js";
3
3
  import { buildOperatorResult, operatorFailed, operatorSucceeded, } from "../../shared/operator/envelope.js";
4
- import { getTaskPaths } from "../../task/runtime.js";
4
+ import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
5
5
  import { diffTaskContract, validateDraftForTask, } from "../../task/contract/index.js";
6
6
  import { stageUtf8Text } from "./app-data.js";
7
7
  import { ALL_CONFIRMATION_CHALLENGES, hashDagBytes, } from "./dag-confirmation.js";
@@ -19,10 +19,12 @@ import { deriveTaskIdentityFromPrd, nextTaskIdRevision, } from "./prd-identity.j
19
19
  import { resolveSiblingAgentWorkerBin } from "./sibling-controller.js";
20
20
  import { projectOperationEventSummary, projectOperationForChat, } from "./chat/chat-event-store.js";
21
21
  import { MutationGateReceiptStore } from "./mutation-gate-receipt-store.js";
22
+ import { assessAutonomousExecutionEligibility, deriveEligibilityFactsFromDagSpec, } from "./dag-execution-receipt.js";
22
23
  import { issueHumanGateToken, mutationGatePayloadHash, verifyHumanGateToken, } from "./human-gate-token.js";
23
24
  const MUTATION_GATE_ACTIONS = new Set([
24
- "standaloneTaskRerun",
25
- "workerTaskRetry",
25
+ // 2026-08-11: standaloneTaskRerun / workerTaskRetry are autonomous (server
26
+ // run-facts checks); only Night Scheduler mutations still need the browser
27
+ // Human Gate.
26
28
  "workerAdmissionPrepare",
27
29
  "workerSchedulerAdd",
28
30
  "workerSchedulerCancel",
@@ -72,6 +74,150 @@ function readWriteSetGateToken(reviewPacket) {
72
74
  const token = reviewPacket.writeSetGateToken;
73
75
  return typeof token === "string" && token.includes(":") ? token : undefined;
74
76
  }
77
+ /** Extract the review packet from a task advance --dag-output operator JSON. */
78
+ function extractReviewPacketFromCliJson(json) {
79
+ if (!json || typeof json !== "object")
80
+ return undefined;
81
+ const envelope = json;
82
+ const payload = envelope.result && typeof envelope.result === "object"
83
+ ? envelope.result
84
+ : envelope;
85
+ const packet = payload.reviewPacket;
86
+ return packet && typeof packet === "object"
87
+ ? packet
88
+ : undefined;
89
+ }
90
+ /**
91
+ * Read the live managed Task Contract binding for a task (server-derived).
92
+ * Returns undefined when the task is not managed or status facts are missing
93
+ * so callers can fail closed.
94
+ */
95
+ async function readLiveTaskContractBinding(ctx, taskId) {
96
+ const showBody = await runReadCli(ctx, ["task", "status", taskId, "--json"], "task status");
97
+ const statusResult = (showBody.result ?? {});
98
+ const c = showBody.ok ? statusResult.contract : undefined;
99
+ if (!c?.canonicalHash || typeof c.revision !== "number")
100
+ return undefined;
101
+ return {
102
+ schemaVersion: 1,
103
+ taskId,
104
+ revision: c.revision,
105
+ canonicalHash: c.canonicalHash,
106
+ taskConfigSha256: c.taskConfigSha256 ?? undefined,
107
+ projectionVersion: c.projectionVersion ?? undefined,
108
+ canonicalizerVersion: c.canonicalizerVersion ?? undefined,
109
+ taskConfigSchemaVersion: c.taskConfigSchemaVersion ?? undefined,
110
+ };
111
+ }
112
+ function gateError(action, code, message, details) {
113
+ const status = code === "NOT_FOUND" ? 404 : code === "INVALID_INPUT" ? 400 : 403;
114
+ return {
115
+ kind: "error",
116
+ status,
117
+ body: operatorFailed({
118
+ command: action,
119
+ outcome: "blocked",
120
+ code,
121
+ message,
122
+ details,
123
+ }),
124
+ };
125
+ }
126
+ /**
127
+ * 2026-08-11 R2 run-facts check for standaloneTaskRerun (read-only CLI
128
+ * evidence only — no browser Human Gate). Fails closed when facts are missing.
129
+ */
130
+ async function assertAutonomousRerunEligible(ctx, runId, taskId) {
131
+ const report = await runReadCli(ctx, ["dag", "report", "--run-id", runId, "--json"], "dag report");
132
+ const reportResult = (report.ok ? report.result : undefined);
133
+ const recovery = reportResult?.primaryRecovery;
134
+ if (!report.ok || !reportResult?.primaryFailure || !recovery) {
135
+ return gateError("standaloneTaskRerun", "INVALID_INPUT", "cannot read dagReport run facts; standaloneTaskRerun requires server-side run evidence", { runId });
136
+ }
137
+ if (recovery.humanRequired === true) {
138
+ return gateError("standaloneTaskRerun", "HUMAN_CONFIRMATION_REQUIRED", `primaryRecovery requires a human decision (action=${recovery.action ?? "?"}); stop autonomous rerun`, { runId });
139
+ }
140
+ const nodeId = reportResult.primaryFailure.scope === "node"
141
+ ? reportResult.primaryFailure.nodeId
142
+ : undefined;
143
+ let planEligible;
144
+ if (nodeId) {
145
+ const plan = await runReadCli(ctx, [
146
+ "dag",
147
+ "rerun",
148
+ "--run-id",
149
+ runId,
150
+ "--from-node",
151
+ nodeId,
152
+ "--plan",
153
+ "--json",
154
+ ], "dag rerun plan");
155
+ const planResult = (plan.result ?? {});
156
+ if (plan.ok && planResult.eligible === true) {
157
+ return gateError("standaloneTaskRerun", "INVALID_INPUT", `node rerun plan is eligible (planHash=${planResult.planHash ?? "?"}); use dagRerun with the fresh planHash instead of standaloneTaskRerun`, { runId, nodeId });
158
+ }
159
+ if (!plan.ok && planResult.eligible === undefined) {
160
+ return gateError("standaloneTaskRerun", "INVALID_INPUT", "dagRerunPlan run facts unavailable; standaloneTaskRerun fails closed", { runId, nodeId });
161
+ }
162
+ planEligible = planResult.eligible;
163
+ }
164
+ if (planEligible !== false) {
165
+ // No ineligible-plan evidence: require an explicit rerun-task recommendation.
166
+ const recommendsRerunTask = recovery.action === "rerun-after-fix" ||
167
+ /rerun-task|standalone/i.test(recovery.commandHint ?? "");
168
+ if (!recommendsRerunTask) {
169
+ return gateError("standaloneTaskRerun", "HUMAN_CONFIRMATION_REQUIRED", `run facts do not support autonomous standaloneTaskRerun (recovery action=${recovery.action ?? "?"}); stop`, { runId });
170
+ }
171
+ }
172
+ if (taskId) {
173
+ // Keep the original taskId: the run must still be bound to it.
174
+ const status = await runReadCli(ctx, ["task", "status", taskId, "--json"], "task status");
175
+ const statusResult = (status.ok ? status.result : undefined);
176
+ const boundRunId = statusResult?.activeRun?.runId ?? statusResult?.latestRun?.runId;
177
+ if (!status.ok || !boundRunId || boundRunId !== runId) {
178
+ return gateError("standaloneTaskRerun", "INVALID_INPUT", `taskId ${taskId} is not bound to run ${runId}; keep the original taskId for same-task rerun`, { runId, taskId, boundRunId });
179
+ }
180
+ }
181
+ return { kind: "ready" };
182
+ }
183
+ /**
184
+ * 2026-08-11 read-only facts check for workerTaskRetry: the task must exist
185
+ * in the pool doctor inventory with status Failed (featureId consistent).
186
+ */
187
+ async function assertWorkerTaskRetryFacts(ctx, taskId, featureId) {
188
+ const run = ctx.runCommand ??
189
+ ((args, options) => ctx.client.runExternal
190
+ ? ctx.client.runExternal(resolveSiblingAgentWorkerBin(), args, options)
191
+ : ctx.client.run(args, options));
192
+ let result;
193
+ try {
194
+ result = await run(["pool", "doctor", "--repo", ctx.repoRoot, "--json"], {
195
+ cwd: ctx.repoRoot,
196
+ artifactName: "worker-task-retry-facts",
197
+ expectJson: true,
198
+ });
199
+ }
200
+ catch (error) {
201
+ return gateError("workerTaskRetry", "INVALID_INPUT", `cannot read pool doctor facts: ${error instanceof Error ? error.message : String(error)}`);
202
+ }
203
+ if (!result.ok || result.exitCode !== 0 || !result.json) {
204
+ return gateError("workerTaskRetry", "INVALID_INPUT", "cannot read pool doctor run facts; workerTaskRetry fails closed");
205
+ }
206
+ const doctor = result.json;
207
+ const v2Entry = (doctor.v2 ?? []).find((entry) => entry.taskId === taskId);
208
+ const legacyEntry = (doctor.legacy ?? []).find((entry) => entry.taskId === taskId && Boolean(entry.featureId));
209
+ const entry = v2Entry ?? legacyEntry;
210
+ if (!entry) {
211
+ return gateError("workerTaskRetry", "INVALID_INPUT", `task ${taskId} not found in the pool doctor inventory`, { taskId });
212
+ }
213
+ if (entry.status !== "Failed") {
214
+ return gateError("workerTaskRetry", "HUMAN_CONFIRMATION_REQUIRED", `task ${taskId} is ${entry.status}; only Failed tasks are retry-eligible`, { taskId, status: entry.status });
215
+ }
216
+ if (featureId && entry.featureId && entry.featureId !== featureId) {
217
+ return gateError("workerTaskRetry", "INVALID_INPUT", `featureId ${featureId} does not match the pool task ${taskId} (${entry.featureId})`, { taskId, featureId, observedFeatureId: entry.featureId });
218
+ }
219
+ return { kind: "ready" };
220
+ }
75
221
  /**
76
222
  * Prefer structured stdout JSON failure semantics (findings / message) over bare
77
223
  * `exit N` when stderr is empty — spine audit and similar read CLIs write details
@@ -889,6 +1035,239 @@ export async function dispatchOperatorAction(ctx, req) {
889
1035
  }),
890
1036
  };
891
1037
  }
1038
+ case "prepareDagExecution": {
1039
+ const taskId = str(p.taskId);
1040
+ if (!taskId)
1041
+ return invalid(action, "taskId is required");
1042
+ const profile = str(p.profile) ?? "auto";
1043
+ const run = ctx.runCommand ?? ((a, o) => ctx.client.run(a, o));
1044
+ // Server-side generate + strict validate only. The model never supplies
1045
+ // DAG bytes or raw paths (AC-02): everything is re-derived here.
1046
+ const staged = await stageUtf8Text(ctx.appData, "", {
1047
+ extension: ".json",
1048
+ });
1049
+ const gen = await run([
1050
+ "task",
1051
+ "advance",
1052
+ taskId,
1053
+ "--profile",
1054
+ profile,
1055
+ "--dag-output",
1056
+ staged.path,
1057
+ "--json",
1058
+ ], {
1059
+ cwd: ctx.repoRoot,
1060
+ artifactName: "prepare-dag-execution-task-advance",
1061
+ expectJson: true,
1062
+ timeoutMs: 180_000,
1063
+ });
1064
+ if (!gen.ok || gen.exitCode !== 0) {
1065
+ return {
1066
+ kind: "error",
1067
+ status: 400,
1068
+ body: operatorFailed({
1069
+ command: action,
1070
+ outcome: "invalid",
1071
+ code: "INVALID_INPUT",
1072
+ message: gen.stderr?.trim() ||
1073
+ `task advance (prepare DAG execution) failed with exit ${gen.exitCode}`,
1074
+ details: {
1075
+ exitCode: gen.exitCode,
1076
+ stdoutPreview: (gen.stdout ?? "").slice(0, 1200),
1077
+ },
1078
+ }),
1079
+ };
1080
+ }
1081
+ let dagText;
1082
+ try {
1083
+ dagText = await readFile(staged.path, "utf8");
1084
+ }
1085
+ catch {
1086
+ dagText = "";
1087
+ }
1088
+ if (!dagText.trim()) {
1089
+ // Fallback: advance may skip generate-dag when a draft already
1090
+ // exists; still prefer the task DAG over an empty staged file.
1091
+ const taskDagPath = getTaskPaths(ctx.repoRoot, taskId).dagDraftPath;
1092
+ try {
1093
+ dagText = await readFile(taskDagPath, "utf8");
1094
+ }
1095
+ catch {
1096
+ dagText = "";
1097
+ }
1098
+ }
1099
+ if (!dagText.trim()) {
1100
+ return {
1101
+ kind: "error",
1102
+ status: 400,
1103
+ body: operatorFailed({
1104
+ command: action,
1105
+ outcome: "invalid",
1106
+ code: "INVALID_INPUT",
1107
+ message: "task advance produced empty DAG output",
1108
+ }),
1109
+ };
1110
+ }
1111
+ const writeSetGateToken = extractWriteSetGateTokenFromCliJson(gen.json, taskId);
1112
+ const finalStaged = await stageUtf8Text(ctx.appData, dagText, {
1113
+ extension: ".json",
1114
+ });
1115
+ const controller = ctx.client.getIdentity?.()?.packageFingerprint?.value ??
1116
+ "unknown-controller";
1117
+ const draftRec = await ctx.drafts.get(taskId);
1118
+ const sourceBindingSha256 = draftRec
1119
+ ? draftRec.draftSha256
1120
+ : hashDagBytes(`source:${taskId}:${finalStaged.sha256}`);
1121
+ const validateBody = await runReadCli(ctx, ["dag", "validate", "--dag", finalStaged.path, "--json"], "dag validate");
1122
+ if (!validateBody.ok) {
1123
+ return {
1124
+ kind: "error",
1125
+ status: 400,
1126
+ body: operatorFailed({
1127
+ command: action,
1128
+ outcome: "invalid",
1129
+ code: "INVALID_INPUT",
1130
+ message: validateBody.error?.message ??
1131
+ "dag validate failed; cannot prepare execution for an invalid DAG",
1132
+ details: {
1133
+ validateError: validateBody.error ?? null,
1134
+ dagSha256: finalStaged.sha256,
1135
+ },
1136
+ }),
1137
+ };
1138
+ }
1139
+ const validationResultSha256 = hashDagBytes(JSON.stringify({
1140
+ ok: true,
1141
+ command: "dag validate",
1142
+ result: validateBody.result ?? null,
1143
+ }));
1144
+ const taskContractBinding = await readLiveTaskContractBinding(ctx, taskId);
1145
+ if (!taskContractBinding) {
1146
+ return {
1147
+ kind: "error",
1148
+ status: 409,
1149
+ body: operatorFailed({
1150
+ command: action,
1151
+ outcome: "blocked",
1152
+ code: "BINDING_DRIFT",
1153
+ message: "managed Task Contract binding unavailable; apply/adopt before prepareDagExecution",
1154
+ details: { taskId },
1155
+ }),
1156
+ };
1157
+ }
1158
+ // G2 deterministic assessment from server-side facts only.
1159
+ let allowedPaths;
1160
+ let forbiddenPaths;
1161
+ try {
1162
+ const taskConfig = await loadTaskConfig(ctx.repoRoot, taskId);
1163
+ allowedPaths = Array.isArray(taskConfig.allowedPaths)
1164
+ ? taskConfig.allowedPaths
1165
+ : [];
1166
+ forbiddenPaths = Array.isArray(taskConfig.forbiddenPaths)
1167
+ ? taskConfig.forbiddenPaths
1168
+ : [];
1169
+ }
1170
+ catch {
1171
+ return {
1172
+ kind: "error",
1173
+ status: 403,
1174
+ body: operatorFailed({
1175
+ command: action,
1176
+ outcome: "blocked",
1177
+ code: "HUMAN_CONFIRMATION_REQUIRED",
1178
+ message: "task boundary (task.json allowedPaths/forbiddenPaths) unavailable; autonomous execution not permitted",
1179
+ }),
1180
+ };
1181
+ }
1182
+ const reviewPacket = extractReviewPacketFromCliJson(gen.json) ?? {};
1183
+ const derivedFacts = deriveEligibilityFactsFromDagSpec({
1184
+ dagSpec: JSON.parse(dagText),
1185
+ allowedPaths,
1186
+ forbiddenPaths,
1187
+ });
1188
+ const packetWriters = Array.isArray(reviewPacket.writers)
1189
+ ? reviewPacket.writers.map((writer) => ({
1190
+ nodeId: writer.nodeId ?? "unknown",
1191
+ writeSet: Array.isArray(writer.writeSet)
1192
+ ? writer.writeSet.filter((v) => typeof v === "string")
1193
+ : [],
1194
+ }))
1195
+ : derivedFacts.writersWriteSets;
1196
+ const packetShellVerification = Array.isArray(reviewPacket.shellVerification)
1197
+ ? reviewPacket.shellVerification.length > 0
1198
+ : derivedFacts.hasStructuredVerification;
1199
+ const eligibility = {
1200
+ writersWriteSets: packetWriters,
1201
+ allowedPaths,
1202
+ forbiddenPaths,
1203
+ broadWriteSetRisk: typeof reviewPacket.broadWriteSetRisk === "boolean"
1204
+ ? reviewPacket.broadWriteSetRisk
1205
+ : derivedFacts.broadWriteSetRisk,
1206
+ forbiddenOverlapRisk: typeof reviewPacket.forbiddenOverlapRisk === "boolean"
1207
+ ? reviewPacket.forbiddenOverlapRisk
1208
+ : derivedFacts.forbiddenOverlapRisk,
1209
+ hasStructuredVerification: packetShellVerification,
1210
+ };
1211
+ const assessment = assessAutonomousExecutionEligibility(eligibility);
1212
+ if (!assessment.ok) {
1213
+ return {
1214
+ kind: "error",
1215
+ status: 403,
1216
+ body: operatorFailed({
1217
+ command: action,
1218
+ outcome: "blocked",
1219
+ code: "HUMAN_CONFIRMATION_REQUIRED",
1220
+ message: assessment.reason,
1221
+ details: {
1222
+ taskId,
1223
+ dagSha256: finalStaged.sha256,
1224
+ eligibility: "denied",
1225
+ },
1226
+ }),
1227
+ };
1228
+ }
1229
+ const prepared = await ctx.dagExecutionReceipts.prepare({
1230
+ operatorSessionId: ctx.operatorSessionId,
1231
+ taskId,
1232
+ dagHandle: "staged-dag.json",
1233
+ dagBytes: dagText,
1234
+ dagBytesPath: finalStaged.path,
1235
+ sourceBindingSha256,
1236
+ taskContractBinding,
1237
+ controllerFingerprint: controller,
1238
+ validationResultSha256,
1239
+ eligibility,
1240
+ reviewPacket: {
1241
+ taskId,
1242
+ dagHandle: "staged-dag.json",
1243
+ validationOk: true,
1244
+ ...(writeSetGateToken ? { writeSetGateToken } : {}),
1245
+ },
1246
+ });
1247
+ if (!prepared.ok) {
1248
+ return {
1249
+ kind: "error",
1250
+ status: 403,
1251
+ body: operatorFailed({
1252
+ command: action,
1253
+ outcome: "blocked",
1254
+ code: prepared.code,
1255
+ message: prepared.reason,
1256
+ }),
1257
+ };
1258
+ }
1259
+ return {
1260
+ kind: "sync",
1261
+ status: 200,
1262
+ body: operatorSucceeded("prepareDagExecution", {
1263
+ executionId: prepared.receipt.executionId,
1264
+ eligibility: "approved",
1265
+ taskId,
1266
+ dagSha256: finalStaged.sha256,
1267
+ expiresAt: prepared.receipt.expiresAt,
1268
+ }),
1269
+ };
1270
+ }
892
1271
  case "confirmDagConfirmation": {
893
1272
  const confirmationId = str(p.confirmationId);
894
1273
  const humanGateToken = p.humanGateToken;
@@ -1110,14 +1489,118 @@ export async function dispatchOperatorAction(ctx, req) {
1110
1489
  });
1111
1490
  }
1112
1491
  case "runDag": {
1113
- // Only confirmationId — never raw dag path from browser
1114
- const confirmationId = str(p.confirmationId);
1492
+ // Model path (AC-02): only executionId — never a raw dag path from
1493
+ // the model. The browser Human Gate route (chat human-gate dag-run)
1494
+ // still dispatches with confirmationId for the preserved global
1495
+ // Human Gate flow; that path is not model-reachable (the confirmation
1496
+ // must be in "confirmed" state, which requires a server-signed
1497
+ // human-gate token verified by confirmDagConfirmation).
1115
1498
  const clientRequestId = str(req.clientRequestId);
1116
- if (!confirmationId || !clientRequestId) {
1117
- return invalid(action, "runDag requires confirmationId and clientRequestId only");
1499
+ const executionId = str(p.executionId);
1500
+ if (!clientRequestId) {
1501
+ return invalid(action, "runDag requires clientRequestId");
1118
1502
  }
1119
- const conf = await ctx.confirmations.get(confirmationId);
1120
- if (!conf) {
1503
+ if (executionId && str(p.confirmationId)) {
1504
+ return invalid(action, "runDag accepts either executionId or confirmationId, not both");
1505
+ }
1506
+ if (!executionId) {
1507
+ const confirmationId = str(p.confirmationId);
1508
+ if (!confirmationId) {
1509
+ return invalid(action, "runDag requires executionId (autonomous) or confirmationId (browser Human Gate)");
1510
+ }
1511
+ // Preserved browser Human Gate flow (dag-confirmation.ts untouched).
1512
+ const conf = await ctx.confirmations.get(confirmationId);
1513
+ if (!conf) {
1514
+ return {
1515
+ kind: "error",
1516
+ status: 404,
1517
+ body: operatorFailed({
1518
+ command: action,
1519
+ outcome: "not-found",
1520
+ code: "NOT_FOUND",
1521
+ message: `confirmation not found: ${confirmationId}`,
1522
+ }),
1523
+ };
1524
+ }
1525
+ // Re-read staged DAG bytes and hash.
1526
+ let dagBytes;
1527
+ try {
1528
+ dagBytes = await readFile(conf.dagBytesPath, "utf8");
1529
+ }
1530
+ catch (error) {
1531
+ return {
1532
+ kind: "error",
1533
+ status: 409,
1534
+ body: operatorFailed({
1535
+ command: action,
1536
+ outcome: "conflict",
1537
+ code: "CONFIRMATION_STALE",
1538
+ message: `cannot re-read staged DAG bytes: ${error instanceof Error ? error.message : String(error)}`,
1539
+ }),
1540
+ };
1541
+ }
1542
+ const dagSha256 = hashDagBytes(dagBytes);
1543
+ const controller = ctx.client.getIdentity?.()?.packageFingerprint?.value ??
1544
+ conf.controllerFingerprint;
1545
+ const consumed = await ctx.confirmations.consume({
1546
+ confirmationId,
1547
+ operatorSessionId: ctx.operatorSessionId,
1548
+ current: {
1549
+ dagSha256,
1550
+ sourceBindingSha256: conf.sourceBindingSha256,
1551
+ taskContractBinding: conf.taskContractBinding,
1552
+ controllerFingerprint: controller,
1553
+ validationResultSha256: conf.validationResultSha256,
1554
+ },
1555
+ });
1556
+ if (!consumed.ok) {
1557
+ return {
1558
+ kind: "error",
1559
+ status: confirmationErrorStatus(consumed.code),
1560
+ body: operatorFailed({
1561
+ command: action,
1562
+ outcome: "rejected",
1563
+ code: consumed.code,
1564
+ message: consumed.message,
1565
+ }),
1566
+ };
1567
+ }
1568
+ const taskId = conf.taskContractBinding.taskId;
1569
+ const gateToken = readWriteSetGateToken(conf.reviewPacket);
1570
+ if (gateToken) {
1571
+ return acceptOperation(ctx, {
1572
+ action,
1573
+ actionParams: { confirmationId },
1574
+ clientRequestId,
1575
+ taskId,
1576
+ cliArgs: [
1577
+ "task",
1578
+ "advance",
1579
+ taskId,
1580
+ "--approve-gate",
1581
+ gateToken,
1582
+ "--json",
1583
+ ],
1584
+ });
1585
+ }
1586
+ return acceptOperation(ctx, {
1587
+ action,
1588
+ actionParams: { confirmationId },
1589
+ clientRequestId,
1590
+ taskId,
1591
+ cliArgs: [
1592
+ "dag",
1593
+ "execute",
1594
+ "--dag",
1595
+ conf.dagBytesPath,
1596
+ "--cwd",
1597
+ ctx.repoRoot,
1598
+ "--json",
1599
+ ],
1600
+ });
1601
+ }
1602
+ const receipt = await ctx.dagExecutionReceipts.get(executionId);
1603
+ if (!receipt) {
1121
1604
  return {
1122
1605
  kind: "error",
1123
1606
  status: 404,
@@ -1125,40 +1608,45 @@ export async function dispatchOperatorAction(ctx, req) {
1125
1608
  command: action,
1126
1609
  outcome: "not-found",
1127
1610
  code: "NOT_FOUND",
1128
- message: `confirmation not found: ${confirmationId}`,
1611
+ message: `execution receipt not found: ${executionId}`,
1129
1612
  }),
1130
1613
  };
1131
1614
  }
1132
- // Re-read staged DAG bytes and hash
1133
- let dagBytes;
1615
+ const taskId = receipt.taskId;
1616
+ // Live server-derived bindings only; any drift fails closed inside
1617
+ // consume (staged bytes/hash + controller + task binding + source).
1618
+ const controller = ctx.client.getIdentity?.()?.packageFingerprint?.value ??
1619
+ receipt.controllerFingerprint;
1620
+ let sourceBindingSha256 = receipt.sourceBindingSha256;
1134
1621
  try {
1135
- dagBytes = await readFile(conf.dagBytesPath, "utf8");
1622
+ const draftRec = await ctx.drafts.get(taskId);
1623
+ if (draftRec)
1624
+ sourceBindingSha256 = draftRec.draftSha256;
1136
1625
  }
1137
- catch (error) {
1626
+ catch {
1627
+ // keep the receipt binding; consume will still compare it
1628
+ }
1629
+ const liveBinding = await readLiveTaskContractBinding(ctx, taskId);
1630
+ if (!liveBinding) {
1138
1631
  return {
1139
1632
  kind: "error",
1140
1633
  status: 409,
1141
1634
  body: operatorFailed({
1142
1635
  command: action,
1143
- outcome: "conflict",
1144
- code: "CONFIRMATION_STALE",
1145
- message: `cannot re-read staged DAG bytes: ${error instanceof Error ? error.message : String(error)}`,
1636
+ outcome: "blocked",
1637
+ code: "BINDING_DRIFT",
1638
+ message: "managed Task Contract binding unavailable at consume time",
1639
+ details: { taskId },
1146
1640
  }),
1147
1641
  };
1148
1642
  }
1149
- const dagSha256 = hashDagBytes(dagBytes);
1150
- const controller = ctx.client.getIdentity?.()?.packageFingerprint?.value ??
1151
- conf.controllerFingerprint;
1152
- const consumed = await ctx.confirmations.consume({
1153
- confirmationId,
1643
+ const consumed = await ctx.dagExecutionReceipts.consume({
1644
+ executionId,
1154
1645
  operatorSessionId: ctx.operatorSessionId,
1155
- current: {
1156
- dagSha256,
1157
- sourceBindingSha256: conf.sourceBindingSha256,
1158
- taskContractBinding: conf.taskContractBinding,
1159
- controllerFingerprint: controller,
1160
- validationResultSha256: conf.validationResultSha256,
1161
- },
1646
+ controllerFingerprint: controller,
1647
+ taskContractBinding: liveBinding,
1648
+ sourceBindingSha256,
1649
+ validationResultSha256: receipt.validationResultSha256,
1162
1650
  });
1163
1651
  if (!consumed.ok) {
1164
1652
  return {
@@ -1172,41 +1660,48 @@ export async function dispatchOperatorAction(ctx, req) {
1172
1660
  }),
1173
1661
  };
1174
1662
  }
1175
- const taskId = conf.taskContractBinding.taskId;
1176
- const gateToken = readWriteSetGateToken(conf.reviewPacket);
1177
- if (gateToken) {
1178
- // Standard lifecycle path: human confirmation maps to writeSet gate approve.
1663
+ const gateToken = readWriteSetGateToken(receipt.reviewPacket);
1664
+ const dispatchRun = async () => {
1665
+ if (gateToken) {
1666
+ // Standard lifecycle path: the execution receipt replaces the
1667
+ // browser confirm; the gate token is still consumed by the CLI.
1668
+ return acceptOperation(ctx, {
1669
+ action,
1670
+ actionParams: { executionId },
1671
+ clientRequestId,
1672
+ taskId,
1673
+ cliArgs: [
1674
+ "task",
1675
+ "advance",
1676
+ taskId,
1677
+ "--approve-gate",
1678
+ gateToken,
1679
+ "--json",
1680
+ ],
1681
+ });
1682
+ }
1683
+ // Advanced: execute the server-staged DAG bytes (never a model path).
1179
1684
  return acceptOperation(ctx, {
1180
1685
  action,
1181
- actionParams: { confirmationId },
1686
+ actionParams: { executionId },
1182
1687
  clientRequestId,
1183
1688
  taskId,
1184
1689
  cliArgs: [
1185
- "task",
1186
- "advance",
1187
- taskId,
1188
- "--approve-gate",
1189
- gateToken,
1690
+ "dag",
1691
+ "execute",
1692
+ "--dag",
1693
+ receipt.dagBytesPath,
1694
+ "--cwd",
1695
+ ctx.repoRoot,
1190
1696
  "--json",
1191
1697
  ],
1192
1698
  });
1699
+ };
1700
+ const accepted = await dispatchRun();
1701
+ if (accepted.kind === "accepted") {
1702
+ await ctx.dagExecutionReceipts.markDispatched(executionId, accepted.body.operationId);
1193
1703
  }
1194
- // Advanced: arbitrary staged DagSpec (client-supplied dagText) until dag execute rename.
1195
- return acceptOperation(ctx, {
1196
- action,
1197
- actionParams: { confirmationId },
1198
- clientRequestId,
1199
- taskId,
1200
- cliArgs: [
1201
- "dag",
1202
- "execute",
1203
- "--dag",
1204
- conf.dagBytesPath,
1205
- "--cwd",
1206
- ctx.repoRoot,
1207
- "--json",
1208
- ],
1209
- });
1704
+ return accepted;
1210
1705
  }
1211
1706
  case "dagRerunPlan": {
1212
1707
  const runId = str(p.runId);
@@ -1263,11 +1758,11 @@ export async function dispatchOperatorAction(ctx, req) {
1263
1758
  if (!runId || !reason || !clientRequestId) {
1264
1759
  return invalid(action, "runId, reason, and clientRequestId are required");
1265
1760
  }
1266
- const gated = await consumeMutationGateReceipt(ctx, action, p);
1761
+ // 2026-08-11: read-only run-facts check replaces the browser Human
1762
+ // Gate (R2). Fails closed when evidence is missing.
1763
+ const gated = await assertAutonomousRerunEligible(ctx, runId, str(p.taskId));
1267
1764
  if (gated.kind === "error")
1268
1765
  return gated;
1269
- if (gated.kind === "idempotent")
1270
- return gated.result;
1271
1766
  const profile = str(p.profile);
1272
1767
  const taskId = str(p.taskId);
1273
1768
  const cliArgs = [
@@ -1285,15 +1780,13 @@ export async function dispatchOperatorAction(ctx, req) {
1285
1780
  cliArgs.push("--profile", profile);
1286
1781
  if (taskId)
1287
1782
  cliArgs.push("--task-id", taskId);
1288
- const accepted = await acceptOperation(ctx, {
1783
+ return acceptOperation(ctx, {
1289
1784
  action,
1290
1785
  actionParams: p,
1291
1786
  clientRequestId,
1292
1787
  taskId,
1293
1788
  cliArgs,
1294
1789
  });
1295
- await finalizeMutationGateReceipt(ctx, gated.receiptId, accepted);
1296
- return accepted;
1297
1790
  }
1298
1791
  case "workerTaskRetry": {
1299
1792
  const taskId = str(p.taskId);
@@ -1303,11 +1796,11 @@ export async function dispatchOperatorAction(ctx, req) {
1303
1796
  if (!taskId || !reason || !clientRequestId) {
1304
1797
  return invalid(action, "taskId, reason, and clientRequestId are required");
1305
1798
  }
1306
- const gated = await consumeMutationGateReceipt(ctx, action, p);
1799
+ // 2026-08-11: read-only pool facts check replaces the browser Human
1800
+ // Gate (task must exist in Failed state; featureId consistent).
1801
+ const gated = await assertWorkerTaskRetryFacts(ctx, taskId, featureId);
1307
1802
  if (gated.kind === "error")
1308
1803
  return gated;
1309
- if (gated.kind === "idempotent")
1310
- return gated.result;
1311
1804
  const args = [
1312
1805
  "task",
1313
1806
  "retry",
@@ -1319,7 +1812,7 @@ export async function dispatchOperatorAction(ctx, req) {
1319
1812
  ];
1320
1813
  if (featureId)
1321
1814
  args.push("--feature-id", featureId);
1322
- const accepted = await acceptOperation(ctx, {
1815
+ return acceptOperation(ctx, {
1323
1816
  action,
1324
1817
  actionParams: p,
1325
1818
  clientRequestId,
@@ -1327,8 +1820,6 @@ export async function dispatchOperatorAction(ctx, req) {
1327
1820
  cliArgs: args,
1328
1821
  externalCommand: resolveSiblingAgentWorkerBin(),
1329
1822
  });
1330
- await finalizeMutationGateReceipt(ctx, gated.receiptId, accepted);
1331
- return accepted;
1332
1823
  }
1333
1824
  case "workerAdmissionPrepare": {
1334
1825
  const featureDir = str(p.featureDir);
@@ -1510,7 +2001,7 @@ export async function dispatchOperatorAction(ctx, req) {
1510
2001
  return invalid(action, "action and actionParams are required");
1511
2002
  }
1512
2003
  if (!MUTATION_GATE_ACTIONS.has(targetAction)) {
1513
- return invalid(action, "action must be a mutation-gate target (standaloneTaskRerun | workerTaskRetry | workerAdmissionPrepare | workerSchedulerAdd | workerSchedulerCancel | workerSchedulerHarvest | workerSchedulerDiscard)");
2004
+ return invalid(action, "action must be a mutation-gate target (workerAdmissionPrepare | workerSchedulerAdd | workerSchedulerCancel | workerSchedulerHarvest | workerSchedulerDiscard)");
1514
2005
  }
1515
2006
  const secret = ctx.humanGateSecret;
1516
2007
  if (!secret) {