@tea-agent/loop-agent 0.43.0-next.18 → 0.43.0-next.19

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 (161) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +60 -2308
  3. package/README.md +7 -0
  4. package/dist/application/task-lifecycle/advance.js +38 -7
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/commands/init.js +13 -9
  7. package/dist/commands/task-advance.js +32 -0
  8. package/dist/executors/dag-pi-executor.js +1615 -188
  9. package/dist/executors/pi-executor.js +85 -3
  10. package/dist/executors/pi-sdk-executor.js +18 -0
  11. package/dist/executors/shell-executor.js +92 -7
  12. package/dist/shared/backend-dogfood-preflight.js +47 -0
  13. package/dist/shared/dag-failure-category.js +3 -0
  14. package/dist/shared/operator/capabilities.js +180 -0
  15. package/dist/task/config-types.js +29 -0
  16. package/dist/task/contract/project.js +9 -0
  17. package/dist/task/contract/schema.js +17 -0
  18. package/dist/task/source-prepare/build-draft.js +60 -0
  19. package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
  20. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +24 -16
  21. package/dist/worker/console/chat/pi-runtime/custom-tools/scheduled-goal-tools.js +22 -0
  22. package/dist/worker/console/chat/pi-runtime/progressive-tools.js +195 -0
  23. package/dist/worker/console/chat/pi-runtime.js +86 -27
  24. package/dist/worker/console/chat/routes.js +19 -0
  25. package/dist/worker/console/chat/scheduled-goal-booking.js +59 -0
  26. package/dist/worker/console/chat/scheduled-goal-delivery.js +27 -0
  27. package/dist/worker/console/chat/scheduled-goal-request.js +190 -0
  28. package/dist/worker/console/chat/session-mode.js +7 -3
  29. package/dist/worker/console/chat/session-store.js +42 -9
  30. package/dist/worker/console/chat/tool-preview.js +115 -0
  31. package/dist/worker/console/chat/turn-order.js +13 -0
  32. package/dist/worker/console/chat/turn-process.js +32 -30
  33. package/dist/worker/console/operator-actions.js +50 -0
  34. package/dist/worker/console/prd-intake-bridge.js +54 -1
  35. package/dist/worker/console/scheduled-goal-host.js +98 -0
  36. package/dist/worker/console/scheduled-goal-operation-adapter.js +127 -0
  37. package/dist/worker/console/server.js +15 -0
  38. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B_UDknhR.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
  39. package/dist/worker/console/static/assets/{arc-D6hU0drN.js → arc-eoQiMvuk.js} +1 -1
  40. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DmVikdRK.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
  41. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DydABCRg.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
  42. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DrXKC2Jl.js → c4Diagram-5PPSVZJV-j1RkJziL.js} +1 -1
  43. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +1 -0
  44. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D2-qFxr7.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ChfeF41P.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Drwrz55x.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CAQGErL7.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
  48. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DHO38w5U.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
  49. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-EYIN0EOI.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
  50. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CrFLKqkT.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
  51. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMchffoo.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
  52. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DaSvtKQa.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DcW8W9Qj.js → chunk-XXDRQBXY-DC_Ds11b.js} +1 -1
  54. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +1 -0
  55. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +1 -0
  56. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BUirrMEi.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
  57. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXzO8iXR.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
  58. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DAtgaxIn.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
  59. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C1re9Noh.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
  60. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-N8Kj08J9.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
  61. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-D6BpSLs3.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
  62. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C2Vlgnl4.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
  63. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CMbqtVLS.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
  64. package/dist/worker/console/static/assets/{diagram-UB23O5K3-A770Eb-0.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
  65. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-LV2w_2pT.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
  66. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-bwqf56ah.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
  67. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BnVtHhZh.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
  68. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BGVToEqc.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
  69. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DL2l7Vne.js → gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js} +1 -1
  70. package/dist/worker/console/static/assets/index-BWkIfcrK.css +1 -0
  71. package/dist/worker/console/static/assets/{index-B_V4wvXs.js → index-Cdkvw_H6.js} +97 -97
  72. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BWIpubOx.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
  73. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DjYBD8vv.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
  74. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C0_mBaHb.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
  75. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Cy71zbX6.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
  76. package/dist/worker/console/static/assets/{linear-Dt3_w3Vn.js → linear-BDZ9riWi.js} +1 -1
  77. package/dist/worker/console/static/assets/{mermaid.core-DnNOlfEP.js → mermaid.core-7pKqYtpZ.js} +5 -5
  78. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DAbTspwq.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
  79. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-NNiM141p.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
  80. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CN80Z1Do.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
  81. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bxyfh0JU.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
  82. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BlWnC79M.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
  83. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BrEu4P_e.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
  84. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bt70igBr.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
  85. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C8DQR-r9.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
  86. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DEMinJAT.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
  87. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-DDdDBzQc.js → stateDiagram-OKZ733FA-Cv2stqMA.js} +1 -1
  88. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +1 -0
  89. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-G2wcBs3T.js → swimlanes-SLNWSIFB-qDAo4Yc1.js} +2 -2
  90. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +8 -0
  91. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Dzn4g2gJ.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
  92. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-5v2rP9oO.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
  93. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-B4EOx7ew.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
  94. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQ1v8eYP.js → xychartDiagram-ELKLHX3M-BclH5hGh.js} +1 -1
  95. package/dist/worker/console/static/index.html +2 -2
  96. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +41 -11
  97. package/dist/worker/console/static-src/operator-chat/compaction-message.js +3 -17
  98. package/dist/worker/console/static-src/operator-chat/timeline-merge.js +29 -0
  99. package/dist/worker/console/static-src/operator-chat/useChatThread.js +2 -5
  100. package/dist/worker/console/workspace-context.js +11 -0
  101. package/dist/worker/observe/static/operator-chrome.js +3 -1
  102. package/dist/worker/scheduler/scheduled-goal-dispatch.js +117 -0
  103. package/dist/worker/scheduler/scheduled-goal-evidence.js +167 -0
  104. package/dist/worker/scheduler/scheduled-goal-recovery.js +62 -0
  105. package/dist/worker/scheduler/scheduled-goal-store.js +699 -0
  106. package/dist/worker/scheduler/scheduled-goal-supervisor.js +132 -0
  107. package/dist/worker/scheduler/scheduled-goal-time.js +102 -0
  108. package/dist/worker/scheduler/scheduled-goal-types.js +95 -0
  109. package/dist/workflows/dag/backend-test-markdown-workflow.js +6 -24
  110. package/dist/workflows/dag/backend-test-plan-protocol.js +82 -5
  111. package/dist/workflows/dag/dag-retry-schema.js +11 -0
  112. package/dist/workflows/dag/frontend-closeout.js +4 -2
  113. package/dist/workflows/dag/frontend-committed-facts.js +461 -0
  114. package/dist/workflows/dag/frontend-durable-tools.js +15 -3
  115. package/dist/workflows/dag/frontend-implementation-contract.js +365 -8
  116. package/dist/workflows/dag/frontend-plan-canary.js +53 -0
  117. package/dist/workflows/dag/frontend-plan-decision-contract.js +803 -0
  118. package/dist/workflows/dag/frontend-plan-render.js +0 -2
  119. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  120. package/dist/workflows/dag/frontend-provider-capability-matrix.js +8 -61
  121. package/dist/workflows/dag/frontend-recovery-run.js +57 -0
  122. package/dist/workflows/dag/frontend-review-context.js +43 -70
  123. package/dist/workflows/dag/frontend-risk.js +92 -10
  124. package/dist/workflows/dag/frontend-session-budget.js +117 -3
  125. package/dist/workflows/dag/frontend-shape.js +11 -55
  126. package/dist/workflows/dag/frontend-test-execution-evidence.js +35 -10
  127. package/dist/workflows/dag/frontend-typed-event-store.js +32 -25
  128. package/dist/workflows/dag/frontend-verification-trace.js +11 -2
  129. package/dist/workflows/dag/frontend-writer-admission.js +3 -47
  130. package/dist/workflows/dag/frontend-writer-status.js +0 -23
  131. package/dist/workflows/dag/init-hybrid.js +182 -185
  132. package/dist/workflows/dag/lifecycle.js +7 -2
  133. package/dist/workflows/dag/node-execution.js +48 -13
  134. package/dist/workflows/dag/rerun-plan.js +10 -0
  135. package/dist/workflows/dag/rerun-task.js +77 -1
  136. package/dist/workflows/dag/retry-policy.js +18 -0
  137. package/dist/workflows/dag/scheduler.js +2 -4
  138. package/dist/workflows/dag/types.js +23 -0
  139. package/docs/README.md +1 -0
  140. package/docs/init-surface.manifest.json +1 -0
  141. package/docs/operations/README.md +2 -0
  142. package/docs/templates/README.md +1 -1
  143. package/docs/templates/agent-dag.schema.json +2 -2
  144. package/docs/templates/backend-test-dag.json +14 -10
  145. package/docs/templates/frontend-implementation-contract.schema.json +0 -7
  146. package/docs/templates/frontend-implementation-dag.json +8 -9
  147. package/package.json +6 -3
  148. package/skills/frontend-bounded-implement/SKILL.md +3 -4
  149. package/skills/frontend-design-review/SKILL.md +7 -12
  150. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  151. package/skills/frontend-plan/SKILL.md +8 -18
  152. package/skills/frontend-plan/references/decision-contract.md +19 -26
  153. package/skills/frontend-review/SKILL.md +21 -25
  154. package/skills/frontend-review/references/review-findings.md +48 -24
  155. package/dist/worker/console/static/assets/channel-Drecd94a.js +0 -1
  156. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-8Udu0t8-.js +0 -1
  157. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-8Udu0t8-.js +0 -1
  158. package/dist/worker/console/static/assets/index-DcudonhZ.css +0 -1
  159. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CyCiS0Lt.js +0 -1
  160. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-U0lhi6N0.js +0 -8
  161. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
