@tea-agent/loop-agent 0.33.6 → 0.33.7-beta.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 (69) hide show
  1. package/CHANGELOG.md +23 -23
  2. package/dist/application/task-lifecycle/advance.js +4 -254
  3. package/dist/application/task-lifecycle/gates.js +0 -50
  4. package/dist/application/task-lifecycle/observe.js +2 -11
  5. package/dist/commands/init-upgrade.js +1 -32
  6. package/dist/commands/init.js +3 -94
  7. package/dist/executors/shell-executor.js +91 -4
  8. package/dist/executors/shell-write-guard.js +8 -26
  9. package/dist/shared/operator/capabilities.js +42 -72
  10. package/dist/task/source-prepare/index.js +0 -2
  11. package/dist/task/source-prepare/parse-intent.js +10 -58
  12. package/dist/task/source-prepare/prepare.js +16 -180
  13. package/dist/task/source-prepare/reference-integrity.js +2 -18
  14. package/dist/worker/console/app-data.js +0 -2
  15. package/dist/worker/console/chat/chat-event-store.js +25 -190
  16. package/dist/worker/console/chat/instruction-skills.js +217 -0
  17. package/dist/worker/console/chat/pi-console-config.js +32 -250
  18. package/dist/worker/console/chat/pi-runtime.js +71 -625
  19. package/dist/worker/console/chat/resource-loader.js +4 -5
  20. package/dist/worker/console/chat/routes.js +146 -324
  21. package/dist/worker/console/chat/runtime-context.js +12 -48
  22. package/dist/worker/console/chat/runtime-selection.js +0 -59
  23. package/dist/worker/console/chat/shortcuts.js +0 -1
  24. package/dist/worker/console/chat/tool-adapter.js +3 -9
  25. package/dist/worker/console/chat/tools.js +1 -5
  26. package/dist/worker/console/operator-actions.js +68 -559
  27. package/dist/worker/console/server.js +15 -8
  28. package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
  29. package/dist/worker/console/static/assets/index-CteJFFL2.js +29 -0
  30. package/dist/worker/console/static/index.html +2 -2
  31. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +8 -45
  32. package/dist/worker/console/static-src/operator-chat/refs.js +0 -9
  33. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -16
  34. package/dist/worker/console/static-src/operator-chat/useChatStream.js +184 -210
  35. package/dist/worker/console/static-src/operator-chat/useChatThread.js +5 -49
  36. package/dist/worker/console/static-src/operator-chat/useComposer.js +0 -17
  37. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +74 -225
  38. package/dist/worker/delivery/final-verification.js +5 -13
  39. package/dist/worker/delivery/package.js +19 -31
  40. package/dist/worker/delivery/verification-bundle.js +4 -6
  41. package/dist/worker/observe/static/operator-chrome.css +2 -5
  42. package/dist/worker/observe/static/operator-chrome.js +1 -6
  43. package/dist/worker/observe/static/styles.css +9 -39
  44. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +462 -33
  45. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  46. package/dist/workflows/dag/backend-test-markdown-workflow.js +25 -1
  47. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  48. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  49. package/dist/workflows/dag/backend-test-scenario-param.js +269 -82
  50. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  51. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  52. package/dist/workflows/dag/frontend-worktree-diff.js +27 -12
  53. package/dist/workflows/dag/init-hybrid.js +46 -34
  54. package/dist/workflows/dag/types.js +7 -0
  55. package/dist/workflows/dag/workspace-checkpoint.js +27 -8
  56. package/docs/templates/backend-test-dag.json +32 -29
  57. package/harness.json +1 -1
  58. package/package.json +1 -1
  59. package/skills/loop-agent/references/command-reference.md +1 -3
  60. package/skills/loop-agent/references/source-and-plan-practice.md +0 -13
  61. package/skills/loop-agent/references/task-workflow.md +0 -4
  62. package/dist/shared/resilient-git.js +0 -133
  63. package/dist/task/source-prepare/artifact-meta.js +0 -137
  64. package/dist/task/source-prepare/semantic-intake.js +0 -404
  65. package/dist/worker/console/dag-execution-receipt.js +0 -380
  66. package/dist/worker/console/static/assets/index-BUOLppPr.js +0 -28
  67. package/dist/worker/console/static/assets/index-C1KzazY5.css +0 -1
  68. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +0 -257
  69. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +0 -196
@@ -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, loadTaskConfig } from "../../task/runtime.js";
4
+ import { getTaskPaths } 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,12 +19,10 @@ 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";
23
22
  import { issueHumanGateToken, mutationGatePayloadHash, verifyHumanGateToken, } from "./human-gate-token.js";
