@tea-agent/loop-agent 0.33.5 → 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.
- package/CHANGELOG.md +33 -0
- package/dist/application/task-lifecycle/advance.js +254 -4
- package/dist/application/task-lifecycle/gates.js +50 -0
- package/dist/application/task-lifecycle/observe.js +11 -2
- package/dist/commands/init-upgrade.js +32 -1
- package/dist/commands/init.js +94 -3
- package/dist/executors/shell-write-guard.js +26 -8
- package/dist/shared/operator/capabilities.js +72 -42
- package/dist/shared/resilient-git.js +133 -0
- package/dist/task/source-prepare/artifact-meta.js +137 -0
- package/dist/task/source-prepare/index.js +2 -0
- package/dist/task/source-prepare/parse-intent.js +58 -10
- package/dist/task/source-prepare/prepare.js +180 -16
- package/dist/task/source-prepare/reference-integrity.js +18 -2
- package/dist/task/source-prepare/semantic-intake.js +404 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/chat-event-store.js +190 -25
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +625 -71
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +324 -146
- package/dist/worker/console/chat/runtime-context.js +48 -12
- package/dist/worker/console/chat/runtime-selection.js +59 -0
- package/dist/worker/console/chat/shortcuts.js +1 -0
- package/dist/worker/console/chat/tool-adapter.js +9 -3
- package/dist/worker/console/chat/tools.js +5 -1
- package/dist/worker/console/dag-execution-receipt.js +380 -0
- package/dist/worker/console/operator-actions.js +559 -68
- package/dist/worker/console/server.js +8 -15
- package/dist/worker/console/static/assets/index-BUOLppPr.js +28 -0
- package/dist/worker/console/static/assets/index-C1KzazY5.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
- package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +16 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +210 -184
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
- package/dist/worker/delivery/final-verification.js +13 -5
- package/dist/worker/delivery/package.js +31 -19
- package/dist/worker/delivery/verification-bundle.js +6 -4
- package/dist/worker/observe/static/operator-chrome.css +5 -2
- package/dist/worker/observe/static/operator-chrome.js +6 -1
- package/dist/worker/observe/static/styles.css +39 -9
- package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +3 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
- package/skills/loop-agent/references/task-workflow.md +4 -0
- package/dist/worker/console/chat/instruction-skills.js +0 -217
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
- 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
|
-
|
|
25
|
-
|
|
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
|
-
//
|
|
1114
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
1499
|
+
const executionId = str(p.executionId);
|
|
1500
|
+
if (!clientRequestId) {
|
|
1501
|
+
return invalid(action, "runDag requires clientRequestId");
|
|
1118
1502
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
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: `
|
|
1611
|
+
message: `execution receipt not found: ${executionId}`,
|
|
1129
1612
|
}),
|
|
1130
1613
|
};
|
|
1131
1614
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
-
|
|
1622
|
+
const draftRec = await ctx.drafts.get(taskId);
|
|
1623
|
+
if (draftRec)
|
|
1624
|
+
sourceBindingSha256 = draftRec.draftSha256;
|
|
1136
1625
|
}
|
|
1137
|
-
catch
|
|
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: "
|
|
1144
|
-
code: "
|
|
1145
|
-
message:
|
|
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
|
|
1150
|
-
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
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: {
|
|
1686
|
+
actionParams: { executionId },
|
|
1182
1687
|
clientRequestId,
|
|
1183
1688
|
taskId,
|
|
1184
1689
|
cliArgs: [
|
|
1185
|
-
"
|
|
1186
|
-
"
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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) {
|