@@ -1023,7 +1023,74 @@ export function shouldRetryWithFallback(stderr, stdout) {
1023
1023
  stdout,
1024
1024
  timedOut: false,
1025
1025
  });
1026
- return ["quota", "rate-limit", "unavailable", "network"].includes(category);
1026
+ return [
1027
+ "quota",
1028
+ "rate-limit",
1029
+ "unavailable",
1030
+ "upstream-unavailable",
1031
+ "network",
1032
+ ].includes(category);
1033
+ }
1034
+ /**
1035
+ * Provider/transport failure text, with model-authored content excluded.
1036
+ *
1037
+ * The captured stdout is a JSONL session transcript: it carries assistant text,
1038
+ * tool arguments and saved findings verbatim. Matching provider vocabulary
1039
+ * against that whole stream let model content drive the verdict (smoke r28: the
1040
+ * reviewer's own finding id `FDR-R28-UNAUTHORIZED-APP-CSS` was read as an auth
1041
+ * failure, which is not retryable, so a completed review was reported as a node
1042
+ * error). Only error-shaped channels may classify a transport failure:
1043
+ * stderr, error events, and the error fields of a message record.
1044
+ */
1045
+ export function extractProviderFailureText(stdout) {
1046
+ const parts = [];
1047
+ for (const rawLine of stdout.split(/\r?\n/)) {
1048
+ const line = rawLine.trim();
1049
+ if (!line)
1050
+ continue;
1051
+ if (!line.startsWith("{")) {
1052
+ // Non-JSON child output (warnings, CLI errors) is not model-authored.
1053
+ parts.push(line);
1054
+ continue;
1055
+ }
1056
+ let record;
1057
+ try {
1058
+ const parsed = JSON.parse(line);
1059
+ if (!isRecord(parsed))
1060
+ continue;
1061
+ record = parsed;
1062
+ }
1063
+ catch {
1064
+ // An unparseable line may be truncated model content: skip it.
1065
+ continue;
1066
+ }
1067
+ const type = typeof record.type === "string" ? record.type : "";
1068
+ const isErrorEvent = type === "error" || type === "agent_error" || type.endsWith("_error");
1069
+ const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : "";
1070
+ const errorField = record.error;
1071
+ const message = isRecord(record.message) ? record.message : undefined;
1072
+ const messageErrorMessage = typeof message?.errorMessage === "string" ? message.errorMessage : "";
1073
+ const messageErrored = message?.stopReason === "error";
1074
+ if (!isErrorEvent &&
1075
+ !errorMessage &&
1076
+ errorField === undefined &&
1077
+ !messageErrorMessage) {
1078
+ continue;
1079
+ }
1080
+ parts.push([type, errorMessage, messageErrorMessage, messageErrored ? "stopReason error" : ""]
1081
+ .filter(Boolean)
1082
+ .join(" "));
1083
+ if (typeof errorField === "string") {
1084
+ parts.push(errorField);
1085
+ }
1086
+ else if (isRecord(errorField)) {
1087
+ parts.push(["message", "errorMessage", "reason", "code", "status", "type"]
1088
+ .map((key) => (errorField[key] === undefined ? "" : String(errorField[key])))
1089
+ .filter(Boolean)
1090
+ .join(" "));
1091
+ }
1092
+ }
1093
+ return parts.join("\n");
1027
1094
  }
