@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.
- package/CHANGELOG.md +23 -23
- package/dist/application/task-lifecycle/advance.js +4 -254
- package/dist/application/task-lifecycle/gates.js +0 -50
- package/dist/application/task-lifecycle/observe.js +2 -11
- package/dist/commands/init-upgrade.js +1 -32
- package/dist/commands/init.js +3 -94
- package/dist/executors/shell-executor.js +91 -4
- package/dist/executors/shell-write-guard.js +8 -26
- package/dist/shared/operator/capabilities.js +42 -72
- package/dist/task/source-prepare/index.js +0 -2
- package/dist/task/source-prepare/parse-intent.js +10 -58
- package/dist/task/source-prepare/prepare.js +16 -180
- package/dist/task/source-prepare/reference-integrity.js +2 -18
- package/dist/worker/console/app-data.js +0 -2
- package/dist/worker/console/chat/chat-event-store.js +25 -190
- package/dist/worker/console/chat/instruction-skills.js +217 -0
- package/dist/worker/console/chat/pi-console-config.js +32 -250
- package/dist/worker/console/chat/pi-runtime.js +71 -625
- package/dist/worker/console/chat/resource-loader.js +4 -5
- package/dist/worker/console/chat/routes.js +146 -324
- package/dist/worker/console/chat/runtime-context.js +12 -48
- package/dist/worker/console/chat/runtime-selection.js +0 -59
- package/dist/worker/console/chat/shortcuts.js +0 -1
- package/dist/worker/console/chat/tool-adapter.js +3 -9
- package/dist/worker/console/chat/tools.js +1 -5
- package/dist/worker/console/operator-actions.js +68 -559
- package/dist/worker/console/server.js +15 -8
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
- package/dist/worker/console/static/assets/index-CteJFFL2.js +29 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +8 -45
- package/dist/worker/console/static-src/operator-chat/refs.js +0 -9
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -16
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +184 -210
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +5 -49
- package/dist/worker/console/static-src/operator-chat/useComposer.js +0 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +74 -225
- package/dist/worker/delivery/final-verification.js +5 -13
- package/dist/worker/delivery/package.js +19 -31
- package/dist/worker/delivery/verification-bundle.js +4 -6
- package/dist/worker/observe/static/operator-chrome.css +2 -5
- package/dist/worker/observe/static/operator-chrome.js +1 -6
- package/dist/worker/observe/static/styles.css +9 -39
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +462 -33
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +25 -1
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
- package/dist/workflows/dag/backend-test-scenario-param.js +269 -82
- package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/frontend-worktree-diff.js +27 -12
- package/dist/workflows/dag/init-hybrid.js +46 -34
- package/dist/workflows/dag/types.js +7 -0
- package/dist/workflows/dag/workspace-checkpoint.js +27 -8
- package/docs/templates/backend-test-dag.json +32 -29
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -3
- package/skills/loop-agent/references/source-and-plan-practice.md +0 -13
- package/skills/loop-agent/references/task-workflow.md +0 -4
- package/dist/shared/resilient-git.js +0 -133
- package/dist/task/source-prepare/artifact-meta.js +0 -137
- package/dist/task/source-prepare/semantic-intake.js +0 -404
- package/dist/worker/console/dag-execution-receipt.js +0 -380
- package/dist/worker/console/static/assets/index-BUOLppPr.js +0 -28
- package/dist/worker/console/static/assets/index-C1KzazY5.css +0 -1
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +0 -257
- 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
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
//
|
|
1493
|
-
|
|
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
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
1507
|
-
|
|
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: `
|
|
1128
|
+
message: `confirmation not found: ${confirmationId}`,
|
|
1612
1129
|
}),
|
|
1613
1130
|
};
|
|
1614
1131
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
1637
|
-
code: "
|
|
1638
|
-
message:
|
|
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
|
|
1644
|
-
|
|
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
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
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
|
|
1664
|
-
const
|
|
1665
|
-
|
|
1666
|
-
|
|
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: {
|
|
1181
|
+
actionParams: { confirmationId },
|
|
1687
1182
|
clientRequestId,
|
|
1688
1183
|
taskId,
|
|
1689
1184
|
cliArgs: [
|
|
1690
|
-
"
|
|
1691
|
-
"
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|