@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.1
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 +67 -16
- package/dist/application/task-lifecycle/advance.js +309 -9
- package/dist/application/task-lifecycle/gates.js +50 -0
- package/dist/application/task-lifecycle/observe.js +11 -2
- package/dist/application/task-lifecycle/plan-transitions.js +13 -21
- package/dist/commands/init-upgrade.js +32 -1
- package/dist/commands/init.js +94 -3
- package/dist/executors/dag-pi-executor.js +18 -3
- package/dist/executors/shell-executor.js +4 -91
- package/dist/executors/shell-write-guard.js +26 -8
- package/dist/shared/operator/capabilities.js +98 -44
- 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 +229 -18
- 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/model-resolver.js +17 -0
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +1007 -188
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +495 -157
- 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/session-store.js +39 -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/interview/grill-me.js +7 -6
- package/dist/worker/console/operator-actions.js +785 -85
- package/dist/worker/console/prd-intake-bridge.js +393 -0
- package/dist/worker/console/recovery-cta.js +35 -6
- package/dist/worker/console/server.js +8 -15
- package/dist/worker/console/static/assets/index-BQkhJpV8.css +1 -0
- package/dist/worker/console/static/assets/index-BpuHmlSP.js +29 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +1 -0
- package/dist/worker/console/static-src/app/usePrdImport.js +2 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +24 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +19 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +57 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
- package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
- 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/session-title-watcher.js +128 -0
- package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -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/useOverlayFocus.js +84 -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/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -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/observability/read-model.js +3 -0
- 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/backend-test-case-coverage-analysis.js +33 -462
- package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
- package/dist/workflows/dag/backend-test-module-stem.js +0 -5
- package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
- package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
- package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
- package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
- package/dist/workflows/dag/failure-category.js +3 -0
- package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
- package/dist/workflows/dag/init-hybrid.js +49 -55
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/retry-policy.js +44 -11
- package/dist/workflows/dag/runner.js +6 -0
- package/dist/workflows/dag/scheduler.js +49 -1
- package/dist/workflows/dag/types.js +0 -7
- package/dist/workflows/dag/validate.js +16 -2
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/docs/templates/agent-dag.schema.json +4 -4
- package/docs/templates/backend-test-dag.json +29 -32
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/local-jacoco-coverage/SKILL.md +281 -0
- package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
- package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
- package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
- package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
- 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";
|
|
@@ -16,13 +16,17 @@ import { applyGrillMeAnswer, draftSummary, emptyDraft, isDraftStructurallyComple
|
|
|
16
16
|
import { toCanonicalDraftForCli, } from "./draft-store.js";
|
|
17
17
|
import { normalizeConsoleWorkflowKind } from "./workflow-kinds.js";
|
|
18
18
|
import { deriveTaskIdentityFromPrd, nextTaskIdRevision, } from "./prd-identity.js";
|
|
19
|
+
import { appendEngineeringCliArgs, buildDraftFromTaskIntake, parseEngineeringBoundaryFromParams, } from "./prd-intake-bridge.js";
|
|
19
20
|
import { resolveSiblingAgentWorkerBin } from "./sibling-controller.js";
|
|
20
21
|
import { projectOperationEventSummary, projectOperationForChat, } from "./chat/chat-event-store.js";
|
|
21
22
|
import { MutationGateReceiptStore } from "./mutation-gate-receipt-store.js";
|
|
23
|
+
import { assessAutonomousExecutionEligibility, deriveEligibilityFactsFromDagSpec, } from "./dag-execution-receipt.js";
|
|
22
24
|
import { issueHumanGateToken, mutationGatePayloadHash, verifyHumanGateToken, } from "./human-gate-token.js";
|
|
23
25
|
const MUTATION_GATE_ACTIONS = new Set([
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
// 2026-08-11: standaloneTaskRerun / workerTaskRetry are autonomous (server
|
|
27
|
+
// run-facts checks). Destructive DAG reconciliation and Night Scheduler
|
|
28
|
+
// mutations still need the browser Human Gate.
|
|
29
|
+
"dagReconcileRun",
|
|
26
30
|
"workerAdmissionPrepare",
|
|
27
31
|
"workerSchedulerAdd",
|
|
28
32
|
"workerSchedulerCancel",
|
|
@@ -38,6 +42,7 @@ const MUTATION_OR_LONG = new Set([
|
|
|
38
42
|
"dagRunTask",
|
|
39
43
|
"runDag",
|
|
40
44
|
"dagRerun",
|
|
45
|
+
"dagReconcileRun",
|
|
41
46
|
"standaloneTaskRerun",
|
|
42
47
|
"workerTaskRetry",
|
|
43
48
|
]);
|
|
@@ -72,6 +77,150 @@ function readWriteSetGateToken(reviewPacket) {
|
|
|
72
77
|
const token = reviewPacket.writeSetGateToken;
|
|
73
78
|
return typeof token === "string" && token.includes(":") ? token : undefined;
|
|
74
79
|
}
|
|
80
|
+
/** Extract the review packet from a task advance --dag-output operator JSON. */
|
|
81
|
+
function extractReviewPacketFromCliJson(json) {
|
|
82
|
+
if (!json || typeof json !== "object")
|
|
83
|
+
return undefined;
|
|
84
|
+
const envelope = json;
|
|
85
|
+
const payload = envelope.result && typeof envelope.result === "object"
|
|
86
|
+
? envelope.result
|
|
87
|
+
: envelope;
|
|
88
|
+
const packet = payload.reviewPacket;
|
|
89
|
+
return packet && typeof packet === "object"
|
|
90
|
+
? packet
|
|
91
|
+
: undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Read the live managed Task Contract binding for a task (server-derived).
|
|
95
|
+
* Returns undefined when the task is not managed or status facts are missing
|
|
96
|
+
* so callers can fail closed.
|
|
97
|
+
*/
|
|
98
|
+
async function readLiveTaskContractBinding(ctx, taskId) {
|
|
99
|
+
const showBody = await runReadCli(ctx, ["task", "status", taskId, "--json"], "task status");
|
|
100
|
+
const statusResult = (showBody.result ?? {});
|
|
101
|
+
const c = showBody.ok ? statusResult.contract : undefined;
|
|
102
|
+
if (!c?.canonicalHash || typeof c.revision !== "number")
|
|
103
|
+
return undefined;
|
|
104
|
+
return {
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
taskId,
|
|
107
|
+
revision: c.revision,
|
|
108
|
+
canonicalHash: c.canonicalHash,
|
|
109
|
+
taskConfigSha256: c.taskConfigSha256 ?? undefined,
|
|
110
|
+
projectionVersion: c.projectionVersion ?? undefined,
|
|
111
|
+
canonicalizerVersion: c.canonicalizerVersion ?? undefined,
|
|
112
|
+
taskConfigSchemaVersion: c.taskConfigSchemaVersion ?? undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function gateError(action, code, message, details) {
|
|
116
|
+
const status = code === "NOT_FOUND" ? 404 : code === "INVALID_INPUT" ? 400 : 403;
|
|
117
|
+
return {
|
|
118
|
+
kind: "error",
|
|
119
|
+
status,
|
|
120
|
+
body: operatorFailed({
|
|
121
|
+
command: action,
|
|
122
|
+
outcome: "blocked",
|
|
123
|
+
code,
|
|
124
|
+
message,
|
|
125
|
+
details,
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 2026-08-11 R2 run-facts check for standaloneTaskRerun (read-only CLI
|
|
131
|
+
* evidence only — no browser Human Gate). Fails closed when facts are missing.
|
|
132
|
+
*/
|
|
133
|
+
async function assertAutonomousRerunEligible(ctx, runId, taskId) {
|
|
134
|
+
const report = await runReadCli(ctx, ["dag", "report", "--run-id", runId, "--json"], "dag report");
|
|
135
|
+
const reportResult = (report.ok ? report.result : undefined);
|
|
136
|
+
const recovery = reportResult?.primaryRecovery;
|
|
137
|
+
if (!report.ok || !reportResult?.primaryFailure || !recovery) {
|
|
138
|
+
return gateError("standaloneTaskRerun", "INVALID_INPUT", "cannot read dagReport run facts; standaloneTaskRerun requires server-side run evidence", { runId });
|
|
139
|
+
}
|
|
140
|
+
if (recovery.humanRequired === true) {
|
|
141
|
+
return gateError("standaloneTaskRerun", "HUMAN_CONFIRMATION_REQUIRED", `primaryRecovery requires a human decision (action=${recovery.action ?? "?"}); stop autonomous rerun`, { runId });
|
|
142
|
+
}
|
|
143
|
+
const nodeId = reportResult.primaryFailure.scope === "node"
|
|
144
|
+
? reportResult.primaryFailure.nodeId
|
|
145
|
+
: undefined;
|
|
146
|
+
let planEligible;
|
|
147
|
+
if (nodeId) {
|
|
148
|
+
const plan = await runReadCli(ctx, [
|
|
149
|
+
"dag",
|
|
150
|
+
"rerun",
|
|
151
|
+
"--run-id",
|
|
152
|
+
runId,
|
|
153
|
+
"--from-node",
|
|
154
|
+
nodeId,
|
|
155
|
+
"--plan",
|
|
156
|
+
"--json",
|
|
157
|
+
], "dag rerun plan");
|
|
158
|
+
const planResult = (plan.result ?? {});
|
|
159
|
+
if (plan.ok && planResult.eligible === true) {
|
|
160
|
+
return gateError("standaloneTaskRerun", "INVALID_INPUT", `node rerun plan is eligible (planHash=${planResult.planHash ?? "?"}); use dagRerun with the fresh planHash instead of standaloneTaskRerun`, { runId, nodeId });
|
|
161
|
+
}
|
|
162
|
+
if (!plan.ok && planResult.eligible === undefined) {
|
|
163
|
+
return gateError("standaloneTaskRerun", "INVALID_INPUT", "dagRerunPlan run facts unavailable; standaloneTaskRerun fails closed", { runId, nodeId });
|
|
164
|
+
}
|
|
165
|
+
planEligible = planResult.eligible;
|
|
166
|
+
}
|
|
167
|
+
if (planEligible !== false) {
|
|
168
|
+
// No ineligible-plan evidence: require an explicit rerun-task recommendation.
|
|
169
|
+
const recommendsRerunTask = recovery.action === "rerun-after-fix" ||
|
|
170
|
+
/rerun-task|standalone/i.test(recovery.commandHint ?? "");
|
|
171
|
+
if (!recommendsRerunTask) {
|
|
172
|
+
return gateError("standaloneTaskRerun", "HUMAN_CONFIRMATION_REQUIRED", `run facts do not support autonomous standaloneTaskRerun (recovery action=${recovery.action ?? "?"}); stop`, { runId });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (taskId) {
|
|
176
|
+
// Keep the original taskId: the run must still be bound to it.
|
|
177
|
+
const status = await runReadCli(ctx, ["task", "status", taskId, "--json"], "task status");
|
|
178
|
+
const statusResult = (status.ok ? status.result : undefined);
|
|
179
|
+
const boundRunId = statusResult?.activeRun?.runId ?? statusResult?.latestRun?.runId;
|
|
180
|
+
if (!status.ok || !boundRunId || boundRunId !== runId) {
|
|
181
|
+
return gateError("standaloneTaskRerun", "INVALID_INPUT", `taskId ${taskId} is not bound to run ${runId}; keep the original taskId for same-task rerun`, { runId, taskId, boundRunId });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { kind: "ready" };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 2026-08-11 read-only facts check for workerTaskRetry: the task must exist
|
|
188
|
+
* in the pool doctor inventory with status Failed (featureId consistent).
|
|
189
|
+
*/
|
|
190
|
+
async function assertWorkerTaskRetryFacts(ctx, taskId, featureId) {
|
|
191
|
+
const run = ctx.runCommand ??
|
|
192
|
+
((args, options) => ctx.client.runExternal
|
|
193
|
+
? ctx.client.runExternal(resolveSiblingAgentWorkerBin(), args, options)
|
|
194
|
+
: ctx.client.run(args, options));
|
|
195
|
+
let result;
|
|
196
|
+
try {
|
|
197
|
+
result = await run(["pool", "doctor", "--repo", ctx.repoRoot, "--json"], {
|
|
198
|
+
cwd: ctx.repoRoot,
|
|
199
|
+
artifactName: "worker-task-retry-facts",
|
|
200
|
+
expectJson: true,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
return gateError("workerTaskRetry", "INVALID_INPUT", `cannot read pool doctor facts: ${error instanceof Error ? error.message : String(error)}`);
|
|
205
|
+
}
|
|
206
|
+
if (!result.ok || result.exitCode !== 0 || !result.json) {
|
|
207
|
+
return gateError("workerTaskRetry", "INVALID_INPUT", "cannot read pool doctor run facts; workerTaskRetry fails closed");
|
|
208
|
+
}
|
|
209
|
+
const doctor = result.json;
|
|
210
|
+
const v2Entry = (doctor.v2 ?? []).find((entry) => entry.taskId === taskId);
|
|
211
|
+
const legacyEntry = (doctor.legacy ?? []).find((entry) => entry.taskId === taskId && Boolean(entry.featureId));
|
|
212
|
+
const entry = v2Entry ?? legacyEntry;
|
|
213
|
+
if (!entry) {
|
|
214
|
+
return gateError("workerTaskRetry", "INVALID_INPUT", `task ${taskId} not found in the pool doctor inventory`, { taskId });
|
|
215
|
+
}
|
|
216
|
+
if (entry.status !== "Failed") {
|
|
217
|
+
return gateError("workerTaskRetry", "HUMAN_CONFIRMATION_REQUIRED", `task ${taskId} is ${entry.status}; only Failed tasks are retry-eligible`, { taskId, status: entry.status });
|
|
218
|
+
}
|
|
219
|
+
if (featureId && entry.featureId && entry.featureId !== featureId) {
|
|
220
|
+
return gateError("workerTaskRetry", "INVALID_INPUT", `featureId ${featureId} does not match the pool task ${taskId} (${entry.featureId})`, { taskId, featureId, observedFeatureId: entry.featureId });
|
|
221
|
+
}
|
|
222
|
+
return { kind: "ready" };
|
|
223
|
+
}
|
|
75
224
|
/**
|
|
76
225
|
* Prefer structured stdout JSON failure semantics (findings / message) over bare
|
|
77
226
|
* `exit N` when stderr is empty — spine audit and similar read CLIs write details
|
|
@@ -889,6 +1038,239 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
889
1038
|
}),
|
|
890
1039
|
};
|
|
891
1040
|
}
|
|
1041
|
+
case "prepareDagExecution": {
|
|
1042
|
+
const taskId = str(p.taskId);
|
|
1043
|
+
if (!taskId)
|
|
1044
|
+
return invalid(action, "taskId is required");
|
|
1045
|
+
const profile = str(p.profile) ?? "auto";
|
|
1046
|
+
const run = ctx.runCommand ?? ((a, o) => ctx.client.run(a, o));
|
|
1047
|
+
// Server-side generate + strict validate only. The model never supplies
|
|
1048
|
+
// DAG bytes or raw paths (AC-02): everything is re-derived here.
|
|
1049
|
+
const staged = await stageUtf8Text(ctx.appData, "", {
|
|
1050
|
+
extension: ".json",
|
|
1051
|
+
});
|
|
1052
|
+
const gen = await run([
|
|
1053
|
+
"task",
|
|
1054
|
+
"advance",
|
|
1055
|
+
taskId,
|
|
1056
|
+
"--profile",
|
|
1057
|
+
profile,
|
|
1058
|
+
"--dag-output",
|
|
1059
|
+
staged.path,
|
|
1060
|
+
"--json",
|
|
1061
|
+
], {
|
|
1062
|
+
cwd: ctx.repoRoot,
|
|
1063
|
+
artifactName: "prepare-dag-execution-task-advance",
|
|
1064
|
+
expectJson: true,
|
|
1065
|
+
timeoutMs: 180_000,
|
|
1066
|
+
});
|
|
1067
|
+
if (!gen.ok || gen.exitCode !== 0) {
|
|
1068
|
+
return {
|
|
1069
|
+
kind: "error",
|
|
1070
|
+
status: 400,
|
|
1071
|
+
body: operatorFailed({
|
|
1072
|
+
command: action,
|
|
1073
|
+
outcome: "invalid",
|
|
1074
|
+
code: "INVALID_INPUT",
|
|
1075
|
+
message: gen.stderr?.trim() ||
|
|
1076
|
+
`task advance (prepare DAG execution) failed with exit ${gen.exitCode}`,
|
|
1077
|
+
details: {
|
|
1078
|
+
exitCode: gen.exitCode,
|
|
1079
|
+
stdoutPreview: (gen.stdout ?? "").slice(0, 1200),
|
|
1080
|
+
},
|
|
1081
|
+
}),
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
let dagText;
|
|
1085
|
+
try {
|
|
1086
|
+
dagText = await readFile(staged.path, "utf8");
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
dagText = "";
|
|
1090
|
+
}
|
|
1091
|
+
if (!dagText.trim()) {
|
|
1092
|
+
// Fallback: advance may skip generate-dag when a draft already
|
|
1093
|
+
// exists; still prefer the task DAG over an empty staged file.
|
|
1094
|
+
const taskDagPath = getTaskPaths(ctx.repoRoot, taskId).dagDraftPath;
|
|
1095
|
+
try {
|
|
1096
|
+
dagText = await readFile(taskDagPath, "utf8");
|
|
1097
|
+
}
|
|
1098
|
+
catch {
|
|
1099
|
+
dagText = "";
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (!dagText.trim()) {
|
|
1103
|
+
return {
|
|
1104
|
+
kind: "error",
|
|
1105
|
+
status: 400,
|
|
1106
|
+
body: operatorFailed({
|
|
1107
|
+
command: action,
|
|
1108
|
+
outcome: "invalid",
|
|
1109
|
+
code: "INVALID_INPUT",
|
|
1110
|
+
message: "task advance produced empty DAG output",
|
|
1111
|
+
}),
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
const writeSetGateToken = extractWriteSetGateTokenFromCliJson(gen.json, taskId);
|
|
1115
|
+
const finalStaged = await stageUtf8Text(ctx.appData, dagText, {
|
|
1116
|
+
extension: ".json",
|
|
1117
|
+
});
|
|
1118
|
+
const controller = ctx.client.getIdentity?.()?.packageFingerprint?.value ??
|
|
1119
|
+
"unknown-controller";
|
|
1120
|
+
const draftRec = await ctx.drafts.get(taskId);
|
|
1121
|
+
const sourceBindingSha256 = draftRec
|
|
1122
|
+
? draftRec.draftSha256
|
|
1123
|
+
: hashDagBytes(`source:${taskId}:${finalStaged.sha256}`);
|
|
1124
|
+
const validateBody = await runReadCli(ctx, ["dag", "validate", "--dag", finalStaged.path, "--json"], "dag validate");
|
|
1125
|
+
if (!validateBody.ok) {
|
|
1126
|
+
return {
|
|
1127
|
+
kind: "error",
|
|
1128
|
+
status: 400,
|
|
1129
|
+
body: operatorFailed({
|
|
1130
|
+
command: action,
|
|
1131
|
+
outcome: "invalid",
|
|
1132
|
+
code: "INVALID_INPUT",
|
|
1133
|
+
message: validateBody.error?.message ??
|
|
1134
|
+
"dag validate failed; cannot prepare execution for an invalid DAG",
|
|
1135
|
+
details: {
|
|
1136
|
+
validateError: validateBody.error ?? null,
|
|
1137
|
+
dagSha256: finalStaged.sha256,
|
|
1138
|
+
},
|
|
1139
|
+
}),
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
const validationResultSha256 = hashDagBytes(JSON.stringify({
|
|
1143
|
+
ok: true,
|
|
1144
|
+
command: "dag validate",
|
|
1145
|
+
result: validateBody.result ?? null,
|
|
1146
|
+
}));
|
|
1147
|
+
const taskContractBinding = await readLiveTaskContractBinding(ctx, taskId);
|
|
1148
|
+
if (!taskContractBinding) {
|
|
1149
|
+
return {
|
|
1150
|
+
kind: "error",
|
|
1151
|
+
status: 409,
|
|
1152
|
+
body: operatorFailed({
|
|
1153
|
+
command: action,
|
|
1154
|
+
outcome: "blocked",
|
|
1155
|
+
code: "BINDING_DRIFT",
|
|
1156
|
+
message: "managed Task Contract binding unavailable; apply/adopt before prepareDagExecution",
|
|
1157
|
+
details: { taskId },
|
|
1158
|
+
}),
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
// G2 deterministic assessment from server-side facts only.
|
|
1162
|
+
let allowedPaths;
|
|
1163
|
+
let forbiddenPaths;
|
|
1164
|
+
try {
|
|
1165
|
+
const taskConfig = await loadTaskConfig(ctx.repoRoot, taskId);
|
|
1166
|
+
allowedPaths = Array.isArray(taskConfig.allowedPaths)
|
|
1167
|
+
? taskConfig.allowedPaths
|
|
1168
|
+
: [];
|
|
1169
|
+
forbiddenPaths = Array.isArray(taskConfig.forbiddenPaths)
|
|
1170
|
+
? taskConfig.forbiddenPaths
|
|
1171
|
+
: [];
|
|
1172
|
+
}
|
|
1173
|
+
catch {
|
|
1174
|
+
return {
|
|
1175
|
+
kind: "error",
|
|
1176
|
+
status: 403,
|
|
1177
|
+
body: operatorFailed({
|
|
1178
|
+
command: action,
|
|
1179
|
+
outcome: "blocked",
|
|
1180
|
+
code: "HUMAN_CONFIRMATION_REQUIRED",
|
|
1181
|
+
message: "task boundary (task.json allowedPaths/forbiddenPaths) unavailable; autonomous execution not permitted",
|
|
1182
|
+
}),
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
const reviewPacket = extractReviewPacketFromCliJson(gen.json) ?? {};
|
|
1186
|
+
const derivedFacts = deriveEligibilityFactsFromDagSpec({
|
|
1187
|
+
dagSpec: JSON.parse(dagText),
|
|
1188
|
+
allowedPaths,
|
|
1189
|
+
forbiddenPaths,
|
|
1190
|
+
});
|
|
1191
|
+
const packetWriters = Array.isArray(reviewPacket.writers)
|
|
1192
|
+
? reviewPacket.writers.map((writer) => ({
|
|
1193
|
+
nodeId: writer.nodeId ?? "unknown",
|
|
1194
|
+
writeSet: Array.isArray(writer.writeSet)
|
|
1195
|
+
? writer.writeSet.filter((v) => typeof v === "string")
|
|
1196
|
+
: [],
|
|
1197
|
+
}))
|
|
1198
|
+
: derivedFacts.writersWriteSets;
|
|
1199
|
+
const packetShellVerification = Array.isArray(reviewPacket.shellVerification)
|
|
1200
|
+
? reviewPacket.shellVerification.length > 0
|
|
1201
|
+
: derivedFacts.hasStructuredVerification;
|
|
1202
|
+
const eligibility = {
|
|
1203
|
+
writersWriteSets: packetWriters,
|
|
1204
|
+
allowedPaths,
|
|
1205
|
+
forbiddenPaths,
|
|
1206
|
+
broadWriteSetRisk: typeof reviewPacket.broadWriteSetRisk === "boolean"
|
|
1207
|
+
? reviewPacket.broadWriteSetRisk
|
|
1208
|
+
: derivedFacts.broadWriteSetRisk,
|
|
1209
|
+
forbiddenOverlapRisk: typeof reviewPacket.forbiddenOverlapRisk === "boolean"
|
|
1210
|
+
? reviewPacket.forbiddenOverlapRisk
|
|
1211
|
+
: derivedFacts.forbiddenOverlapRisk,
|
|
1212
|
+
hasStructuredVerification: packetShellVerification,
|
|
1213
|
+
};
|
|
1214
|
+
const assessment = assessAutonomousExecutionEligibility(eligibility);
|
|
1215
|
+
if (!assessment.ok) {
|
|
1216
|
+
return {
|
|
1217
|
+
kind: "error",
|
|
1218
|
+
status: 403,
|
|
1219
|
+
body: operatorFailed({
|
|
1220
|
+
command: action,
|
|
1221
|
+
outcome: "blocked",
|
|
1222
|
+
code: "HUMAN_CONFIRMATION_REQUIRED",
|
|
1223
|
+
message: assessment.reason,
|
|
1224
|
+
details: {
|
|
1225
|
+
taskId,
|
|
1226
|
+
dagSha256: finalStaged.sha256,
|
|
1227
|
+
eligibility: "denied",
|
|
1228
|
+
},
|
|
1229
|
+
}),
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
const prepared = await ctx.dagExecutionReceipts.prepare({
|
|
1233
|
+
operatorSessionId: ctx.operatorSessionId,
|
|
1234
|
+
taskId,
|
|
1235
|
+
dagHandle: "staged-dag.json",
|
|
1236
|
+
dagBytes: dagText,
|
|
1237
|
+
dagBytesPath: finalStaged.path,
|
|
1238
|
+
sourceBindingSha256,
|
|
1239
|
+
taskContractBinding,
|
|
1240
|
+
controllerFingerprint: controller,
|
|
1241
|
+
validationResultSha256,
|
|
1242
|
+
eligibility,
|
|
1243
|
+
reviewPacket: {
|
|
1244
|
+
taskId,
|
|
1245
|
+
dagHandle: "staged-dag.json",
|
|
1246
|
+
validationOk: true,
|
|
1247
|
+
...(writeSetGateToken ? { writeSetGateToken } : {}),
|
|
1248
|
+
},
|
|
1249
|
+
});
|
|
1250
|
+
if (!prepared.ok) {
|
|
1251
|
+
return {
|
|
1252
|
+
kind: "error",
|
|
1253
|
+
status: 403,
|
|
1254
|
+
body: operatorFailed({
|
|
1255
|
+
command: action,
|
|
1256
|
+
outcome: "blocked",
|
|
1257
|
+
code: prepared.code,
|
|
1258
|
+
message: prepared.reason,
|
|
1259
|
+
}),
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
kind: "sync",
|
|
1264
|
+
status: 200,
|
|
1265
|
+
body: operatorSucceeded("prepareDagExecution", {
|
|
1266
|
+
executionId: prepared.receipt.executionId,
|
|
1267
|
+
eligibility: "approved",
|
|
1268
|
+
taskId,
|
|
1269
|
+
dagSha256: finalStaged.sha256,
|
|
1270
|
+
expiresAt: prepared.receipt.expiresAt,
|
|
1271
|
+
}),
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
892
1274
|
case "confirmDagConfirmation": {
|
|
893
1275
|
const confirmationId = str(p.confirmationId);
|
|
894
1276
|
const humanGateToken = p.humanGateToken;
|
|
@@ -975,23 +1357,106 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
975
1357
|
case "bootstrapFromPrd":
|
|
976
1358
|
return dispatchBootstrapFromPrd(ctx, p);
|
|
977
1359
|
case "importPrd": {
|
|
1360
|
+
// Chat / operator path: same intake bridge as bootstrapFromPrd, but
|
|
1361
|
+
// for an existing taskId (create-if-missing via advance --prd).
|
|
978
1362
|
const taskId = str(p.taskId);
|
|
979
1363
|
const content = str(p.content) ?? str(p.fileText);
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
return invalid(action, "taskId, content, and clientRequestId are required");
|
|
1364
|
+
if (!taskId || !content) {
|
|
1365
|
+
return invalid(action, "taskId and content are required");
|
|
983
1366
|
}
|
|
1367
|
+
const taskKind = normalizeConsoleWorkflowKind(str(p.taskKind), "standard");
|
|
1368
|
+
const engineering = parseEngineeringBoundaryFromParams(p);
|
|
1369
|
+
const identity = deriveTaskIdentityFromPrd(content);
|
|
1370
|
+
const title = str(p.title) ?? identity.title;
|
|
1371
|
+
const run = ctx.runCommand ?? ((a, o) => ctx.client.run(a, o));
|
|
984
1372
|
const staged = await stageUtf8Text(ctx.appData, content, {
|
|
985
1373
|
extension: ".md",
|
|
986
1374
|
});
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
clientRequestId,
|
|
1375
|
+
const imported = await run([
|
|
1376
|
+
"task",
|
|
1377
|
+
"advance",
|
|
991
1378
|
taskId,
|
|
992
|
-
|
|
993
|
-
|
|
1379
|
+
title,
|
|
1380
|
+
"--prd",
|
|
1381
|
+
staged.path,
|
|
1382
|
+
"--role",
|
|
1383
|
+
"requirement",
|
|
1384
|
+
"--json",
|
|
1385
|
+
], {
|
|
1386
|
+
cwd: ctx.repoRoot,
|
|
1387
|
+
artifactName: "import-prd-task-advance",
|
|
1388
|
+
expectJson: true,
|
|
1389
|
+
timeoutMs: 60_000,
|
|
994
1390
|
});
|
|
1391
|
+
if (!imported.ok || imported.exitCode !== 0 || imported.timedOut) {
|
|
1392
|
+
return {
|
|
1393
|
+
kind: "error",
|
|
1394
|
+
status: 400,
|
|
1395
|
+
body: operatorFailed({
|
|
1396
|
+
command: action,
|
|
1397
|
+
outcome: "rejected",
|
|
1398
|
+
code: "INTERNAL_ERROR",
|
|
1399
|
+
message: imported.stderr?.trim() ||
|
|
1400
|
+
`task advance --prd failed with exit ${imported.exitCode}`,
|
|
1401
|
+
details: {
|
|
1402
|
+
exitCode: imported.exitCode,
|
|
1403
|
+
timedOut: imported.timedOut,
|
|
1404
|
+
taskId,
|
|
1405
|
+
},
|
|
1406
|
+
}),
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
const intakeArgs = ["task", "advance", taskId, "--json"];
|
|
1410
|
+
if (taskKind && taskKind !== "standard") {
|
|
1411
|
+
intakeArgs.push("--task-kind", taskKind);
|
|
1412
|
+
}
|
|
1413
|
+
appendEngineeringCliArgs(intakeArgs, engineering);
|
|
1414
|
+
const intake = await run(intakeArgs, {
|
|
1415
|
+
cwd: ctx.repoRoot,
|
|
1416
|
+
artifactName: "import-prd-task-advance-intake",
|
|
1417
|
+
expectJson: true,
|
|
1418
|
+
timeoutMs: 300_000,
|
|
1419
|
+
});
|
|
1420
|
+
const intakeJson = intake.json ?? null;
|
|
1421
|
+
const intakeOk = intake.ok && intake.exitCode === 0 && !intake.timedOut;
|
|
1422
|
+
const bridged = await buildDraftFromTaskIntake({
|
|
1423
|
+
repoRoot: ctx.repoRoot,
|
|
1424
|
+
taskId,
|
|
1425
|
+
title,
|
|
1426
|
+
taskKind,
|
|
1427
|
+
advanceJson: intakeJson,
|
|
1428
|
+
engineering,
|
|
1429
|
+
});
|
|
1430
|
+
const draftSaved = await ctx.drafts.save(bridged.draft);
|
|
1431
|
+
const parseStatus = intakeOk || bridged.parseStatus !== "pending"
|
|
1432
|
+
? bridged.parseStatus
|
|
1433
|
+
: "failed";
|
|
1434
|
+
return {
|
|
1435
|
+
kind: "sync",
|
|
1436
|
+
status: 200,
|
|
1437
|
+
body: operatorSucceeded(action, {
|
|
1438
|
+
taskId,
|
|
1439
|
+
title: draftSaved.draft.title ?? title,
|
|
1440
|
+
taskKind,
|
|
1441
|
+
draftSha256: draftSaved.draftSha256,
|
|
1442
|
+
parseStatus,
|
|
1443
|
+
semanticIntake: bridged.semanticIntake,
|
|
1444
|
+
gaps: bridged.gaps,
|
|
1445
|
+
lifecycleState: bridged.lifecycleState ?? "intake-incomplete",
|
|
1446
|
+
intake: {
|
|
1447
|
+
ok: intakeOk,
|
|
1448
|
+
exitCode: intake.exitCode,
|
|
1449
|
+
timedOut: intake.timedOut,
|
|
1450
|
+
},
|
|
1451
|
+
import: imported.json ?? null,
|
|
1452
|
+
draftPreview: {
|
|
1453
|
+
objective: draftSaved.draft.requirement?.objective ?? null,
|
|
1454
|
+
scope: draftSaved.draft.requirement?.scope ?? [],
|
|
1455
|
+
acceptanceCriteria: draftSaved.draft.requirement?.acceptanceCriteria ?? [],
|
|
1456
|
+
allowedPaths: draftSaved.draft.constraints?.allowedPaths ?? [],
|
|
1457
|
+
},
|
|
1458
|
+
}),
|
|
1459
|
+
};
|
|
995
1460
|
}
|
|
996
1461
|
case "contractApply": {
|
|
997
1462
|
const taskId = str(p.taskId);
|
|
@@ -1110,14 +1575,118 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1110
1575
|
});
|
|
1111
1576
|
}
|
|
1112
1577
|
case "runDag": {
|
|
1113
|
-
//
|
|
1114
|
-
|
|
1578
|
+
// Model path (AC-02): only executionId — never a raw dag path from
|
|
1579
|
+
// the model. The browser Human Gate route (chat human-gate dag-run)
|
|
1580
|
+
// still dispatches with confirmationId for the preserved global
|
|
1581
|
+
// Human Gate flow; that path is not model-reachable (the confirmation
|
|
1582
|
+
// must be in "confirmed" state, which requires a server-signed
|
|
1583
|
+
// human-gate token verified by confirmDagConfirmation).
|
|
1115
1584
|
const clientRequestId = str(req.clientRequestId);
|
|
1116
|
-
|
|
1117
|
-
|
|
1585
|
+
const executionId = str(p.executionId);
|
|
1586
|
+
if (!clientRequestId) {
|
|
1587
|
+
return invalid(action, "runDag requires clientRequestId");
|
|
1118
1588
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1589
|
+
if (executionId && str(p.confirmationId)) {
|
|
1590
|
+
return invalid(action, "runDag accepts either executionId or confirmationId, not both");
|
|
1591
|
+
}
|
|
1592
|
+
if (!executionId) {
|
|
1593
|
+
const confirmationId = str(p.confirmationId);
|
|
1594
|
+
if (!confirmationId) {
|
|
1595
|
+
return invalid(action, "runDag requires executionId (autonomous) or confirmationId (browser Human Gate)");
|
|
1596
|
+
}
|
|
1597
|
+
// Preserved browser Human Gate flow (dag-confirmation.ts untouched).
|
|
1598
|
+
const conf = await ctx.confirmations.get(confirmationId);
|
|
1599
|
+
if (!conf) {
|
|
1600
|
+
return {
|
|
1601
|
+
kind: "error",
|
|
1602
|
+
status: 404,
|
|
1603
|
+
body: operatorFailed({
|
|
1604
|
+
command: action,
|
|
1605
|
+
outcome: "not-found",
|
|
1606
|
+
code: "NOT_FOUND",
|
|
1607
|
+
message: `confirmation not found: ${confirmationId}`,
|
|
1608
|
+
}),
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
// Re-read staged DAG bytes and hash.
|
|
1612
|
+
let dagBytes;
|
|
1613
|
+
try {
|
|
1614
|
+
dagBytes = await readFile(conf.dagBytesPath, "utf8");
|
|
1615
|
+
}
|
|
1616
|
+
catch (error) {
|
|
1617
|
+
return {
|
|
1618
|
+
kind: "error",
|
|
1619
|
+
status: 409,
|
|
1620
|
+
body: operatorFailed({
|
|
1621
|
+
command: action,
|
|
1622
|
+
outcome: "conflict",
|
|
1623
|
+
code: "CONFIRMATION_STALE",
|
|
1624
|
+
message: `cannot re-read staged DAG bytes: ${error instanceof Error ? error.message : String(error)}`,
|
|
1625
|
+
}),
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
const dagSha256 = hashDagBytes(dagBytes);
|
|
1629
|
+
const controller = ctx.client.getIdentity?.()?.packageFingerprint?.value ??
|
|
1630
|
+
conf.controllerFingerprint;
|
|
1631
|
+
const consumed = await ctx.confirmations.consume({
|
|
1632
|
+
confirmationId,
|
|
1633
|
+
operatorSessionId: ctx.operatorSessionId,
|
|
1634
|
+
current: {
|
|
1635
|
+
dagSha256,
|
|
1636
|
+
sourceBindingSha256: conf.sourceBindingSha256,
|
|
1637
|
+
taskContractBinding: conf.taskContractBinding,
|
|
1638
|
+
controllerFingerprint: controller,
|
|
1639
|
+
validationResultSha256: conf.validationResultSha256,
|
|
1640
|
+
},
|
|
1641
|
+
});
|
|
1642
|
+
if (!consumed.ok) {
|
|
1643
|
+
return {
|
|
1644
|
+
kind: "error",
|
|
1645
|
+
status: confirmationErrorStatus(consumed.code),
|
|
1646
|
+
body: operatorFailed({
|
|
1647
|
+
command: action,
|
|
1648
|
+
outcome: "rejected",
|
|
1649
|
+
code: consumed.code,
|
|
1650
|
+
message: consumed.message,
|
|
1651
|
+
}),
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
const taskId = conf.taskContractBinding.taskId;
|
|
1655
|
+
const gateToken = readWriteSetGateToken(conf.reviewPacket);
|
|
1656
|
+
if (gateToken) {
|
|
1657
|
+
return acceptOperation(ctx, {
|
|
1658
|
+
action,
|
|
1659
|
+
actionParams: { confirmationId },
|
|
1660
|
+
clientRequestId,
|
|
1661
|
+
taskId,
|
|
1662
|
+
cliArgs: [
|
|
1663
|
+
"task",
|
|
1664
|
+
"advance",
|
|
1665
|
+
taskId,
|
|
1666
|
+
"--approve-gate",
|
|
1667
|
+
gateToken,
|
|
1668
|
+
"--json",
|
|
1669
|
+
],
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
return acceptOperation(ctx, {
|
|
1673
|
+
action,
|
|
1674
|
+
actionParams: { confirmationId },
|
|
1675
|
+
clientRequestId,
|
|
1676
|
+
taskId,
|
|
1677
|
+
cliArgs: [
|
|
1678
|
+
"dag",
|
|
1679
|
+
"execute",
|
|
1680
|
+
"--dag",
|
|
1681
|
+
conf.dagBytesPath,
|
|
1682
|
+
"--cwd",
|
|
1683
|
+
ctx.repoRoot,
|
|
1684
|
+
"--json",
|
|
1685
|
+
],
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
const receipt = await ctx.dagExecutionReceipts.get(executionId);
|
|
1689
|
+
if (!receipt) {
|
|
1121
1690
|
return {
|
|
1122
1691
|
kind: "error",
|
|
1123
1692
|
status: 404,
|
|
@@ -1125,40 +1694,45 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1125
1694
|
command: action,
|
|
1126
1695
|
outcome: "not-found",
|
|
1127
1696
|
code: "NOT_FOUND",
|
|
1128
|
-
message: `
|
|
1697
|
+
message: `execution receipt not found: ${executionId}`,
|
|
1129
1698
|
}),
|
|
1130
1699
|
};
|
|
1131
1700
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1701
|
+
const taskId = receipt.taskId;
|
|
1702
|
+
// Live server-derived bindings only; any drift fails closed inside
|
|
1703
|
+
// consume (staged bytes/hash + controller + task binding + source).
|
|
1704
|
+
const controller = ctx.client.getIdentity?.()?.packageFingerprint?.value ??
|
|
1705
|
+
receipt.controllerFingerprint;
|
|
1706
|
+
let sourceBindingSha256 = receipt.sourceBindingSha256;
|
|
1134
1707
|
try {
|
|
1135
|
-
|
|
1708
|
+
const draftRec = await ctx.drafts.get(taskId);
|
|
1709
|
+
if (draftRec)
|
|
1710
|
+
sourceBindingSha256 = draftRec.draftSha256;
|
|
1136
1711
|
}
|
|
1137
|
-
catch
|
|
1712
|
+
catch {
|
|
1713
|
+
// keep the receipt binding; consume will still compare it
|
|
1714
|
+
}
|
|
1715
|
+
const liveBinding = await readLiveTaskContractBinding(ctx, taskId);
|
|
1716
|
+
if (!liveBinding) {
|
|
1138
1717
|
return {
|
|
1139
1718
|
kind: "error",
|
|
1140
1719
|
status: 409,
|
|
1141
1720
|
body: operatorFailed({
|
|
1142
1721
|
command: action,
|
|
1143
|
-
outcome: "
|
|
1144
|
-
code: "
|
|
1145
|
-
message:
|
|
1722
|
+
outcome: "blocked",
|
|
1723
|
+
code: "BINDING_DRIFT",
|
|
1724
|
+
message: "managed Task Contract binding unavailable at consume time",
|
|
1725
|
+
details: { taskId },
|
|
1146
1726
|
}),
|
|
1147
1727
|
};
|
|
1148
1728
|
}
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
conf.controllerFingerprint;
|
|
1152
|
-
const consumed = await ctx.confirmations.consume({
|
|
1153
|
-
confirmationId,
|
|
1729
|
+
const consumed = await ctx.dagExecutionReceipts.consume({
|
|
1730
|
+
executionId,
|
|
1154
1731
|
operatorSessionId: ctx.operatorSessionId,
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
controllerFingerprint: controller,
|
|
1160
|
-
validationResultSha256: conf.validationResultSha256,
|
|
1161
|
-
},
|
|
1732
|
+
controllerFingerprint: controller,
|
|
1733
|
+
taskContractBinding: liveBinding,
|
|
1734
|
+
sourceBindingSha256,
|
|
1735
|
+
validationResultSha256: receipt.validationResultSha256,
|
|
1162
1736
|
});
|
|
1163
1737
|
if (!consumed.ok) {
|
|
1164
1738
|
return {
|
|
@@ -1172,41 +1746,48 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1172
1746
|
}),
|
|
1173
1747
|
};
|
|
1174
1748
|
}
|
|
1175
|
-
const
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1749
|
+
const gateToken = readWriteSetGateToken(receipt.reviewPacket);
|
|
1750
|
+
const dispatchRun = async () => {
|
|
1751
|
+
if (gateToken) {
|
|
1752
|
+
// Standard lifecycle path: the execution receipt replaces the
|
|
1753
|
+
// browser confirm; the gate token is still consumed by the CLI.
|
|
1754
|
+
return acceptOperation(ctx, {
|
|
1755
|
+
action,
|
|
1756
|
+
actionParams: { executionId },
|
|
1757
|
+
clientRequestId,
|
|
1758
|
+
taskId,
|
|
1759
|
+
cliArgs: [
|
|
1760
|
+
"task",
|
|
1761
|
+
"advance",
|
|
1762
|
+
taskId,
|
|
1763
|
+
"--approve-gate",
|
|
1764
|
+
gateToken,
|
|
1765
|
+
"--json",
|
|
1766
|
+
],
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
// Advanced: execute the server-staged DAG bytes (never a model path).
|
|
1179
1770
|
return acceptOperation(ctx, {
|
|
1180
1771
|
action,
|
|
1181
|
-
actionParams: {
|
|
1772
|
+
actionParams: { executionId },
|
|
1182
1773
|
clientRequestId,
|
|
1183
1774
|
taskId,
|
|
1184
1775
|
cliArgs: [
|
|
1185
|
-
"
|
|
1186
|
-
"
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1776
|
+
"dag",
|
|
1777
|
+
"execute",
|
|
1778
|
+
"--dag",
|
|
1779
|
+
receipt.dagBytesPath,
|
|
1780
|
+
"--cwd",
|
|
1781
|
+
ctx.repoRoot,
|
|
1190
1782
|
"--json",
|
|
1191
1783
|
],
|
|
1192
1784
|
});
|
|
1785
|
+
};
|
|
1786
|
+
const accepted = await dispatchRun();
|
|
1787
|
+
if (accepted.kind === "accepted") {
|
|
1788
|
+
await ctx.dagExecutionReceipts.markDispatched(executionId, accepted.body.operationId);
|
|
1193
1789
|
}
|
|
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
|
-
});
|
|
1790
|
+
return accepted;
|
|
1210
1791
|
}
|
|
1211
1792
|
case "dagRerunPlan": {
|
|
1212
1793
|
const runId = str(p.runId);
|
|
@@ -1256,10 +1837,18 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1256
1837
|
],
|
|
1257
1838
|
});
|
|
1258
1839
|
}
|
|
1259
|
-
case "
|
|
1840
|
+
case "dagReconcileRun": {
|
|
1841
|
+
// S6 Recovery: orphan/interrupted runs — structured reason is the human
|
|
1842
|
+
// confirmation surface (CLI eligibility still fail-closed).
|
|
1260
1843
|
const runId = str(p.runId);
|
|
1261
1844
|
const reason = str(p.reason);
|
|
1262
1845
|
const clientRequestId = str(req.clientRequestId);
|
|
1846
|
+
const reconcileActionRaw = str(p.reconcileAction) ?? "supersede";
|
|
1847
|
+
if (reconcileActionRaw !== "supersede" &&
|
|
1848
|
+
reconcileActionRaw !== "abandon") {
|
|
1849
|
+
return invalid(action, "reconcileAction must be supersede or abandon");
|
|
1850
|
+
}
|
|
1851
|
+
const reconcileAction = reconcileActionRaw;
|
|
1263
1852
|
if (!runId || !reason || !clientRequestId) {
|
|
1264
1853
|
return invalid(action, "runId, reason, and clientRequestId are required");
|
|
1265
1854
|
}
|
|
@@ -1268,6 +1857,41 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1268
1857
|
return gated;
|
|
1269
1858
|
if (gated.kind === "idempotent")
|
|
1270
1859
|
return gated.result;
|
|
1860
|
+
const accepted = await acceptOperation(ctx, {
|
|
1861
|
+
action,
|
|
1862
|
+
actionParams: {
|
|
1863
|
+
runId,
|
|
1864
|
+
reason,
|
|
1865
|
+
reconcileAction,
|
|
1866
|
+
},
|
|
1867
|
+
clientRequestId,
|
|
1868
|
+
cliArgs: [
|
|
1869
|
+
"dag",
|
|
1870
|
+
"reconcile-run",
|
|
1871
|
+
"--run-id",
|
|
1872
|
+
runId,
|
|
1873
|
+
"--action",
|
|
1874
|
+
reconcileAction,
|
|
1875
|
+
"--reason",
|
|
1876
|
+
reason,
|
|
1877
|
+
"--json",
|
|
1878
|
+
],
|
|
1879
|
+
});
|
|
1880
|
+
await finalizeMutationGateReceipt(ctx, gated.receiptId, accepted);
|
|
1881
|
+
return accepted;
|
|
1882
|
+
}
|
|
1883
|
+
case "standaloneTaskRerun": {
|
|
1884
|
+
const runId = str(p.runId);
|
|
1885
|
+
const reason = str(p.reason);
|
|
1886
|
+
const clientRequestId = str(req.clientRequestId);
|
|
1887
|
+
if (!runId || !reason || !clientRequestId) {
|
|
1888
|
+
return invalid(action, "runId, reason, and clientRequestId are required");
|
|
1889
|
+
}
|
|
1890
|
+
// 2026-08-11: read-only run-facts check replaces the browser Human
|
|
1891
|
+
// Gate (R2). Fails closed when evidence is missing.
|
|
1892
|
+
const gated = await assertAutonomousRerunEligible(ctx, runId, str(p.taskId));
|
|
1893
|
+
if (gated.kind === "error")
|
|
1894
|
+
return gated;
|
|
1271
1895
|
const profile = str(p.profile);
|
|
1272
1896
|
const taskId = str(p.taskId);
|
|
1273
1897
|
const cliArgs = [
|
|
@@ -1285,15 +1909,13 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1285
1909
|
cliArgs.push("--profile", profile);
|
|
1286
1910
|
if (taskId)
|
|
1287
1911
|
cliArgs.push("--task-id", taskId);
|
|
1288
|
-
|
|
1912
|
+
return acceptOperation(ctx, {
|
|
1289
1913
|
action,
|
|
1290
1914
|
actionParams: p,
|
|
1291
1915
|
clientRequestId,
|
|
1292
1916
|
taskId,
|
|
1293
1917
|
cliArgs,
|
|
1294
1918
|
});
|
|
1295
|
-
await finalizeMutationGateReceipt(ctx, gated.receiptId, accepted);
|
|
1296
|
-
return accepted;
|
|
1297
1919
|
}
|
|
1298
1920
|
case "workerTaskRetry": {
|
|
1299
1921
|
const taskId = str(p.taskId);
|
|
@@ -1303,11 +1925,11 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1303
1925
|
if (!taskId || !reason || !clientRequestId) {
|
|
1304
1926
|
return invalid(action, "taskId, reason, and clientRequestId are required");
|
|
1305
1927
|
}
|
|
1306
|
-
|
|
1928
|
+
// 2026-08-11: read-only pool facts check replaces the browser Human
|
|
1929
|
+
// Gate (task must exist in Failed state; featureId consistent).
|
|
1930
|
+
const gated = await assertWorkerTaskRetryFacts(ctx, taskId, featureId);
|
|
1307
1931
|
if (gated.kind === "error")
|
|
1308
1932
|
return gated;
|
|
1309
|
-
if (gated.kind === "idempotent")
|
|
1310
|
-
return gated.result;
|
|
1311
1933
|
const args = [
|
|
1312
1934
|
"task",
|
|
1313
1935
|
"retry",
|
|
@@ -1319,7 +1941,7 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1319
1941
|
];
|
|
1320
1942
|
if (featureId)
|
|
1321
1943
|
args.push("--feature-id", featureId);
|
|
1322
|
-
|
|
1944
|
+
return acceptOperation(ctx, {
|
|
1323
1945
|
action,
|
|
1324
1946
|
actionParams: p,
|
|
1325
1947
|
clientRequestId,
|
|
@@ -1327,8 +1949,6 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1327
1949
|
cliArgs: args,
|
|
1328
1950
|
externalCommand: resolveSiblingAgentWorkerBin(),
|
|
1329
1951
|
});
|
|
1330
|
-
await finalizeMutationGateReceipt(ctx, gated.receiptId, accepted);
|
|
1331
|
-
return accepted;
|
|
1332
1952
|
}
|
|
1333
1953
|
case "workerAdmissionPrepare": {
|
|
1334
1954
|
const featureDir = str(p.featureDir);
|
|
@@ -1510,7 +2130,7 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1510
2130
|
return invalid(action, "action and actionParams are required");
|
|
1511
2131
|
}
|
|
1512
2132
|
if (!MUTATION_GATE_ACTIONS.has(targetAction)) {
|
|
1513
|
-
return invalid(action, "action must be a mutation-gate target (
|
|
2133
|
+
return invalid(action, "action must be a mutation-gate target (dagReconcileRun | workerAdmissionPrepare | workerSchedulerAdd | workerSchedulerCancel | workerSchedulerHarvest | workerSchedulerDiscard)");
|
|
1514
2134
|
}
|
|
1515
2135
|
const secret = ctx.humanGateSecret;
|
|
1516
2136
|
if (!secret) {
|
|
@@ -1876,6 +2496,7 @@ async function dispatchBootstrapFromPrd(ctx, p) {
|
|
|
1876
2496
|
}
|
|
1877
2497
|
const documents = parsedDocs.documents;
|
|
1878
2498
|
const taskKind = normalizeConsoleWorkflowKind(str(p.taskKind), "standard");
|
|
2499
|
+
const engineering = parseEngineeringBoundaryFromParams(p);
|
|
1879
2500
|
const identity = deriveTaskIdentityFromPrd(content);
|
|
1880
2501
|
let taskId;
|
|
1881
2502
|
try {
|
|
@@ -2004,27 +2625,66 @@ async function dispatchBootstrapFromPrd(ctx, p) {
|
|
|
2004
2625
|
}
|
|
2005
2626
|
importResults.push(imported.json ?? null);
|
|
2006
2627
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2628
|
+
// Phase 2 (design 2026-08-11): drive task advance intake after import so
|
|
2629
|
+
// deterministic parse + one-shot Semantic Intake run on archived references.
|
|
2630
|
+
// Timeout is longer than import: Pi MED may take minutes.
|
|
2631
|
+
const intakeArgs = ["task", "advance", taskId, "--json"];
|
|
2632
|
+
if (taskKind && taskKind !== "standard") {
|
|
2633
|
+
intakeArgs.push("--task-kind", taskKind);
|
|
2634
|
+
}
|
|
2635
|
+
appendEngineeringCliArgs(intakeArgs, engineering);
|
|
2636
|
+
const intake = await run(intakeArgs, {
|
|
2637
|
+
cwd: ctx.repoRoot,
|
|
2638
|
+
artifactName: "bootstrap-task-advance-intake",
|
|
2639
|
+
expectJson: true,
|
|
2640
|
+
timeoutMs: 300_000,
|
|
2641
|
+
});
|
|
2642
|
+
const intakeJson = intake.json ?? null;
|
|
2643
|
+
const intakeOk = intake.ok && intake.exitCode === 0 && !intake.timedOut;
|
|
2644
|
+
const bridged = await buildDraftFromTaskIntake({
|
|
2645
|
+
repoRoot: ctx.repoRoot,
|
|
2646
|
+
taskId,
|
|
2647
|
+
title,
|
|
2648
|
+
taskKind,
|
|
2649
|
+
advanceJson: intakeJson,
|
|
2650
|
+
engineering,
|
|
2012
2651
|
});
|
|
2652
|
+
// Never fall back to grill-me product templates when intake fails.
|
|
2653
|
+
const draftSaved = await ctx.drafts.save(bridged.draft);
|
|
2654
|
+
const parseStatus = intakeOk || bridged.parseStatus !== "pending"
|
|
2655
|
+
? bridged.parseStatus
|
|
2656
|
+
: "failed";
|
|
2013
2657
|
return {
|
|
2014
2658
|
kind: "sync",
|
|
2015
2659
|
status: 200,
|
|
2016
2660
|
body: operatorSucceeded("bootstrapFromPrd", {
|
|
2017
2661
|
taskId,
|
|
2018
|
-
title,
|
|
2662
|
+
title: draftSaved.draft.title ?? title,
|
|
2019
2663
|
taskKind,
|
|
2020
2664
|
identitySource: identity.source,
|
|
2021
2665
|
preferredTaskId: identity.taskId,
|
|
2022
2666
|
draftSha256: draftSaved.draftSha256,
|
|
2667
|
+
parseStatus,
|
|
2668
|
+
semanticIntake: bridged.semanticIntake,
|
|
2669
|
+
gaps: bridged.gaps,
|
|
2670
|
+
lifecycleState: bridged.lifecycleState ??
|
|
2671
|
+
(intakeOk ? "intake-incomplete" : "intake-incomplete"),
|
|
2672
|
+
intake: {
|
|
2673
|
+
ok: intakeOk,
|
|
2674
|
+
exitCode: intake.exitCode,
|
|
2675
|
+
timedOut: intake.timedOut,
|
|
2676
|
+
json: intakeJson,
|
|
2677
|
+
},
|
|
2023
2678
|
import: importResults.length === 1 ? importResults[0] : null,
|
|
2024
2679
|
imports: importResults,
|
|
2025
2680
|
documentNames: documents && documents.length > 0
|
|
2026
2681
|
? [...documents.map((d) => d.name), "composed"]
|
|
2027
2682
|
: undefined,
|
|
2683
|
+
draftPreview: {
|
|
2684
|
+
objective: draftSaved.draft.requirement?.objective ?? null,
|
|
2685
|
+
scope: draftSaved.draft.requirement?.scope ?? [],
|
|
2686
|
+
acceptanceCriteria: draftSaved.draft.requirement?.acceptanceCriteria ?? [],
|
|
2687
|
+
},
|
|
2028
2688
|
}),
|
|
2029
2689
|
};
|
|
2030
2690
|
}
|
|
@@ -2206,8 +2866,9 @@ async function dispatchInterviewStart(ctx, p) {
|
|
|
2206
2866
|
};
|
|
2207
2867
|
}
|
|
2208
2868
|
/**
|
|
2209
|
-
* Skip interactive grilling
|
|
2210
|
-
*
|
|
2869
|
+
* Skip interactive grilling only when requirement structure is already present
|
|
2870
|
+
* (from PRD intake) and remaining gaps are engineering/taskKind that can take
|
|
2871
|
+
* non-template recommendations. Never invent console product templates.
|
|
2211
2872
|
*/
|
|
2212
2873
|
async function dispatchInterviewSkip(ctx, p) {
|
|
2213
2874
|
const taskId = str(p.taskId);
|
|
@@ -2216,11 +2877,50 @@ async function dispatchInterviewSkip(ctx, p) {
|
|
|
2216
2877
|
const title = str(p.title) ?? taskId;
|
|
2217
2878
|
const taskKind = str(p.taskKind);
|
|
2218
2879
|
let { draft } = await ensureDraft(ctx, taskId, title, { taskKind });
|
|
2880
|
+
const requirementGaps = new Set([
|
|
2881
|
+
"objective",
|
|
2882
|
+
"scope",
|
|
2883
|
+
"nonGoals",
|
|
2884
|
+
"acceptanceCriteria",
|
|
2885
|
+
]);
|
|
2886
|
+
const currentGaps = draftSummary(draft).gaps;
|
|
2887
|
+
const missingRequirement = currentGaps.filter((g) => requirementGaps.has(g));
|
|
2888
|
+
if (missingRequirement.length > 0) {
|
|
2889
|
+
return {
|
|
2890
|
+
kind: "error",
|
|
2891
|
+
status: 400,
|
|
2892
|
+
body: operatorFailed({
|
|
2893
|
+
command: "interviewSkip",
|
|
2894
|
+
outcome: "invalid",
|
|
2895
|
+
code: "REQUIREMENT_INCOMPLETE",
|
|
2896
|
+
message: "无法跳过:需求层尚未结构化(缺 objective/scope/AC 等)。请先导入 PRD 完成 intake,或手动访谈补齐,禁止用模板冒充解析结果。",
|
|
2897
|
+
details: {
|
|
2898
|
+
gaps: currentGaps,
|
|
2899
|
+
missingRequirement,
|
|
2900
|
+
},
|
|
2901
|
+
}),
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2219
2904
|
const accepted = [];
|
|
2905
|
+
// Only auto-accept engineering / taskKind recommendations; never product templates.
|
|
2906
|
+
const autoAcceptGaps = new Set([
|
|
2907
|
+
"taskKind",
|
|
2908
|
+
"allowedPaths",
|
|
2909
|
+
"forbiddenPaths",
|
|
2910
|
+
"verifyCommands",
|
|
2911
|
+
]);
|
|
2220
2912
|
for (let i = 0; i < 24; i += 1) {
|
|
2221
2913
|
const question = nextGrillMeQuestion(draft);
|
|
2222
2914
|
if (!question)
|
|
2223
2915
|
break;
|
|
2916
|
+
if (!autoAcceptGaps.has(question.gapId))
|
|
2917
|
+
break;
|
|
2918
|
+
// Refuse placeholder recommendations that are not real path/command values.
|
|
2919
|
+
if (question.gapId === "allowedPaths" &&
|
|
2920
|
+
(/请|示例|模板|(/.test(question.recommendation) ||
|
|
2921
|
+
!question.recommendation.includes("*"))) {
|
|
2922
|
+
break;
|
|
2923
|
+
}
|
|
2224
2924
|
draft = applyGrillMeAnswer(draft, question, {
|
|
2225
2925
|
response: "accept-recommendation",
|
|
2226
2926
|
});
|
|
@@ -2237,8 +2937,8 @@ async function dispatchInterviewSkip(ctx, p) {
|
|
|
2237
2937
|
body: operatorFailed({
|
|
2238
2938
|
command: "interviewSkip",
|
|
2239
2939
|
outcome: "invalid",
|
|
2240
|
-
code: "
|
|
2241
|
-
message: "
|
|
2940
|
+
code: "ENGINEERING_BOUNDARY_REQUIRED",
|
|
2941
|
+
message: "无法跳过:工程边界(allowedPaths / forbiddenPaths / verify)尚未确认,请在访谈中填写或通过 CLI flags 提供",
|
|
2242
2942
|
details: {
|
|
2243
2943
|
gaps: draftSummary(saved.draft).gaps,
|
|
2244
2944
|
acceptedRecommendations: accepted,
|