1028
1095
  export function classifyPiFailure(input) {
1029
1096
  if (input.exitCode === 0 && input.assistantText.trim())
@@ -1034,7 +1101,9 @@ export function classifyPiFailure(input) {
1034
1101
  return "timeout";
1035
1102
  if (input.outputTooLarge)
1036
1103
  return "output-too-large";
1037
- const combined = `${input.stderr}\n${input.stdout}`.toLowerCase();
1104
+ // Provider/transport verdicts read error-shaped channels only; the raw stdout
1105
+ // stream embeds assistant text, tool arguments and findings verbatim.
1106
+ const combined = `${input.stderr}\n${extractProviderFailureText(input.stdout)}`.toLowerCase();
1038
1107
  // Context overflow (400 request-too-large / context window exceeded).
1039
1108
  // Must be detected BEFORE the generic rate-limit/quota phrases: an overflow
1040
1109
  // error often contains "too many tokens" / "maximum context" which would
@@ -1052,8 +1121,21 @@ export function classifyPiFailure(input) {
1052
1121
  return "rate-limit";
1053
1122
  if (/quota|usage limit|reached[^\n.]{0,60}limit|5\s*小时|5小时|insufficient_quota|billing/.test(combined))
1054
1123
  return "quota";
1055
- if (/\bunauthorized\b|\bhttp\s*40[13]\b|invalid api key|authentication (?:failed|required|error)|auth(?:entication)? failed/.test(combined))
1124
+ // Error words must stand alone: the captured stream also contains
1125
+ // model-authored identifiers, and smoke r28 lost a whole run because the
1126
+ // reviewer's own finding id `FDR-R28-UNAUTHORIZED-APP-CSS` matched a bare
1127
+ // `\bunauthorized\b`, turning a retryable empty-output into a terminal
1128
+ // `auth`. Identifier delimiters (`-`/`_`/alphanumerics) disqualify a match.
1129
+ if (/(?<![\w-])unauthorized(?![\w-])|\bhttp\s*40[13]\b|invalid api key|(?<![\w-])authentication (?:failed|required|error)(?![\w-])|(?<![\w-])auth(?:entication)? failed(?![\w-])/.test(combined))
1056
1130
  return "auth";
1131
+ // Gateway reachability first: a 502/503 body means the provider was
1132
+ // unreachable, not that the request lacked a capability. Reporting the
1133
+ // generic category here previously let an upstream outage surface as the
1134
+ // plan ladder's `unsupported-provider-capability`, which reads as a model
1135
+ // capability mismatch and sent the incident diagnosis in the wrong
1136
+ // direction (2026-09-11).
1137
+ if (/\bhttp\s*50[23]\b|(?:^|\D)50[23](?:\D|$)|service temporarily unavailable|bad gateway|upstream (?:is )?(?:unavailable|down|unreachable)|网关(?:暂时)?不可用/.test(combined))
1138
+ return "upstream-unavailable";
1057
1139
  if (/unknown provider|unknown model|(?:provider|model|service|gateway)\s+(?:is\s+)?unavailable|overloaded|at capacity|temporarily unavailable/.test(combined))
1058
1140
  return "unavailable";
1059
1141
  if (/network\s*(?:error|failure|issue|timeout|unreachable|reset|abort)|networkerror|connection (?:reset|error|refused|closed|timed\s*out)|request timed out|socket hang up|econn\w*|etimedout|fetch failed|dns (?:error|failure|lookup failed)/.test(combined))
@@ -322,6 +322,21 @@ async function createSdkSession(sdk, input, shared) {
322
322
  extensionToolExtras.push(...computeExtensionToolAllowlistExtras([{ name: SIFT_RETRIEVE_TOOL_NAME }], input.toolNames));
323
323
  }
324
324
  const sessionToolNames = [...input.toolNames, ...extensionToolExtras];
325
+ // Pre-session protocol self-check: createAgentSession silently drops a
326
+ // custom tool whose name is absent from this allowlist (the post-create
327
+ // mismatch check cannot see the omission either — expected and actual are
328
+ // both false). The model would then be prompted to call tools that do not
329
+ // exist and burn attempts discovering it (smoke r10/r14 class of failure),
330
+ // so fail before any model call, naming the dropped tools.
331
+ if (input.requireAllowlistedCustomTools && Array.isArray(input.customTools) && input.customTools.length > 0) {
332
+ const allowlist = new Set(sessionToolNames);
333
+ const dropped = input.customTools
334
+ .map((tool) => (isRecord(tool) && typeof tool.name === "string" ? tool.name : undefined))
335
+ .filter((name) => typeof name === "string" && !allowlist.has(name));
336
+ if (dropped.length > 0) {
337
+ throw new Error(`pi session custom tools would be silently dropped (absent from the activation allowlist): ${dropped.join(", ")}; allowlist: [${sessionToolNames.join(", ")}]`);
338
+ }
339
+ }
325
340
  if (input.requireWriterCustomTools) {
326
341
  if (!Array.isArray(input.customTools) || input.customTools.length === 0) {
327
342
  throw new Error("pi writer tool policy missing customTools; refusing to create uncontrolled writer session");
@@ -707,6 +722,9 @@ async function executeSingleSdkAttemptInternal(options) {
707
722
  ? { customTools: writerCustomTools }
708
723
  : {}),
709
724
  ...(requireWriterCustomTools ? { requireWriterCustomTools: true } : {}),
725
+ ...(options.writerToolPolicy?.requireAllowlistedCustomTools
726
+ ? { requireAllowlistedCustomTools: true }
727
+ : {}),
710
728
  ...(options.piExtensionPaths && options.piExtensionPaths.length > 0
711
729
  ? { additionalExtensionPaths: options.piExtensionPaths }
712
730
  : {}),
@@ -470,6 +470,54 @@ function parseCheckRepoSubcheckDurations(result) {
470
470
  }
471
471
  return durations;
472
472
  }
473
+ /**
474
+ * Frozen-command evidence for the review node: which commandId/label/lane each
475
+ * executed command belongs to, its exit status, and the lint lane's status
476
+ * (explicitly `not-configured` when the task declares no lint commands).
477
+ *
478
+ * The review protocol requires exit evidence ("typecheck, build, and test are
479
+ * required successful final exits") while the verification trace only proves
480
+ * command/file/target binding, so this artifact is what makes that requirement
481
+ * satisfiable at all (smoke r31: the reviewer had to request changes for a
482
+ * check no context could support).
483
+ */
484
+ export function buildFrontendVerificationEvidence(input) {
485
+ const directory = buildFrontendVerifyCommandDirectory({
486
+ staticLabels: input.bundle.staticEvidence?.commandLabels ?? [],
487
+ staticCommandTexts: input.bundle.staticEvidence?.commandTexts ?? [],
488
+ behaviorLabels: input.bundle.behaviorEvidence?.commandLabels ?? [],
489
+ behaviorCommandTexts: input.bundle.behaviorEvidence?.commandTexts ?? [],
490
+ mockLabels: input.bundle.mockEvidence?.commandLabels ?? [],
491
+ mockCommandTexts: input.bundle.mockEvidence?.commandTexts ?? [],
492
+ lintLabels: input.bundle.lintEvidence?.commandLabels ?? [],
493
+ lintCommandTexts: input.bundle.lintEvidence?.commandTexts ?? [],
494
+ });
495
+ const entryByCommand = new Map(directory.map((entry) => [entry.command.trim(), entry]));
496
+ return {
497
+ schemaVersion: 1,
498
+ schemaId: "frontend-verification-evidence-v1",
499
+ nodeId: input.nodeId,
500
+ runId: input.runId,
501
+ capturedAt: new Date().toISOString(),
502
+ allPassed: input.results.every((result) => result.ok),
503
+ lintStatus: input.lintStatus,
504
+ lintConfigured: input.lintConfigured,
505
+ commands: input.results.map((result, index) => {
506
+ const entry = entryByCommand.get(result.command.trim());
507
+ return {
508
+ index: index + 1,
509
+ commandId: entry?.commandId ?? null,
510
+ label: entry?.label ?? null,
511
+ lane: entry?.lane ?? null,
512
+ command: result.command,
513
+ ok: result.ok,
514
+ exitCode: result.exitCode,
515
+ failureCategory: result.failureCategory,
516
+ reused: result.reused === true,
517
+ };
518
+ }),
519
+ };
520
+ }
473
521
  export function buildShellResultSummaryMarkdown(input) {
474
522
  const lines = [
475
523
  "# Shell execution summary",
@@ -1915,7 +1963,7 @@ async function executePipelineCommands(input, meta, overrideCommands) {
1915
1963
  * of inventing refs. */
1916
1964
  async function deriveUnresolvedVerificationEntrypointRefs(runDir, frozenCommandIds) {
1917
1965
  try {
1918
- const { restorePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
1966
+ const { restorePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-implementation-contract.js");
1919
1967
  const records = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
1920
1968
  const patch = restorePlanPatchFromCommittedFacts(records);
1921
1969
  const targets = patch
@@ -2127,6 +2175,27 @@ async function executeFrontendVerificationBundle(input, meta) {
2127
2175
  command: result.command,
2128
2176
  reused: result.reused === true,
2129
2177
  }));
2178
+ // Auditable per-command evidence for the review node (smoke r31): the review
2179
+ // protocol treats shell exit status as authoritative, but the review context
2180
+ // only carried the verification trace's command/file/target *binding*
2181
+ // status, so a reviewer could not confirm the frozen commands actually
2182
+ // exited 0 and had to request changes for missing evidence. Written before
2183
+ // either the failure or the success path returns.
2184
+ const lintConfigured = (bundle.lintCommands?.length ?? 0) > 0;
2185
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verification-evidence.json", buildFrontendVerificationEvidence({
2186
+ nodeId: input.task.id,
2187
+ runId: meta.runId,
2188
+ bundle,
2189
+ results,
2190
+ // The review protocol's lint vocabulary is
2191
+ // passed | baseline-debt | failed | unavailable; a task that declares
2192
+ // no lint commands reports `unavailable` plus lintConfigured:false
2193
+ // rather than inventing a value outside that catalog (smoke r33).
2194
+ lintStatus: lintConfigured
2195
+ ? (lintAssessment?.status ?? "unavailable")
2196
+ : "unavailable",
2197
+ lintConfigured,
2198
+ }));
2130
2199
  // Lint baseline-debt is tolerated, so a lint-only failure must not become the
2131
2200
  // verification failure. A shared lint/static or lint/behavior command is
2132
2201
  // different: once reused by a non-lint lane, its failure belongs to that lane
@@ -2320,16 +2389,22 @@ async function readRunDirJson(runDir, relativePath) {
2320
2389
  }
2321
2390
  /** Committed typed review terminal kinds from the review node's typed event
2322
2391
  * store (AC-003: typed terminal tools are the only authoritative verdict). */
2323
- async function collectReviewTerminalKinds(runDir, reviewNodeId) {
2392
+ export async function collectReviewTerminalKinds(runDir, reviewNodeId) {
2324
2393
  try {
2325
2394
  const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
2395
+ // The store also holds findings and scope checkpoints; only the verdict
2396
+ // kinds decide "missing or ambiguous" (fail closed on 0 or 2+).
2397
+ const terminalKinds = new Set([
2398
+ "approve_review",
2399
+ "request_review_changes",
2400
+ ]);
2326
2401
  return records
2327
2402
  .filter((record) => record.phase === "committed")
2328
2403
  .map((record) => {
2329
2404
  const kind = record.fact.kind;
2330
2405
  return typeof kind === "string" ? kind : "";
2331
2406
  })
2332
- .filter((kind) => kind === "approve_review" || kind === "request_review_changes");
2407
+ .filter((kind) => terminalKinds.has(kind));
2333
2408
  }
2334
2409
  catch {
2335
2410
  return [];
@@ -2339,16 +2414,24 @@ async function collectReviewTerminalKinds(runDir, reviewNodeId) {
2339
2414
  * event store (M8: the committed approve_design / request_design_changes fact
2340
2415
  * is the only authoritative verdict; the legacy first-line VERDICT text is
2341
2416
  * never read for writer admission). Fail-closed on a missing/unreadable store. */
2342
- async function collectDesignTerminalKinds(runDir, designNodeId) {
2417
+ export async function collectDesignTerminalKinds(runDir, designNodeId) {
2343
2418
  try {
2344
2419
  const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
2420
+ // The store also holds findings and scope checkpoints; only the verdict
2421
+ // kinds decide "missing or ambiguous" (fail closed on 0 or 2+). Without
2422
+ // this filter any recorded finding makes admission read the committed
2423
+ // verdict as ambiguous and blocks the writer (r11 decision-path smoke).
2424
+ const terminalKinds = new Set([
2425
+ "approve_design",
2426
+ "request_design_changes",
2427
+ ]);
2345
2428
  return records
2346
2429
  .filter((record) => record.phase === "committed")
2347
2430
  .map((record) => {
2348
2431
  const kind = record.fact.kind;
2349
2432
  return typeof kind === "string" ? kind : "";
2350
2433
  })
2351
- .filter((kind) => kind === "approve_design" || kind === "request_design_changes");
2434
+ .filter((kind) => terminalKinds.has(kind));
2352
2435
  }
2353
2436
  catch {
2354
2437
  return [];
@@ -2511,7 +2594,7 @@ async function executeFrontendDesignPolicy(input, meta) {
2511
2594
  // M9a the plan node's assistantText is a pure Markdown narrative; the
2512
2595
  // compile authority is the committed plan ledger ⊕ runtime skeleton.
2513
2596
  // Fail closed on missing committed facts — never fall back to text.
2514
- const { readCommittedOriginFacts, checkCommittedOriginFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
2597
+ const { readCommittedOriginFacts, checkCommittedOriginFacts } = await import("../workflows/dag/frontend-committed-facts.js");
2515
2598
  // A+B (AC-001/003): fail-closed committed contract/scout typed facts.
2516
2599
  // Missing/uncommitted facts block here; never fall back to text parsing.
2517
2600
  const committedContractFacts = await readCommittedOriginFacts(meta.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl");
@@ -2630,7 +2713,7 @@ async function executeFrontendDesignPolicy(input, meta) {
2630
2713
  });
2631
2714
  }
2632
2715
  // Byte freshness is necessary but not sufficient: require the same ledger
2633
- // semantic checks as the shadow prewrite gate before production writer
2716
+ // semantic checks as the canonical admission policy before production writer
2634
2717
  // admission can be materialized.
2635
2718
  if (meta.spec.sourceBinding) {
2636
2719
  const ledgerCheck = await checkFrontendSourceFidelityLedger({
@@ -3016,6 +3099,8 @@ async function executeFrontendCloseout(input, meta) {
3016
3099
  coverageMatrix: verificationTargets.map((target, index) => ({
3017
3100
  verificationTargetId: trace?.targets?.[index]?.id ?? `vt-${index}`,
3018
3101
  status: target.status === "ok" ? "passed" : "failed",
3102
+ evidence: trace?.targets?.[index]?.evidence ?? "unavailable",
3103
+ behaviorCoverage: "unconfirmed",
3019
3104
  })),
3020
3105
  lintStatus: "unavailable",
3021
3106
  integrationFacts,
@@ -0,0 +1,47 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { buildControllerIdentityFromEntry, readBuildStamp } from "./package-metadata.js";
5
+ export function sha256File(file) {
6
+ return `sha256:${createHash("sha256").update(readFileSync(file)).digest("hex")}`;
7
+ }
8
+ export function classifyBackendDogfoodProvenance(input) {
9
+ if (input.tarballPath)
10
+ return { kind: "local-tarball", version: input.identity.packageVersion, tarballSha256: sha256File(input.tarballPath) };
11
+ if (input.distTag)
12
+ return { kind: "published-npm", version: input.identity.packageVersion, distTag: input.distTag };
13
+ return { kind: "local-candidate", version: input.identity.packageVersion };
14
+ }
15
+ export function buildBackendDogfoodIdentityEvidence(input) {
16
+ const controller = buildControllerIdentityFromEntry({ requested: input.controllerEntry, entry: path.resolve(input.controllerEntry) });
17
+ if (!controller)
18
+ throw new Error("BACKEND_DOGFOOD_IDENTITY_UNRESOLVED");
19
+ const fingerprint = controller.packageFingerprint.value;
20
+ if (fingerprint !== input.expectedFingerprint)
21
+ throw new Error(`BACKEND_DOGFOOD_CONTROLLER_MISMATCH: expected ${input.expectedFingerprint}, got ${fingerprint}`);
22
+ const consoleIdentity = input.consoleHost
23
+ ? undefined
24
+ : buildControllerIdentityFromEntry({
25
+ requested: input.consoleControllerEntry ?? path.join(controller.packageRoot, "bin", "agent-worker.js"),
26
+ entry: input.consoleControllerEntry ?? path.join(controller.packageRoot, "bin", "agent-worker.js"),
27
+ });
28
+ if (!input.consoleHost && !consoleIdentity)
29
+ throw new Error("BACKEND_DOGFOOD_CONSOLE_IDENTITY_UNRESOLVED");
30
+ const consoleHost = {
31
+ packageName: String(input.consoleHost?.packageName ?? consoleIdentity?.packageName),
32
+ packageVersion: String(input.consoleHost?.packageVersion ?? consoleIdentity?.packageVersion),
33
+ packageFingerprint: String(input.consoleHost?.packageFingerprint ?? consoleIdentity?.packageFingerprint.value),
34
+ };
35
+ const expected = `${controller.packageName}@${controller.packageVersion}@${fingerprint}`;
36
+ const actual = `${consoleHost.packageName}@${consoleHost.packageVersion}@${consoleHost.packageFingerprint}`;
37
+ if (actual !== expected)
38
+ throw new Error(`BACKEND_DOGFOOD_CONSOLE_IDENTITY_MISMATCH: expected ${expected}, got ${actual}`);
39
+ return {
40
+ schemaVersion: 1,
41
+ controller,
42
+ provenance: classifyBackendDogfoodProvenance({ identity: controller, distTag: input.distTag, tarballPath: input.tarballPath }),
43
+ buildCommitSha: readBuildStamp(controller.packageRoot)?.gitSha ?? null,
44
+ consoleHost,
45
+ dagController: { packageName: controller.packageName, packageVersion: controller.packageVersion, packageFingerprint: fingerprint },
46
+ };
47
+ }
@@ -59,6 +59,9 @@ const RAW_TO_NORMALIZED = {
59
59
  quota: "executor",
60
60
  "rate-limit": "executor",
61
61
  unavailable: "executor",
62
+ // Gateway reachability outage: still an executor-class failure, but named so
63
+ // reports do not read as a capability mismatch.
64
+ "upstream-unavailable": "executor",
62
65
  // Context window overflow (400 request-too-large): the session needs
63
66
  // compaction / fresh context before retrying — an executor-side
64
67
  // environment condition, not a plan-quality defect.
@@ -392,6 +392,147 @@ const COMMON_MUTATION_ERRORS = [
392
392
  ];
393
393
  export function buildOperatorCapabilitiesDocument() {
394
394
  const actions = [
395
+ {
396
+ action: "prepareScheduledTask", cli: "console scheduled task preparation", kind: "mutation",
397
+ inputSchemaVersion: 1, resultSchemaVersion: 1, envelopeSchemaVersion: 1, requiredErrorCodes: ["INVALID_INPUT"],
398
+ description: "Preferred entry for natural-language one-time scheduled tasks, including read-only inspection reports. Call this directly after understanding the goal, output scope, executable acceptance and time; the server uses the CURRENT project directory and creates the immutable PRD, managed Task and strict writeSet gate, binds THIS Chat automatically and returns a REAL planned goal/confirmation card. No pre-existing task, worktree or session ID is needed: never ask the user for these internal fields and do not manually create/edit them with bash/write/newTask/importPrd. Keep preparation concise: preserve the user goal without inventing extra requirements. Omit budget for ordinary tasks. Use minimal independent acceptance commands (AC-1, AC-2), preferably existing test/build scripts or test -s scheduled-report.md for a report; no compound shell/pipelines/substitution, language-specific grep checks or redundant scope assertions (the host enforces scope). Infer routine defaults and verification commands from the repository; ask only about genuinely ambiguous goal/scope/time. For read-only requests allow just an ordinary report file in the current project directory, e.g. scheduled-report.md, not .harness/. Use schedule.delayMinutes for relative time (counted after preparation finishes), or localAt + IANA timezone for an exact date/time. The current workspace must match committed HEAD; never auto-commit/stash/reset existing changes. The card exposes the exact time, bounded defaults and committed base before one human confirmation. Same request in this Chat is idempotent. Only a returned goalId with state=planned is a prepared reservation; textual drafts, Tasks or failed operations are NOT bookings. Never replace a requested schedule with immediate execution or sleep, never call runDag or self-confirm. On failure explain the returned stage, correct business inputs and retry this tool; do not mutate imported PRD/runtime artifacts. For a new distinct appointment, make its goal/time distinct.",
399
+ inputParams: [{ name: "request", type: "object", required: true, description: "Business goal, workflow, allowedPaths, acceptance and schedule; optional forbiddenPaths/budget. Runtime identity is server-owned." }],
400
+ modelCallable: "always", humanConfirmation: "none",
401
+ },
402
+ {
403
+ "action": "prepareScheduledGoal",
404
+ "cli": "console scheduled goal service",
405
+ "kind": "mutation",
406
+ "inputSchemaVersion": 1,
407
+ "resultSchemaVersion": 1,
408
+ "envelopeSchemaVersion": 1,
409
+ "requiredErrorCodes": [
410
+ "INVALID_INPUT"
411
+ ],
412
+ "description": "Advanced entry ONLY for an already prepared Task. For a new natural-language reservation use prepareScheduledTask instead. Requires the current repository or its linked worktree at frozenBase and a managed Task already strict-prepared at its writeSet gate. booking must include managedTaskId, originalGoal, chatSessionRef, workflow, worktreeRef, frozenBase, allowedPaths, forbiddenPaths, acceptance {policy, criteria [{acId,description,command}]}, budget {maxTurns,maxDagRuns,maxSupervisorCalls}, time {executeAtUtc,latestStartUtc,stopAtUtc,expiresAt,timezone,allowedHours,misfirePolicy}. Resolve natural-language dates with the user timezone; ask about ambiguous time/AC/scope. No execution or authorization occurs. Show the complete scope/time/budget for one browser confirmation. Never call runDag for this reservation.",
413
+ "inputParams": [
414
+ {
415
+ "name": "booking",
416
+ "type": "object",
417
+ "required": true,
418
+ "description": "Complete frozen booking inputs; never supply prepared, controllerRef, approval or owner tokens."
419
+ }
420
+ ],
421
+ "modelCallable": "always",
422
+ "humanConfirmation": "none"
423
+ },
424
+ {
425
+ "action": "confirmScheduledGoal",
426
+ "cli": "console scheduled goal service",
427
+ "kind": "mutation",
428
+ "inputSchemaVersion": 1,
429
+ "resultSchemaVersion": 1,
430
+ "envelopeSchemaVersion": 1,
431
+ "requiredErrorCodes": [
432
+ "INVALID_INPUT"
433
+ ],
434
+ "description": "Human browser confirmation of the exact displayed ScheduledGoal envelope.",
435
+ "inputParams": [
436
+ {
437
+ "name": "goalId",
438
+ "type": "string",
439
+ "required": true,
440
+ "description": "Prepared goal id"
441
+ },
442
+ {
443
+ "name": "stateVersion",
444
+ "type": "number",
445
+ "required": true,
446
+ "description": "Displayed state version"
447
+ },
448
+ {
449
+ "name": "envelopeHash",
450
+ "type": "string",
451
+ "required": true,
452
+ "description": "Displayed envelope hash"
453
+ }
454
+ ],
455
+ "modelCallable": "never",
456
+ "humanConfirmation": "required"
457
+ },
458
+ {
459
+ "action": "cancelScheduledGoal",
460
+ "cli": "console scheduled goal service",
461
+ "kind": "mutation",
462
+ "inputSchemaVersion": 1,
463
+ "resultSchemaVersion": 1,
464
+ "envelopeSchemaVersion": 1,
465
+ "requiredErrorCodes": [
466
+ "INVALID_INPUT"
467
+ ],
468
+ "description": "Cancel future ScheduledGoal admission; in-flight/unknown ownership remains until reconciled.",
469
+ "inputParams": [
470
+ {
471
+ "name": "goalId",
472
+ "type": "string",
473
+ "required": true,
474
+ "description": "Goal id"
475
+ },
476
+ {
477
+ "name": "stateVersion",
478
+ "type": "number",
479
+ "required": true,
480
+ "description": "Current state version"
481
+ },
482
+ {
483
+ "name": "reason",
484
+ "type": "string",
485
+ "required": true,
486
+ "description": "Human cancellation reason"
487
+ }
488
+ ],
489
+ "modelCallable": "never",
490
+ "humanConfirmation": "required"
491
+ },
492
+ {
493
+ "action": "scheduledGoalGet",
494
+ "cli": "console scheduled goal service",
495
+ "kind": "read",
496
+ "inputSchemaVersion": 1,
497
+ "resultSchemaVersion": 1,
498
+ "envelopeSchemaVersion": 1,
499
+ "requiredErrorCodes": [
500
+ "INVALID_INPUT"
501
+ ],
502
+ "description": "Read a durable ScheduledGoal booking and separate execution, acceptance, delivery and reconciliation results. Never implies authorization.",
503
+ "inputParams": [
504
+ {
505
+ "name": "goalId",
506
+ "type": "string",
507
+ "required": true,
508
+ "description": "Goal id"
509
+ }
510
+ ],
511
+ "modelCallable": "always",
512
+ "humanConfirmation": "none"
513
+ },
514
+ {
515
+ "action": "scheduledGoalList",
516
+ "cli": "console scheduled goal service",
517
+ "kind": "read",
518
+ "inputSchemaVersion": 1,
519
+ "resultSchemaVersion": 1,
520
+ "envelopeSchemaVersion": 1,
521
+ "requiredErrorCodes": [
522
+ "INVALID_INPUT"
523
+ ],
524
+ "description": "List durable ScheduledGoals, including those whose original Chat was archived or deleted.",
525
+ "inputParams": [
526
+ {
527
+ "name": "chatSessionRef",
528
+ "type": "string",
529
+ "required": false,
530
+ "description": "Optional original Chat session id"
531
+ }
532
+ ],
533
+ "modelCallable": "always",
534
+ "humanConfirmation": "none"
535
+ },
395
536
  {
396
537
  action: "operatorCapabilities",
397
538
  cli: "loop-agent operator capabilities --json",
@@ -714,6 +855,24 @@ export function buildOperatorCapabilitiesDocument() {
714
855
  required: false,
715
856
  description: "optional verify commands (label:command or shell)",
716
857
  },
858
+ {
859
+ name: "requirementOwnership",
860
+ type: "array",
861
+ required: false,
862
+ description: "optional frozen requirement→file ownership for frontend-implementation DAGs: [{requirementIds, implementationFiles?, verificationFiles?}] — machine-enforced at plan finalize",
863
+ },
864
+ {
865
+ name: "capabilityBoundary",
866
+ type: "array",
867
+ required: false,
868
+ description: "optional frozen test capability boundary {allowed?, forbidden?} composed into dependencyPolicy by the runtime",
869
+ },
870
+ {
871
+ name: "interactionIds",
872
+ type: "array",
873
+ required: false,
874
+ description: "optional frozen interaction id vocabulary — contract interaction names are machine-checked against it at plan finalize",
875
+ },
717
876
  ],
718
877
  modelCallable: "always",
719
878
  humanConfirmation: "none",
@@ -1069,6 +1228,27 @@ export function buildOperatorCapabilitiesDocument() {
1069
1228
  required: false,
1070
1229
  description: "verification entries as label:command strings",
1071
1230
  },
1231
+ {
1232
+ name: "requirementOwnership",
1233
+ type: "array",
1234
+ itemsType: "string",
1235
+ required: false,
1236
+ description: "frozen requirement→file ownership: [{requirementIds, implementationFiles?, verificationFiles?}] — machine-enforced at plan finalize",
1237
+ },
1238
+ {
1239
+ name: "capabilityBoundary",
1240
+ type: "array",
1241
+ itemsType: "string",
1242
+ required: false,
1243
+ description: "frozen test capability boundary {allowed?, forbidden?} composed into dependencyPolicy by the runtime",
1244
+ },
1245
+ {
1246
+ name: "interactionIds",
1247
+ type: "array",
1248
+ itemsType: "string",
1249
+ required: false,
1250
+ description: "frozen interaction id vocabulary — contract interaction names are machine-checked against it at plan finalize",
1251
+ },
1072
1252
  {
1073
1253
  name: "approveGate",
1074
1254
  type: "string",
@@ -286,6 +286,35 @@ const taskConfigObjectSchema = z.object({
286
286
  verifyPreset: verifyPresetSchema.optional().default("auto"),
287
287
  /** Task-contract commands that must be included in final DAG shell verification. */
288
288
  verifyCommands: z.array(taskVerifyCommandSchema).optional().default([]),
289
+ /** Generation-frozen requirement→file ownership for frontend-implementation
290
+ * DAGs. Machine-checked at plan finalize: a requirement's implementation
291
+ * targets (and its verification targets' files) must stay inside the
292
+ * declared sets, so ownership never depends on model compliance with prose.
293
+ */
294
+ requirementOwnership: z
295
+ .array(z
296
+ .object({
297
+ requirementIds: z.array(z.string().min(1)).min(1),
298
+ implementationFiles: z.array(z.string().min(1)).optional(),
299
+ verificationFiles: z.array(z.string().min(1)).optional(),
300
+ })
301
+ .strict()
302
+ .refine((entry) => (entry.implementationFiles?.length ?? 0) > 0 ||
303
+ (entry.verificationFiles?.length ?? 0) > 0, { message: "requirementOwnership entry needs implementationFiles or verificationFiles" }))
304
+ .optional(),
305
+ /** Generation-frozen test capability boundary composed into the canonical
306
+ * dependencyPolicy by the runtime (planner never maintains it). */
307
+ capabilityBoundary: z
308
+ .object({
309
+ allowed: z.array(z.string().min(1)).optional(),
310
+ forbidden: z.array(z.string().min(1)).optional(),
311
+ })
312
+ .strict()
313
+ .refine((entry) => (entry.allowed?.length ?? 0) > 0 || (entry.forbidden?.length ?? 0) > 0, { message: "capabilityBoundary needs allowed or forbidden entries" })
314
+ .optional(),
315
+ /** Generation-frozen interaction id vocabulary. Contract interaction names
316
+ * must come from this set (plan finalize deterministic check). */
317
+ interactionIds: z.array(z.string().min(1)).optional(),
289
318
  /** Verification selection policy. Intermediate loops may use quota; final gates still run full required verification. */
290
319
  verifyQuota: verifyQuotaSchema.optional().default("full"),
291
320
  /** DAG supervised verify strategy. Final quota is fixed to full; intermediate quota may reduce cost. */
@@ -187,6 +187,15 @@ export function mergeManagedTaskConfigFields(existing, draft) {
187
187
  ...(draft.frontendOpenspec !== undefined
188
188
  ? { frontendOpenspec: draft.frontendOpenspec }
189
189
  : {}),
190
+ ...(draft.requirementOwnership !== undefined
191
+ ? { requirementOwnership: draft.requirementOwnership }
192
+ : {}),
193
+ ...(draft.capabilityBoundary !== undefined
194
+ ? { capabilityBoundary: draft.capabilityBoundary }
195
+ : {}),
196
+ ...(draft.interactionIds !== undefined
197
+ ? { interactionIds: draft.interactionIds }
198
+ : {}),
190
199
  };
191
200
  if (draft.featureId !== undefined) {
192
201
  next.featureId = draft.featureId;
@@ -75,6 +75,23 @@ export const taskContractDraftSchema = z
75
75
  assumptions: z.array(z.string()).optional(),
76
76
  hardConstraints: z.array(z.string()).optional(),
77
77
  frontendOpenspec: frontendOpenspecConfigSchema.optional(),
78
+ requirementOwnership: z
79
+ .array(z
80
+ .object({
81
+ requirementIds: z.array(z.string().min(1)).min(1),
82
+ implementationFiles: z.array(z.string().min(1)).optional(),
83
+ verificationFiles: z.array(z.string().min(1)).optional(),
84
+ })
85
+ .strict())
86
+ .optional(),
87
+ capabilityBoundary: z
88
+ .object({
89
+ allowed: z.array(z.string().min(1)).optional(),
90
+ forbidden: z.array(z.string().min(1)).optional(),
91
+ })
92
+ .strict()
93
+ .optional(),
94
+ interactionIds: z.array(z.string().min(1)).optional(),
78
95
  interviewAssessmentRef: z
79
96
  .object({
80
97
  draftSha256: z.string().min(1),