@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +67 -16
  2. package/dist/application/task-lifecycle/advance.js +309 -9
  3. package/dist/application/task-lifecycle/gates.js +50 -0
  4. package/dist/application/task-lifecycle/observe.js +11 -2
  5. package/dist/application/task-lifecycle/plan-transitions.js +13 -21
  6. package/dist/commands/init-upgrade.js +32 -1
  7. package/dist/commands/init.js +94 -3
  8. package/dist/executors/dag-pi-executor.js +18 -3
  9. package/dist/executors/shell-executor.js +4 -91
  10. package/dist/executors/shell-write-guard.js +26 -8
  11. package/dist/shared/operator/capabilities.js +98 -44
  12. package/dist/shared/resilient-git.js +133 -0
  13. package/dist/task/source-prepare/artifact-meta.js +137 -0
  14. package/dist/task/source-prepare/index.js +2 -0
  15. package/dist/task/source-prepare/parse-intent.js +58 -10
  16. package/dist/task/source-prepare/prepare.js +229 -18
  17. package/dist/task/source-prepare/reference-integrity.js +18 -2
  18. package/dist/task/source-prepare/semantic-intake.js +404 -0
  19. package/dist/worker/console/app-data.js +2 -0
  20. package/dist/worker/console/chat/chat-event-store.js +190 -25
  21. package/dist/worker/console/chat/model-resolver.js +17 -0
  22. package/dist/worker/console/chat/pi-console-config.js +250 -32
  23. package/dist/worker/console/chat/pi-runtime.js +1007 -188
  24. package/dist/worker/console/chat/resource-loader.js +5 -4
  25. package/dist/worker/console/chat/routes.js +495 -157
  26. package/dist/worker/console/chat/runtime-context.js +48 -12
  27. package/dist/worker/console/chat/runtime-selection.js +59 -0
  28. package/dist/worker/console/chat/session-store.js +39 -0
  29. package/dist/worker/console/chat/shortcuts.js +1 -0
  30. package/dist/worker/console/chat/tool-adapter.js +9 -3
  31. package/dist/worker/console/chat/tools.js +5 -1
  32. package/dist/worker/console/dag-execution-receipt.js +380 -0
  33. package/dist/worker/console/interview/grill-me.js +7 -6
  34. package/dist/worker/console/operator-actions.js +785 -85
  35. package/dist/worker/console/prd-intake-bridge.js +393 -0
  36. package/dist/worker/console/recovery-cta.js +35 -6
  37. package/dist/worker/console/server.js +8 -15
  38. package/dist/worker/console/static/assets/index-BQkhJpV8.css +1 -0
  39. package/dist/worker/console/static/assets/index-BpuHmlSP.js +29 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +1 -0
  42. package/dist/worker/console/static-src/app/usePrdImport.js +2 -1
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +24 -1
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +19 -1
  45. package/dist/worker/console/static-src/app/useTaskWizard.js +57 -2
  46. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
  47. package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
  48. package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
  49. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
  50. package/dist/worker/console/static-src/operator-chat/session-title-watcher.js +128 -0
  51. package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
  52. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
  53. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
  54. package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -184
  55. package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
  56. package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
  57. package/dist/worker/console/static-src/operator-chat/useOverlayFocus.js +84 -0
  58. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
  59. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
  60. package/dist/worker/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
  61. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -0
  62. package/dist/worker/delivery/final-verification.js +13 -5
  63. package/dist/worker/delivery/package.js +31 -19
  64. package/dist/worker/delivery/verification-bundle.js +6 -4
  65. package/dist/worker/observability/read-model.js +3 -0
  66. package/dist/worker/observe/static/operator-chrome.css +5 -2
  67. package/dist/worker/observe/static/operator-chrome.js +6 -1
  68. package/dist/worker/observe/static/styles.css +39 -9
  69. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +33 -462
  70. package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
  71. package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
  72. package/dist/workflows/dag/backend-test-module-stem.js +0 -5
  73. package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
  74. package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
  75. package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
  76. package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
  77. package/dist/workflows/dag/failure-category.js +3 -0
  78. package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
  79. package/dist/workflows/dag/init-hybrid.js +49 -55
  80. package/dist/workflows/dag/node-execution.js +3 -2
  81. package/dist/workflows/dag/retry-policy.js +44 -11
  82. package/dist/workflows/dag/runner.js +6 -0
  83. package/dist/workflows/dag/scheduler.js +49 -1
  84. package/dist/workflows/dag/types.js +0 -7
  85. package/dist/workflows/dag/validate.js +16 -2
  86. package/dist/workflows/dag/workspace-checkpoint.js +8 -27
  87. package/docs/templates/agent-dag.schema.json +4 -4
  88. package/docs/templates/backend-test-dag.json +29 -32
  89. package/harness.json +1 -1
  90. package/package.json +1 -1
  91. package/skills/local-jacoco-coverage/SKILL.md +281 -0
  92. package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
  93. package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
  94. package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
  95. package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
  96. package/skills/loop-agent/references/command-reference.md +3 -1
  97. package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
  98. package/skills/loop-agent/references/task-workflow.md +4 -0
  99. package/dist/worker/console/chat/instruction-skills.js +0 -217
  100. package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
  101. 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
