@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.3

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 (202) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +187 -92
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/recommendations.js +13 -2
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +429 -28
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/executors/dag-pi-executor.js +1645 -29
  17. package/dist/executors/pi-extension-resolver.js +233 -0
  18. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  19. package/dist/executors/pi-sdk-executor.js +180 -33
  20. package/dist/executors/shell-executor.js +1150 -171
  21. package/dist/executors/shell-write-guard.js +14 -0
  22. package/dist/shared/dag-prompt-override.js +28 -0
  23. package/dist/shared/openspec-spec.js +70 -4
  24. package/dist/shared/operator/capabilities.js +32 -0
  25. package/dist/shared/playwright-cli-command-policy.js +15 -0
  26. package/dist/shared/update/console-notifier.js +100 -0
  27. package/dist/{cli → shared}/update/npm-client.js +41 -8
  28. package/dist/{cli → shared}/update/state.js +41 -13
  29. package/dist/task/config-types.js +19 -5
  30. package/dist/task/contract/project.js +3 -0
  31. package/dist/task/contract/schema.js +2 -1
  32. package/dist/task/frontend-project-capability.js +42 -16
  33. package/dist/task/source-prepare/parse-intent.js +24 -4
  34. package/dist/task/source-prepare/prepare.js +36 -0
  35. package/dist/worker/cli.js +31 -1
  36. package/dist/worker/console/chat/pi-console-config.js +81 -10
  37. package/dist/worker/console/chat/routes.js +1 -0
  38. package/dist/worker/console/dag-execution-receipt.js +33 -0
  39. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  40. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  41. package/dist/worker/console/index.js +3 -0
  42. package/dist/worker/console/operation-store.js +226 -6
  43. package/dist/worker/console/operator-actions.js +263 -0
  44. package/dist/worker/console/operator-user-error.js +2 -2
  45. package/dist/worker/console/routes.js +43 -2
  46. package/dist/worker/console/security.js +63 -9
  47. package/dist/worker/console/server.js +151 -6
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
  80. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
  81. package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  107. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  108. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  109. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  110. package/dist/worker/observe/node-input.js +74 -3
  111. package/dist/worker/observe/node-process.js +377 -0
  112. package/dist/worker/observe/routes.js +36 -3
  113. package/dist/worker/observe/shell-handler-keys.js +34 -0
  114. package/dist/worker/observe/static/api.js +66 -0
  115. package/dist/worker/observe/static/constants.js +8 -1
  116. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  117. package/dist/worker/observe/static/format.js +23 -0
  118. package/dist/worker/observe/static/markdown-render.js +20 -1
  119. package/dist/worker/observe/static/operator-chrome.js +4 -0
  120. package/dist/worker/observe/static/state.js +13 -2
  121. package/dist/worker/observe/static/styles.css +73 -0
  122. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  123. package/dist/worker/observe/static/views/dag.js +8 -1
  124. package/dist/worker/observe/static/views/failures.js +5 -2
  125. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  126. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  127. package/dist/workflows/dag/contract-output-registry.js +15 -0
  128. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  129. package/dist/workflows/dag/failure-category.js +12 -0
  130. package/dist/workflows/dag/frontend-closeout.js +221 -0
  131. package/dist/workflows/dag/frontend-design-policy.js +347 -0
  132. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  133. package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
  134. package/dist/workflows/dag/frontend-prewrite-gate.js +258 -393
  135. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  136. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  137. package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
  138. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  139. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  140. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  141. package/dist/workflows/dag/frontend-repair.js +7 -424
  142. package/dist/workflows/dag/frontend-review-context.js +195 -8
  143. package/dist/workflows/dag/frontend-risk.js +15 -2
  144. package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
  145. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  146. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  147. package/dist/workflows/dag/frontend-shape.js +425 -0
  148. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  149. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  150. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  151. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  152. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  153. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  154. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  155. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  156. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  157. package/dist/workflows/dag/frontend-typed-event-store.js +438 -0
  158. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  159. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  160. package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
  161. package/dist/workflows/dag/frontend-writer-admission.js +260 -0
  162. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  163. package/dist/workflows/dag/init-hybrid.js +572 -500
  164. package/dist/workflows/dag/node-execution.js +307 -70
  165. package/dist/workflows/dag/prompt.js +10 -18
  166. package/dist/workflows/dag/rerun-plan.js +144 -5
  167. package/dist/workflows/dag/rerun-run.js +58 -6
  168. package/dist/workflows/dag/retry-policy.js +228 -4
  169. package/dist/workflows/dag/runner.js +314 -119
  170. package/dist/workflows/dag/scheduler.js +114 -16
  171. package/dist/workflows/dag/structured-output-repair.js +712 -0
  172. package/dist/workflows/dag/types.js +235 -23
  173. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  174. package/dist/workflows/dag/validate.js +60 -10
  175. package/docs/init-surface.manifest.json +5 -0
  176. package/docs/operations/README.md +1 -1
  177. package/docs/templates/agent-dag.schema.json +44 -0
  178. package/docs/templates/frontend-test-dag.json +8 -10
  179. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  180. package/docs/templates/init-managed-agents.md +13 -3
  181. package/harness.json +3 -3
  182. package/package.json +5 -1
  183. package/scripts/next-info.mjs +356 -0
  184. package/scripts/next-publish-gate.mjs +238 -0
  185. package/scripts/release-source-binding.mjs +251 -0
  186. package/skills/codebase-scout/SKILL.md +1 -1
  187. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  188. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  189. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  190. package/skills/frontend-implementation/SKILL.md +4 -4
  191. package/skills/frontend-implementation/references/design-spec.md +11 -7
  192. package/skills/frontend-implementation/references/node-contracts.md +20 -11
  193. package/skills/loop-agent/references/command-reference.md +11 -4
  194. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  195. package/skills/playwright-cli/SKILL.md +22 -0
  196. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  197. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  198. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  199. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  200. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  201. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  202. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -0,0 +1,159 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { TYPED_EVENT_FACT_KINDS, stageTypedEventRecord, } from "./frontend-typed-event-store.js";