24
23
  const MUTATION_GATE_ACTIONS = new Set([
25
- // 2026-08-11: standaloneTaskRerun / workerTaskRetry are autonomous (server
26
- // run-facts checks); only Night Scheduler mutations still need the browser
27
- // Human Gate.
24
+ "standaloneTaskRerun",
25
+ "workerTaskRetry",
28
26
  "workerAdmissionPrepare",
29
27
  "workerSchedulerAdd",
30
28
  "workerSchedulerCancel",
@@ -74,150 +72,6 @@ function readWriteSetGateToken(reviewPacket) {
74
72
  const token = reviewPacket.writeSetGateToken;
75
73
  return typeof token === "string" && token.includes(":") ? token : undefined;
76
74
  }
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
- }
221
75
  /**
222
76
  * Prefer structured stdout JSON failure semantics (findings / message) over bare
223
77
  * `exit N` when stderr is empty — spine audit and similar read CLIs write details
@@ -1035,239 +889,6 @@ export async function dispatchOperatorAction(ctx, req) {
1035
889
  }),
1036
890
  };
1037
891
  }
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
- }
1271
892
  case "confirmDagConfirmation": {
1272
893
  const confirmationId = str(p.confirmationId);
1273
894
  const humanGateToken = p.humanGateToken;
@@ -1489,118 +1110,14 @@ export async function dispatchOperatorAction(ctx, req) {
1489
1110
  });
1490
1111
  }
1491
1112
  case "runDag": {
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).
1113
+ // Only confirmationId — never raw dag path from browser
1114
+ const confirmationId = str(p.confirmationId);
1498
1115
  const clientRequestId = str(req.clientRequestId);
1499
- const executionId = str(p.executionId);
1500
- if (!clientRequestId) {
1501
- return invalid(action, "runDag requires clientRequestId");
1502
- }
1503
- if (executionId && str(p.confirmationId)) {
1504
- return invalid(action, "runDag accepts either executionId or confirmationId, not both");
1116
+ if (!confirmationId || !clientRequestId) {
1117
+ return invalid(action, "runDag requires confirmationId and clientRequestId only");
1505
1118
  }
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) {
1119
+ const conf = await ctx.confirmations.get(confirmationId);
1120
+ if (!conf) {
1604
1121
  return {
1605
1122
  kind: "error",
1606
1123
  status: 404,
@@ -1608,45 +1125,40 @@ export async function dispatchOperatorAction(ctx, req) {
1608
1125
  command: action,
1609
1126
  outcome: "not-found",
1610
1127
  code: "NOT_FOUND",
1611
- message: `execution receipt not found: ${executionId}`,
1128
+ message: `confirmation not found: ${confirmationId}`,
1612
1129
  }),
1613
1130
  };
1614
1131
  }
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;
1132
+ // Re-read staged DAG bytes and hash
1133
+ let dagBytes;
1621
1134
  try {
1622
- const draftRec = await ctx.drafts.get(taskId);
1623
- if (draftRec)
1624
- sourceBindingSha256 = draftRec.draftSha256;
1625
- }
1626
- catch {
1627
- // keep the receipt binding; consume will still compare it
1135
+ dagBytes = await readFile(conf.dagBytesPath, "utf8");
1628
1136
  }
1629
- const liveBinding = await readLiveTaskContractBinding(ctx, taskId);
1630
- if (!liveBinding) {
1137
+ catch (error) {
1631
1138
  return {
1632
1139
  kind: "error",
1633
1140
  status: 409,
1634
1141
  body: operatorFailed({
1635
1142
  command: action,
1636
- outcome: "blocked",
1637
- code: "BINDING_DRIFT",
1638
- message: "managed Task Contract binding unavailable at consume time",
1639
- details: { taskId },
1143
+ outcome: "conflict",
1144
+ code: "CONFIRMATION_STALE",
1145
+ message: `cannot re-read staged DAG bytes: ${error instanceof Error ? error.message : String(error)}`,
1640
1146
  }),
1641
1147
  };
1642
1148
  }
1643
- const consumed = await ctx.dagExecutionReceipts.consume({
1644
- executionId,
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,
1645
1154
  operatorSessionId: ctx.operatorSessionId,
1646
- controllerFingerprint: controller,
1647
- taskContractBinding: liveBinding,
1648
- sourceBindingSha256,
1649
- validationResultSha256: receipt.validationResultSha256,
1155
+ current: {
1156
+ dagSha256,
1157
+ sourceBindingSha256: conf.sourceBindingSha256,
1158
+ taskContractBinding: conf.taskContractBinding,
1159
+ controllerFingerprint: controller,
1160
+ validationResultSha256: conf.validationResultSha256,
1161
+ },
1650
1162
  });
1651
1163
  if (!consumed.ok) {
1652
1164
  return {
@@ -1660,48 +1172,41 @@ export async function dispatchOperatorAction(ctx, req) {
1660
1172
  }),
1661
1173
  };
1662
1174
  }
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).
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.
1684
1179
  return acceptOperation(ctx, {
1685
1180
  action,
1686
- actionParams: { executionId },
1181
+ actionParams: { confirmationId },
1687
1182
  clientRequestId,
1688
1183
  taskId,
1689
1184
  cliArgs: [
1690
- "dag",
1691
- "execute",
1692
- "--dag",
1693
- receipt.dagBytesPath,
1694
- "--cwd",
1695
- ctx.repoRoot,
1185
+ "task",
1186
+ "advance",
1187
+ taskId,
1188
+ "--approve-gate",
1189
+ gateToken,
1696
1190
  "--json",
1697
1191
  ],
1698
1192
  });
1699
- };
1700
- const accepted = await dispatchRun();
1701
- if (accepted.kind === "accepted") {
1702
- await ctx.dagExecutionReceipts.markDispatched(executionId, accepted.body.operationId);
1703
1193
  }
1704
- return accepted;
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
+ });
1705
1210
  }
1706
1211
  case "dagRerunPlan": {
1707
1212
  const runId = str(p.runId);
@@ -1758,11 +1263,11 @@ export async function dispatchOperatorAction(ctx, req) {
1758
1263
  if (!runId || !reason || !clientRequestId) {
1759
1264
  return invalid(action, "runId, reason, and clientRequestId are required");
1760
1265
  }
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));
1266
+ const gated = await consumeMutationGateReceipt(ctx, action, p);
1764
1267
  if (gated.kind === "error")
1765
1268
  return gated;
1269
+ if (gated.kind === "idempotent")
1270
+ return gated.result;
1766
1271
  const profile = str(p.profile);
1767
1272
  const taskId = str(p.taskId);
1768
1273
  const cliArgs = [
@@ -1780,13 +1285,15 @@ export async function dispatchOperatorAction(ctx, req) {
1780
1285
  cliArgs.push("--profile", profile);
1781
1286
  if (taskId)
1782
1287
  cliArgs.push("--task-id", taskId);
1783
- return acceptOperation(ctx, {
1288
+ const accepted = await acceptOperation(ctx, {
1784
1289
  action,
1785
1290
  actionParams: p,
1786
1291
  clientRequestId,
1787
1292
  taskId,
1788
1293
  cliArgs,
1789
1294
  });
1295
+ await finalizeMutationGateReceipt(ctx, gated.receiptId, accepted);
1296
+ return accepted;
1790
1297
  }
1791
1298
  case "workerTaskRetry": {
1792
1299
  const taskId = str(p.taskId);
@@ -1796,11 +1303,11 @@ export async function dispatchOperatorAction(ctx, req) {
1796
1303
  if (!taskId || !reason || !clientRequestId) {
1797
1304
  return invalid(action, "taskId, reason, and clientRequestId are required");
1798
1305
  }
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);
1306
+ const gated = await consumeMutationGateReceipt(ctx, action, p);
1802
1307
  if (gated.kind === "error")
1803
1308
  return gated;
1309
+ if (gated.kind === "idempotent")
1310
+ return gated.result;
1804
1311
  const args = [
1805
1312
  "task",
1806
1313
  "retry",
@@ -1812,7 +1319,7 @@ export async function dispatchOperatorAction(ctx, req) {
1812
1319
  ];
1813
1320
  if (featureId)
1814
1321
  args.push("--feature-id", featureId);
1815
- return acceptOperation(ctx, {
1322
+ const accepted = await acceptOperation(ctx, {
1816
1323
  action,
1817
1324
  actionParams: p,
1818
1325
  clientRequestId,
@@ -1820,6 +1327,8 @@ export async function dispatchOperatorAction(ctx, req) {
1820
1327
  cliArgs: args,
1821
1328
  externalCommand: resolveSiblingAgentWorkerBin(),
1822
1329
  });
1330
+ await finalizeMutationGateReceipt(ctx, gated.receiptId, accepted);
1331
+ return accepted;
1823
1332
  }
1824
1333
  case "workerAdmissionPrepare": {
1825
1334
  const featureDir = str(p.featureDir);
@@ -2001,7 +1510,7 @@ export async function dispatchOperatorAction(ctx, req) {
2001
1510
  return invalid(action, "action and actionParams are required");
2002
1511
  }
2003
1512
  if (!MUTATION_GATE_ACTIONS.has(targetAction)) {
2004
- return invalid(action, "action must be a mutation-gate target (workerAdmissionPrepare | workerSchedulerAdd | workerSchedulerCancel | workerSchedulerHarvest | workerSchedulerDiscard)");
1513
+ return invalid(action, "action must be a mutation-gate target (standaloneTaskRerun | workerTaskRetry | workerAdmissionPrepare | workerSchedulerAdd | workerSchedulerCancel | workerSchedulerHarvest | workerSchedulerDiscard)");
2005
1514
  }
2006
1515
  const secret = ctx.humanGateSecret;
2007
1516
  if (!secret) {