- "standaloneTaskRerun",
25
- "workerTaskRetry",
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
- const clientRequestId = str(req.clientRequestId);
981
- if (!taskId || !content || !clientRequestId) {
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
- return acceptOperation(ctx, {
988
- action,
989
- actionParams: { taskId, stagedSha256: staged.sha256 },
990
- clientRequestId,
1375
+ const imported = await run([
1376
+ "task",
1377
+ "advance",
991
1378
  taskId,
992
- // PRD archive via task advance; create if missing.
993
- cliArgs: ["task", "advance", taskId, "--prd", staged.path, "--json"],
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
- // Only confirmationId — never raw dag path from browser
1114
- const confirmationId = str(p.confirmationId);
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
- if (!confirmationId || !clientRequestId) {
1117
- return invalid(action, "runDag requires confirmationId and clientRequestId only");
1585
+ const executionId = str(p.executionId);
1586
+ if (!clientRequestId) {
1587
+ return invalid(action, "runDag requires clientRequestId");
1118
1588
  }
1119
- const conf = await ctx.confirmations.get(confirmationId);
1120
- if (!conf) {
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: `confirmation not found: ${confirmationId}`,
1697
+ message: `execution receipt not found: ${executionId}`,
1129
1698
  }),
1130
1699
  };
1131
1700
  }
1132
- // Re-read staged DAG bytes and hash
1133
- let dagBytes;
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
- dagBytes = await readFile(conf.dagBytesPath, "utf8");
1708
+ const draftRec = await ctx.drafts.get(taskId);
1709
+ if (draftRec)
1710
+ sourceBindingSha256 = draftRec.draftSha256;
1136
1711
  }
1137
- catch (error) {
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: "conflict",
1144
- code: "CONFIRMATION_STALE",
1145
- message: `cannot re-read staged DAG bytes: ${error instanceof Error ? error.message : String(error)}`,
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 dagSha256 = hashDagBytes(dagBytes);
1150
- const controller = ctx.client.getIdentity?.()?.packageFingerprint?.value ??
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
- current: {
1156
- dagSha256,
1157
- sourceBindingSha256: conf.sourceBindingSha256,
1158
- taskContractBinding: conf.taskContractBinding,
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 taskId = conf.taskContractBinding.taskId;
1176
- const gateToken = readWriteSetGateToken(conf.reviewPacket);
1177
- if (gateToken) {
1178
- // Standard lifecycle path: human confirmation maps to writeSet gate approve.
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: { confirmationId },
1772
+ actionParams: { executionId },
1182
1773
  clientRequestId,
1183
1774
  taskId,
1184
1775
  cliArgs: [
1185
- "task",
1186
- "advance",
1187
- taskId,
1188
- "--approve-gate",
1189
- gateToken,
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
- // 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
- });
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 "standaloneTaskRerun": {
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
- const accepted = await acceptOperation(ctx, {
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
- const gated = await consumeMutationGateReceipt(ctx, action, p);
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
- const accepted = await acceptOperation(ctx, {
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 (standaloneTaskRerun | workerTaskRetry | workerAdmissionPrepare | workerSchedulerAdd | workerSchedulerCancel | workerSchedulerHarvest | workerSchedulerDiscard)");
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
- const draftSaved = await ctx.drafts.save({
2008
- ...emptyDraft(taskId, title, { taskKind }),
2009
- requirement: {
2010
- objective: title,
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: accept recommendations for remaining gaps and
2210
- * issue a complete assessment so contractApply can proceed.
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: "INVALID_INPUT",
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,