3
+ import { createDagEventObserver } from "./event-observer.js";
4
+ /**
5
+ * M1 shadow-only provider/model capability matrix (AC-003).
6
+ *
7
+ * Records seven provider/model capability facts as read-only observations:
8
+ * - `terminal-commit`
9
+ * - `disconnect`
10
+ * - `duplicate-tool-call`
11
+ * - `length-before-terminal`
12
+ * - `reasoning-timeout`
13
+ * - `provider-ended-without-terminal`
14
+ * - `read-only-fact-channel`
15
+ *
16
+ * This matrix is intentionally shadow-only: `recordProviderCapabilityFact`
17
+ * returns a record for audit, never a branch-control signal, and this module
18
+ * must never be imported into `node-execution.ts`'s model-selection / branch
19
+ * control path. It is deliberately distinct from `frontend-project-capability`
20
+ * (framework / mock discovery).
21
+ *
22
+ * The read-only fact channel composes the existing `createDagEventObserver`
23
+ * (which already swallows its own write errors) so that a shadow-channel
24
+ * failure can never affect canonical DAG execution.
25
+ */
26
+ export const PROVIDER_CAPABILITY_FACT_KINDS = TYPED_EVENT_FACT_KINDS;
27
+ /** Terminal finish-reason shapes, modeled after
28
+ * `src/executors/pi-event-serializer.ts`'s private `TERMINAL_FINISH_REASON_TYPES`
29
+ * / `readExplicitFinishReason` (those symbols are not exported, so the matrix
30
+ * keeps its own local projection). */
31
+ const TERMINAL_FINISH_REASON_TYPES = new Set([
32
+ "message_end",
33
+ "turn_end",
34
+ "assistant_message",
35
+ ]);
36
+ function readExplicitFinishReason(source) {
37
+ const message = isRecord(source.message) ? source.message : undefined;
38
+ const candidates = [
39
+ source.finish_reason,
40
+ source.finishReason,
41
+ source.stop_reason,
42
+ source.stopReason,
43
+ message?.finish_reason,
44
+ message?.finishReason,
45
+ message?.stop_reason,
46
+ message?.stopReason,
47
+ ];
48
+ for (const candidate of candidates) {
49
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
50
+ return candidate;
51
+ }
52
+ }
53
+ return undefined;
54
+ }
55
+ function isRecord(value) {
56
+ return typeof value === "object" && value !== null && !Array.isArray(value);
57
+ }
58
+ /** True when an observed provider event is terminal-shaped. Shadow-only: the
59
+ * result is a fact, not a control decision. */
60
+ export function isTerminalProviderEventShape(event) {
61
+ const type = typeof event.type === "string" ? event.type : undefined;
62
+ return type !== undefined && TERMINAL_FINISH_REASON_TYPES.has(type);
63
+ }
64
+ /** Build a `terminal-commit` fact from a provider event, if the event carries a
65
+ * terminal shape or an explicit finish reason. Returns undefined when the event
66
+ * has no terminal evidence (the caller records nothing in that case). */
67
+ export function buildTerminalCommitFact(event) {
68
+ if (!isTerminalProviderEventShape(event)) {
69
+ const reason = readExplicitFinishReason(event);
70
+ if (reason === undefined)
71
+ return undefined;
72
+ return { kind: "terminal-commit", finishReason: reason };
73
+ }
74
+ return {
75
+ kind: "terminal-commit",
76
+ finishReason: readExplicitFinishReason(event),
77
+ };
78
+ }
79
+ /**
80
+ * A+B (AC-005): route a requested fact kind through the frozen provider
81
+ * capability matrix. Kinds outside the seven-value matrix fail closed to
82
+ * `unsupported-provider-capability` so a plan retry can select a bounded
83
+ * tool-only / backup-model step instead of repeating an unsupported tool.
84
+ */
85
+ export function routeFrontendProviderCapability(input) {
86
+ if (TYPED_EVENT_FACT_KINDS.includes(input.factKind)) {
87
+ return { ok: true, kind: input.factKind };
88
+ }
89
+ return { ok: false, reason: "unsupported-provider-capability" };
90
+ }
91
+ /**
92
+ * Append a capability fact as a staged read-only observation. Returns the
93
+ * record (for audit) and never a branch-control signal. Unknown fact kinds are
94
+ * rejected fail-closed so the matrix cannot silently widen its own catalog.
95
+ */
96
+ export function recordProviderCapabilityFact(options) {
97
+ const kind = options.fact.kind;
98
+ if (!TYPED_EVENT_FACT_KINDS.includes(kind)) {
99
+ throw new Error(`unknown provider capability fact kind: ${String(kind)}`);
100
+ }
101
+ const eventId = options.eventId ?? randomUUID();
102
+ const record = stageTypedEventRecord(options.store, {
103
+ eventId,
104
+ requestId: options.requestId,
105
+ attemptId: options.attemptId,
106
+ fact: options.fact,
107
+ recordedAt: options.recordedAt,
108
+ });
109
+ return { eventId, record };
110
+ }
111
+ /**
112
+ * Attach a shadow observer that records `read-only-fact-channel` facts while
113
+ * delegating the canonical event JSONL to the existing `createDagEventObserver`.
114
+ * The shadow channel never throws into the caller: any recording failure is
115
+ * swallowed so canonical execution is unaffected.
116
+ */
117
+ export function createProviderCapabilityShadowObserver(options) {
118
+ const base = createDagEventObserver({
119
+ eventsJsonlPath: options.eventsJsonlPath,
120
+ dagRunId: options.dagRunId,
121
+ spec: options.spec,
122
+ });
123
+ const recordReadOnlyFact = (eventType) => {
124
+ try {
125
+ recordProviderCapabilityFact({
126
+ store: options.store,
127
+ attemptId: options.attemptId,
128
+ requestId: options.requestId,
129
+ fact: { kind: "read-only-fact-channel", eventType },
130
+ });
131
+ }
132
+ catch {
133
+ // Shadow channel must never affect canonical execution.
134
+ }
135
+ };
136
+ const observer = {
137
+ onRunStart: async (state) => {
138
+ recordReadOnlyFact("dag.run.started");
139
+ await base.observer.onRunStart?.(state);
140
+ },
141
+ onNodeStart: async (nodeId, state) => {
142
+ recordReadOnlyFact("dag.node.started");
143
+ await base.observer.onNodeStart?.(nodeId, state);
144
+ },
145
+ onNodeOutput: async (nodeId, chunk, state) => {
146
+ recordReadOnlyFact("dag.node.output");
147
+ await base.observer.onNodeOutput?.(nodeId, chunk, state);
148
+ },
149
+ onNodeFinish: async (nodeId, state) => {
150
+ recordReadOnlyFact("dag.node.finished");
151
+ await base.observer.onNodeFinish?.(nodeId, state);
152
+ },
153
+ onRunFinish: async (state) => {
154
+ recordReadOnlyFact("dag.run.finished");
155
+ await base.observer.onRunFinish?.(state);
156
+ },
157
+ };
158
+ return { observer, flush: base.flush };
159
+ }
@@ -0,0 +1,455 @@
1
+ import { z } from "zod";
2
+ import { normalizeDagFailureCategory, } from "./failure-category.js";
3
+ import { routeDagFailure, } from "./failure-routing.js";
4
+ import { computeNormalizedFailureFingerprint, } from "./frontend-recovery-lineage.js";
5
+ /**
6
+ * M6 failure capsule + deterministic ownership classifier + fact invalidation
7
+ * matrix + frozen command reuse validation (pure functions).
8
+ *
9
+ * AC-001 invariant: `restartPhase` is always the runtime classifier output.
10
+ * `modelSuggestedRestartPhase` is recorded separately and NEVER overrides it.
11
+ *
12
+ * AC-001/AC-002: canonical frontend recovery taxonomy. Operation state and the
13
+ * optional terminal are separate fields; legacy Console states
14
+ * (`queued|running|succeeded|failed|timed-out|needs-reconcile`), boolean
15
+ * terminal markers, and legacy owner/mode strings never appear in the capsule.
16
+ *
17
+ * All schemas are self-contained and `.strict()`: this module never extends
18
+ * `DagRunState`/`DagSpec` and only imports the existing recovery modules.
19
+ */
20
+ const hash64 = z.string().regex(/^[a-f0-9]{64}$/);
21
+ /* ------------------------------------------------------------------ *
22
+ * Restart phase + ownership. *
23
+ * ------------------------------------------------------------------ */
24
+ export const restartPhaseSchema = z.enum([
25
+ "contract",
26
+ "scout",
27
+ "plan",
28
+ "implement",
29
+ ]);
30
+ export const RESTART_PHASE_ORDER = [
31
+ "contract",
32
+ "scout",
33
+ "plan",
34
+ "implement",
35
+ ];
36
+ export const failureOwnerSchema = z.enum([
37
+ "contract",
38
+ "scout",
39
+ "plan",
40
+ "implementation",
41
+ "verification-config",
42
+ "environment",
43
+ "scope",
44
+ "unknown",
45
+ ]);
46
+ export const recoveryModeSchema = z.enum([
47
+ "node-retry",
48
+ "continuation",
49
+ "new-dag",
50
+ "wait-external",
51
+ "human-decision",
52
+ "none",
53
+ ]);
54
+ /* ------------------------------------------------------------------ *
55
+ * Canonical operation state / terminal / protocol failure. *
56
+ * ------------------------------------------------------------------ */
57
+ export const frontendOperationTerminalSchema = z.enum([
58
+ "success",
59
+ "non-converging",
60
+ "cancelled",
61
+ "superseded",
62
+ "failed-internal",
63
+ ]);
64
+ export const frontendOperationStateSchema = z.enum([
65
+ "active",
66
+ "orchestrating-split",
67
+ "recoverable",
68
+ "blocked-human",
69
+ "blocked-external",
70
+ "success",
71
+ "non-converging",
72
+ "cancelled",
73
+ "superseded",
74
+ "failed-internal",
75
+ ]);
76
+ const FRONTEND_OPERATION_TERMINAL_VALUES = frontendOperationTerminalSchema.options;
77
+ /** True when the canonical state is one of the five terminal values. */
78
+ export function isFrontendOperationTerminalState(state) {
79
+ return FRONTEND_OPERATION_TERMINAL_VALUES.includes(state);
80
+ }
81
+ export const frontendNodeProtocolFailureReasonSchema = z.enum([
82
+ "length-before-terminal",
83
+ "terminal-event-missing",
84
+ "reasoning-timeout",
85
+ "provider-ended-without-terminal",
86
+ "terminal-conflict",
87
+ "unsupported-tool-capability",
88
+ "typed-fact-invalid",
89
+ ]);
90
+ const PROTOCOL_REASON_ALIASES = {
91
+ "frontend candidate contract invalid": "typed-fact-invalid",
92
+ };
93
+ /**
94
+ * Project a free-form protocol failure string to the canonical 7-value enum.
95
+ * Known aliases map to their canonical value; the 7 enum values pass through;
96
+ * anything else (including `frontend writer transient partial write` and
97
+ * `unknown failure`) fails closed to `undefined`.
98
+ */
99
+ export function projectProtocolFailureReason(reason) {
100
+ const alias = PROTOCOL_REASON_ALIASES[reason];
101
+ if (alias)
102
+ return alias;
103
+ const parsed = frontendNodeProtocolFailureReasonSchema.safeParse(reason);
104
+ return parsed.success ? parsed.data : undefined;
105
+ }
106
+ function ownerFromNodeId(nodeId) {
107
+ const id = nodeId.toLowerCase();
108
+ if (id.includes("contract"))
109
+ return "contract";
110
+ if (id.includes("scout"))
111
+ return "scout";
112
+ if (id.includes("plan"))
113
+ return "plan";
114
+ if (id.includes("implement"))
115
+ return "implementation";
116
+ return undefined;
117
+ }
118
+ function ownerFromProductLine(productLine) {
119
+ if (!productLine)
120
+ return "unknown";
121
+ switch (productLine) {
122
+ case "SpecUnclear":
123
+ return "contract";
124
+ case "ContractMismatch":
125
+ return "plan";
126
+ case "ProductBug":
127
+ case "TestBug":
128
+ case "RiskyChange":
129
+ return "implementation";
130
+ case "EnvFailure":
131
+ case "FlakyTest":
132
+ case "DependencyFailure":
133
+ return "environment";
134
+ case "NeedsHuman":
135
+ case "Unknown":
136
+ return "unknown";
137
+ default: {
138
+ const exhaustive = productLine;
139
+ return exhaustive;
140
+ }
141
+ }
142
+ }
143
+ /**
144
+ * Deterministic ownership classifier. Priority order:
145
+ * 1. explicit failure source/nodeId phase substring;
146
+ * 2. verification-config signals (frozen command drift, stale verification
147
+ * plan, unresolved entrypoint);
148
+ * 3. environment signals (env/flaky/dependency/network/rate-limit/provider
149
+ * unavailable);
150
+ * 4. scope signals (path/write/forbidden/write-guard/writeSet violations);
151
+ * 5. product-line routing fallback;
152
+ * 6. fail-closed `unknown`.
153
+ *
154
+ * `verification-config`, `environment`, `scope`, and `unknown` never resolve to
155
+ * `implementation` (AC-003): writer recovery is never auto-selected for them.
156
+ */
157
+ export function classifyFailureOwner(input) {
158
+ const source = (input.failureSource ?? input.nodeId ?? "").toLowerCase();
159
+ const fromSource = ownerFromNodeId(source);
160
+ if (fromSource)
161
+ return fromSource;
162
+ const raw = (input.rawFailureCategory ?? "").toLowerCase();
163
+ const nodeId = (input.nodeId ?? "").toLowerCase();
164
+ const normalized = input.normalizedFailureCategory ??
165
+ normalizeDagFailureCategory(input.rawFailureCategory);
166
+ // 2. verification-config signals.
167
+ if (raw.includes("verification-plan-stale") ||
168
+ raw.includes("verification-plan") ||
169
+ raw.includes("frozen") ||
170
+ raw.includes("drift") ||
171
+ raw.includes("entrypoint") ||
172
+ nodeId.includes("verification-config")) {
173
+ return "verification-config";
174
+ }
175
+ // 3. environment signals.
176
+ if (raw.includes("env-failure") ||
177
+ raw.includes("env failure") ||
178
+ raw.includes("flaky") ||
179
+ raw.includes("dependency") ||
180
+ raw.includes("network") ||
181
+ raw.includes("rate-limit") ||
182
+ raw.includes("rate_limit") ||
183
+ raw.includes("quota") ||
184
+ raw.includes("unavailable") ||
185
+ normalized === "executor") {
186
+ return "environment";
187
+ }
188
+ // 4. scope signals.
189
+ if (raw.includes("path") ||
190
+ raw.includes("forbidden") ||
191
+ raw.includes("write-guard") ||
192
+ raw.includes("write_guard") ||
193
+ raw.includes("writeset") ||
194
+ normalized === "write-guard") {
195
+ return "scope";
196
+ }
197
+ if (normalized === "success")
198
+ return "unknown";
199
+ const routing = routeDagFailure({
200
+ rawFailureCategory: input.rawFailureCategory,
201
+ normalizedFailureCategory: normalized,
202
+ nodeId: input.nodeId,
203
+ });
204
+ return ownerFromProductLine(routing.productLineFailureCategory);
205
+ }
206
+ /**
207
+ * Deterministic owner → restart phase mapping. `environment` and `unknown`
208
+ * have no restart phase (fail closed to `undefined`); `verification-config`
209
+ * and `scope` restart from contract (the only rebuild path — new-dag from
210
+ * contract, §6.1.2). The parameter accepts a legacy/unknown string so existing
211
+ * shell-executor call sites that pass legacy owner names still typecheck and
212
+ * fail closed to `undefined`.
213
+ */
214
+ export function classifyRestartPhase(owner) {
215
+ switch (owner) {
216
+ case "contract":
217
+ return "contract";
218
+ case "scout":
219
+ return "scout";
220
+ case "plan":
221
+ return "plan";
222
+ case "implementation":
223
+ return "implement";
224
+ case "verification-config":
225
+ case "scope":
226
+ return "contract";
227
+ case "environment":
228
+ case "unknown":
229
+ return undefined;
230
+ default:
231
+ return undefined;
232
+ }
233
+ }
234
+ /**
235
+ * A+B (AC-009): deterministic issueCategory → restart phase mapping (§4.10).
236
+ * `approved-design-defect` restarts at `plan` (design decisions live in the
237
+ * plan phase); `unknown` fails closed to `undefined` so the runtime classifier
238
+ * never trusts an untyped category.
239
+ */
240
+ export function classifyIssueCategoryRestartPhase(issueCategory) {
241
+ switch (issueCategory) {
242
+ case "implementation-mismatch":
243
+ return "implement";
244
+ case "approved-design-defect":
245
+ return "plan";
246
+ case "target-surface-defect":
247
+ return "scout";
248
+ case "contract-requirement-gap":
249
+ return "contract";
250
+ case "unknown":
251
+ default:
252
+ return undefined;
253
+ }
254
+ }
255
+ function factPhaseOf(factId) {
256
+ const id = factId.toLowerCase();
257
+ if (id.includes("contract"))
258
+ return "contract";
259
+ if (id.includes("scout"))
260
+ return "scout";
261
+ if (id.includes("plan"))
262
+ return "plan";
263
+ if (id.includes("implement"))
264
+ return "implement";
265
+ return undefined;
266
+ }
267
+ /**
268
+ * Deterministic fact invalidation matrix. Restarting at a phase invalidates
269
+ * that phase and everything downstream, while upstream facts stay reusable.
270
+ * Unknown fact ids fail closed to `invalidated`. At the exact restart phase,
271
+ * a committed candidate survives (reusable); an uncommitted partial write does
272
+ * not.
273
+ */
274
+ export function evaluateFactInvalidation(input) {
275
+ const factPhase = factPhaseOf(input.factId);
276
+ if (factPhase === undefined)
277
+ return "invalidated";
278
+ const factIndex = RESTART_PHASE_ORDER.indexOf(factPhase);
279
+ const restartIndex = RESTART_PHASE_ORDER.indexOf(input.restartPhase);
280
+ if (factIndex < restartIndex)
281
+ return "reusable";
282
+ if (factIndex > restartIndex)
283
+ return "invalidated";
284
+ return input.commitState === "committed-candidate" ? "reusable" : "invalidated";
285
+ }
286
+ /**
287
+ * Hash/fingerprint-driven frozen command reuse check. Unknown/malformed
288
+ * command hash → refuse (fail-closed). Any invalidated input fact (by matrix
289
+ * or explicit set) → refuse. All facts reusable + known hash → reuse.
290
+ */
291
+ export function validateFrozenCommandReuse(input) {
292
+ if (!/^[a-f0-9]{64}$/.test(input.commandHash)) {
293
+ return {
294
+ decision: "refuse",
295
+ reason: "unknown or malformed command hash; fail-closed",
296
+ };
297
+ }
298
+ const explicitlyInvalidated = new Set(input.invalidatedFactIds);
299
+ for (const factId of input.inputFactIds) {
300
+ const disposition = evaluateFactInvalidation({
301
+ factId,
302
+ restartPhase: input.restartPhase,
303
+ commitState: input.commitState,
304
+ });
305
+ if (disposition === "invalidated" || explicitlyInvalidated.has(factId)) {
306
+ return {
307
+ decision: "refuse",
308
+ reason: `input fact ${factId} is invalidated; frozen command cannot be reused`,
309
+ };
310
+ }
311
+ }
312
+ return {
313
+ decision: "reuse",
314
+ reason: "all input facts reusable and command hash is known",
315
+ };
316
+ }
317
+ /* ------------------------------------------------------------------ *
318
+ * Failure capsule (AC-001). *
319
+ * ------------------------------------------------------------------ */
320
+ export const failureCapsuleV1Schema = z
321
+ .object({
322
+ schemaVersion: z.literal(1),
323
+ parentRunId: z.string().min(1),
324
+ operationState: frontendOperationStateSchema,
325
+ operationTerminal: frontendOperationTerminalSchema.optional(),
326
+ recoveryMode: recoveryModeSchema,
327
+ protocolFailureReason: frontendNodeProtocolFailureReasonSchema.optional(),
328
+ failureOwner: failureOwnerSchema,
329
+ restartPhase: restartPhaseSchema.optional(),
330
+ modelSuggestedRestartPhase: restartPhaseSchema.optional(),
331
+ invalidatedFactIds: z.array(z.string().min(1)),
332
+ reusableFactIds: z.array(z.string().min(1)),
333
+ evidenceRefs: z.array(z.string().min(1)),
334
+ normalizedFingerprint: hash64,
335
+ })
336
+ .strict()
337
+ .superRefine((value, ctx) => {
338
+ // Cross-field invariant 1: terminal state ⇔ matching terminal present;
339
+ // non-terminal state ⇔ terminal absent.
340
+ if (isFrontendOperationTerminalState(value.operationState)) {
341
+ if (value.operationTerminal === undefined) {
342
+ ctx.addIssue({
343
+ code: z.ZodIssueCode.custom,
344
+ message: `terminal operation state ${value.operationState} requires operationTerminal`,
345
+ path: ["operationTerminal"],
346
+ });
347
+ }
348
+ else if (value.operationTerminal !== value.operationState) {
349
+ ctx.addIssue({
350
+ code: z.ZodIssueCode.custom,
351
+ message: `operationTerminal must equal operationState for terminal state (${value.operationState})`,
352
+ path: ["operationTerminal"],
353
+ });
354
+ }
355
+ }
356
+ else if (value.operationTerminal !== undefined) {
357
+ ctx.addIssue({
358
+ code: z.ZodIssueCode.custom,
359
+ message: `non-terminal operation state ${value.operationState} must not carry operationTerminal`,
360
+ path: ["operationTerminal"],
361
+ });
362
+ }
363
+ // Cross-field invariant 2: continuation/new-dag require a restart phase;
364
+ // all other modes forbid one.
365
+ const modeRequiresRestart = value.recoveryMode === "continuation" || value.recoveryMode === "new-dag";
366
+ if (modeRequiresRestart && value.restartPhase === undefined) {
367
+ ctx.addIssue({
368
+ code: z.ZodIssueCode.custom,
369
+ message: `recoveryMode ${value.recoveryMode} requires restartPhase`,
370
+ path: ["restartPhase"],
371
+ });
372
+ }
373
+ if (!modeRequiresRestart && value.restartPhase !== undefined) {
374
+ ctx.addIssue({
375
+ code: z.ZodIssueCode.custom,
376
+ message: `recoveryMode ${value.recoveryMode} must not carry restartPhase`,
377
+ path: ["restartPhase"],
378
+ });
379
+ }
380
+ // Cross-field invariant 3: when present, restartPhase must equal the
381
+ // runtime classifier output (a model suggestion never overrides it).
382
+ if (value.restartPhase !== undefined) {
383
+ const expectedRestartPhase = classifyRestartPhase(value.failureOwner);
384
+ if (value.restartPhase !== expectedRestartPhase) {
385
+ ctx.addIssue({
386
+ code: z.ZodIssueCode.custom,
387
+ message: `restartPhase must equal runtime classifier output (${expectedRestartPhase ?? "undefined"})`,
388
+ path: ["restartPhase"],
389
+ });
390
+ }
391
+ }
392
+ const invalidated = new Set(value.invalidatedFactIds);
393
+ const overlap = value.reusableFactIds.filter((id) => invalidated.has(id));
394
+ if (overlap.length > 0) {
395
+ ctx.addIssue({
396
+ code: z.ZodIssueCode.custom,
397
+ message: `fact ids cannot be both invalidated and reusable: ${overlap.join(",")}`,
398
+ path: ["reusableFactIds"],
399
+ });
400
+ }
401
+ const expectedFingerprint = computeNormalizedFailureFingerprint({
402
+ failureOwner: value.failureOwner,
403
+ protocolFailureReason: value.protocolFailureReason ?? "",
404
+ restartPhase: value.restartPhase,
405
+ invalidatedFactIds: value.invalidatedFactIds,
406
+ });
407
+ if (value.normalizedFingerprint !== expectedFingerprint) {
408
+ ctx.addIssue({
409
+ code: z.ZodIssueCode.custom,
410
+ message: "normalizedFingerprint must match the deterministic fingerprint",
411
+ path: ["normalizedFingerprint"],
412
+ });
413
+ }
414
+ });
415
+ /**
416
+ * Assemble a validated failure capsule. `restartPhase` is derived from the
417
+ * runtime classifier only when the mode requires one; a model suggestion is
418
+ * only recorded, never applied. The schema then enforces the cross-field
419
+ * invariants (terminal/state, mode/phase, classifier authority).
420
+ */
421
+ export function buildFailureCapsule(input) {
422
+ const modeRequiresRestart = input.recoveryMode === "continuation" || input.recoveryMode === "new-dag";
423
+ const restartPhase = modeRequiresRestart
424
+ ? classifyRestartPhase(input.failureOwner)
425
+ : undefined;
426
+ const operationTerminal = isFrontendOperationTerminalState(input.operationState)
427
+ ? input.operationState
428
+ : undefined;
429
+ const invalidatedFactIds = [...new Set(input.invalidatedFactIds ?? [])].sort();
430
+ const reusableFactIds = [...new Set(input.reusableFactIds ?? [])].sort();
431
+ return failureCapsuleV1Schema.parse({
432
+ schemaVersion: 1,
433
+ parentRunId: input.parentRunId,
434
+ operationState: input.operationState,
435
+ ...(operationTerminal !== undefined ? { operationTerminal } : {}),
436
+ recoveryMode: input.recoveryMode,
437
+ ...(input.protocolFailureReason !== undefined
438
+ ? { protocolFailureReason: input.protocolFailureReason }
439
+ : {}),
440
+ failureOwner: input.failureOwner,
441
+ ...(restartPhase !== undefined ? { restartPhase } : {}),
442
+ ...(input.modelSuggestedRestartPhase !== undefined
443
+ ? { modelSuggestedRestartPhase: input.modelSuggestedRestartPhase }
444
+ : {}),
445
+ invalidatedFactIds,
446
+ reusableFactIds,
447
+ evidenceRefs: [...new Set(input.evidenceRefs ?? [])].sort(),
448
+ normalizedFingerprint: computeNormalizedFailureFingerprint({
449
+ failureOwner: input.failureOwner,
450
+ protocolFailureReason: input.protocolFailureReason ?? "",
451
+ restartPhase,
452
+ invalidatedFactIds,
453
+ }),
454
+ });
455
+ }