@tea-agent/loop-agent 0.41.1-beta.0 → 0.42.0-next.2

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 (277) hide show
  1. package/CHANGELOG.md +78 -206
  2. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  3. package/dist/adapters/context-transfer/pi-session.js +61 -0
  4. package/dist/application/dag/generate-task-dag.js +19 -75
  5. package/dist/application/task-lifecycle/advance.js +5 -24
  6. package/dist/application/task-lifecycle/observe.js +17 -171
  7. package/dist/application/task-lifecycle/plan-transitions.js +7 -42
  8. package/dist/application/task-lifecycle/recommendations.js +2 -13
  9. package/dist/build-stamp.json +3 -3
  10. package/dist/cli/command-definitions.js +12 -7
  11. package/dist/cli/program.js +23 -6
  12. package/dist/commands/client-recovery.js +0 -3
  13. package/dist/commands/dag-artifact.js +284 -0
  14. package/dist/commands/dag-context.js +184 -0
  15. package/dist/commands/dag-rerun.js +206 -1
  16. package/dist/commands/init.js +1 -27
  17. package/dist/commands/task-advance.js +0 -19
  18. package/dist/executors/dag-pi-executor.js +266 -3792
  19. package/dist/executors/pi-executor.js +4 -15
  20. package/dist/executors/pi-sdk-executor.js +3 -0
  21. package/dist/executors/shell-executor.js +217 -945
  22. package/dist/executors/shell-write-guard.js +0 -7
  23. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  24. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  25. package/dist/infrastructure/console/context-export-store.js +160 -0
  26. package/dist/infrastructure/console/dir-lock.js +132 -0
  27. package/dist/infrastructure/console/operation-store.js +197 -0
  28. package/dist/infrastructure/harness/artifact-store.js +10 -1
  29. package/dist/infrastructure/harness/atomic-write.js +12 -2
  30. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  31. package/dist/shared/context-transfer.js +418 -0
  32. package/dist/shared/dag-failure-category.js +0 -12
  33. package/dist/shared/openspec-spec.js +4 -70
  34. package/dist/shared/operator/capabilities.js +14 -7
  35. package/dist/shared/operator/safe-run-summary.js +1 -0
  36. package/dist/shared/path-safety.js +93 -0
  37. package/dist/shared/preview.js +28 -4
  38. package/dist/task/config-types.js +80 -35
  39. package/dist/task/contract/adopt.js +0 -4
  40. package/dist/task/contract/import-revision.js +0 -4
  41. package/dist/task/contract/project.js +0 -3
  42. package/dist/task/contract/schema.js +1 -2
  43. package/dist/task/frontend-project-capability.js +20 -203
  44. package/dist/task/runtime.js +2 -5
  45. package/dist/task/source-prepare/build-draft.js +3 -3
  46. package/dist/task/source-prepare/fragment-inventory.js +9 -15
  47. package/dist/task/source-prepare/prepare.js +1 -89
  48. package/dist/task/source-prepare/semantic-intake.js +2 -6
  49. package/dist/task/source-references.js +1 -22
  50. package/dist/task/task-demand-routing.js +3 -0
  51. package/dist/worker/console/chat/chat-event-store.js +2 -2
  52. package/dist/worker/console/chat/pi-runtime.js +3 -2
  53. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  54. package/dist/worker/console/chat/routes.js +13 -4
  55. package/dist/worker/console/chat/semantic-activity.js +12 -11
  56. package/dist/worker/console/chat/session-stats.js +96 -0
  57. package/dist/worker/console/chat/session-store.js +1 -1
  58. package/dist/worker/console/chat/turn-process.js +28 -4
  59. package/dist/worker/console/chat/user-questions.js +1 -1
  60. package/dist/worker/console/console-update-runtime.js +1 -1
  61. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  62. package/dist/worker/console/dag-confirmation.js +1 -1
  63. package/dist/worker/console/dag-execution-receipt.js +1 -1
  64. package/dist/worker/console/doctor.js +1 -1
  65. package/dist/worker/console/draft-store.js +1 -1
  66. package/dist/worker/console/human-gate-token.js +1 -1
  67. package/dist/worker/console/index.js +3 -6
  68. package/dist/worker/console/interview/assessment.js +1 -1
  69. package/dist/worker/console/interview/session.js +1 -1
  70. package/dist/worker/console/operation-runner.js +45 -4
  71. package/dist/worker/console/operation-sse.js +1 -1
  72. package/dist/worker/console/operation-wait.js +1 -1
  73. package/dist/worker/console/operator-actions.js +107 -168
  74. package/dist/worker/console/operator-surface-health.js +1 -1
  75. package/dist/worker/console/operator-user-error.js +169 -0
  76. package/dist/worker/console/pi-plugins.js +60 -1
  77. package/dist/worker/console/routes.js +637 -8
  78. package/dist/worker/console/security.js +35 -0
  79. package/dist/worker/console/server.js +6 -6
  80. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-DZZ8m3PO.js} +1 -1
  81. package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-D6PvaVd-.js} +1 -1
  82. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-B_OTOiI8.js} +1 -1
  83. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-Bv6rqHBg.js} +1 -1
  84. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-B8eHr0oz.js} +1 -1
  85. package/dist/worker/console/static/assets/channel-BU5gOilw.js +1 -0
  86. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-DYwR0im2.js} +1 -1
  87. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-D2WPGqXt.js} +1 -1
  88. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-CQISJ_I7.js} +1 -1
  89. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-C0o2Du1e.js} +1 -1
  90. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-4f8kr-U7.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-D_OWvXzX.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-CasdPz5X.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-Cj8lRrla.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-CUvD1FSd.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-CqLw_eqB.js} +1 -1
  96. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +1 -0
  97. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +1 -0
  98. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-ku-WqJPx.js} +1 -1
  99. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-Bq_PYDhl.js} +1 -1
  100. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-BiGZV641.js} +1 -1
  101. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-C4JRlglF.js} +1 -1
  102. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-B0dNeWYG.js} +1 -1
  103. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-Cj35YvYM.js} +1 -1
  104. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-uzQoJ2-8.js} +1 -1
  105. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-D1a-Buoz.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-XjRrLRSs.js} +1 -1
  107. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-Da_O24cW.js} +1 -1
  108. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-BLJ8jrYU.js} +1 -1
  109. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-B9GrLjM0.js} +1 -1
  110. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-CeJ0TqiK.js} +1 -1
  111. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js} +1 -1
  112. package/dist/worker/console/static/assets/index-H9rFJiGL.css +1 -0
  113. package/dist/worker/console/static/assets/index-xwu9GxEc.js +451 -0
  114. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-DB26i3d8.js} +1 -1
  115. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-BOGRyeZT.js} +1 -1
  116. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-CZRoBO_6.js} +1 -1
  117. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-qlWhJyoB.js} +1 -1
  118. package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-CfUiDDB3.js} +1 -1
  119. package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-BQe6fpqj.js} +5 -5
  120. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-zFzWHw64.js} +1 -1
  121. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-DFGUqjZO.js} +1 -1
  122. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-BB5i0l8Z.js} +1 -1
  123. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-obbVZ_X5.js} +1 -1
  124. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-BwdaNzc1.js} +1 -1
  125. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-CkTJaugh.js} +1 -1
  126. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-BwJ-hIgq.js} +1 -1
  127. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-BzSgmm7w.js} +1 -1
  128. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-Bn3oj5Q1.js} +1 -1
  129. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-dxhGtL8K.js} +1 -1
  130. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +1 -0
  131. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-BLpEInya.js} +2 -2
  132. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +8 -0
  133. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-ChJGYcXy.js} +1 -1
  134. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-DK-qjRer.js} +1 -1
  135. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-3qeaWAg-.js} +1 -1
  136. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-CootlyP9.js} +1 -1
  137. package/dist/worker/console/static/index.html +7 -2
  138. package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
  139. package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
  140. package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
  141. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
  142. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
  143. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  144. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
  145. package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
  146. package/dist/worker/console/workspace-context.js +28 -112
  147. package/dist/worker/console/workspace-registry.js +2 -2
  148. package/dist/worker/continuation/worker-continuation.js +278 -0
  149. package/dist/worker/observability/read-model.js +4 -0
  150. package/dist/worker/observe/health.js +1 -1
  151. package/dist/worker/observe/node-transparency.js +572 -0
  152. package/dist/worker/observe/routes.js +51 -1
  153. package/dist/worker/observe/static/api.js +69 -5
  154. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  155. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  156. package/dist/worker/observe/static/dag-node-purpose.js +0 -5
  157. package/dist/worker/observe/static/state.js +3 -1
  158. package/dist/worker/observe/static/styles.css +1037 -126
  159. package/dist/worker/observe/static/views/dag-inspector.js +1217 -65
  160. package/dist/worker/observe/static/views/session-timeline.js +121 -25
  161. package/dist/worker/outcomes/adapters.js +19 -0
  162. package/dist/worker/outcomes/types.js +1 -0
  163. package/dist/worker/pool/attempt-lease.js +97 -66
  164. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  165. package/dist/worker/pool/reconcile.js +46 -1
  166. package/dist/worker/pool/run-store.js +2 -0
  167. package/dist/worker/pool/state-projection.js +2 -0
  168. package/dist/worker/task-spec/workflow-routing.js +8 -3
  169. package/dist/workflows/dag/artifact-bindings.js +149 -0
  170. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  171. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +99 -4
  172. package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
  173. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  174. package/dist/workflows/dag/backend-test-writer-completeness.js +45 -3
  175. package/dist/workflows/dag/budget-enforcement.js +10 -0
  176. package/dist/workflows/dag/context-receipt.js +305 -0
  177. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  178. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  179. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  180. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  181. package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
  182. package/dist/workflows/dag/frontend-plan-render.js +1 -2
  183. package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
  184. package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
  185. package/dist/workflows/dag/frontend-recovery-run.js +20 -33
  186. package/dist/workflows/dag/frontend-repair.js +432 -1
  187. package/dist/workflows/dag/frontend-review-context.js +15 -261
  188. package/dist/workflows/dag/frontend-verification-trace.js +24 -249
  189. package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
  190. package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
  191. package/dist/workflows/dag/init-hybrid.js +740 -718
  192. package/dist/workflows/dag/interrupt-request.js +0 -7
  193. package/dist/workflows/dag/node-execution.js +136 -759
  194. package/dist/workflows/dag/path-safety.js +1 -0
  195. package/dist/workflows/dag/prompt.js +30 -38
  196. package/dist/workflows/dag/report.js +1 -37
  197. package/dist/workflows/dag/rerun-feedback.js +1 -256
  198. package/dist/workflows/dag/rerun-plan.js +557 -11
  199. package/dist/workflows/dag/rerun-run.js +601 -38
  200. package/dist/workflows/dag/rerun-task.js +0 -29
  201. package/dist/workflows/dag/retry-policy.js +109 -214
  202. package/dist/workflows/dag/runner.js +142 -430
  203. package/dist/workflows/dag/scheduler.js +16 -114
  204. package/dist/workflows/dag/skill-snapshot.js +17 -0
  205. package/dist/workflows/dag/types.js +195 -246
  206. package/dist/workflows/dag/validate.js +12 -28
  207. package/docs/architecture/runtime-boundaries.md +6 -6
  208. package/docs/architecture/worker-and-feature.md +1 -1
  209. package/docs/init-surface.manifest.json +12 -30
  210. package/docs/skills/vetted-skill-registry.md +2 -4
  211. package/docs/templates/README.md +0 -2
  212. package/docs/templates/agent-dag-report.schema.json +2 -8
  213. package/docs/templates/agent-dag.schema.json +33 -1
  214. package/docs/templates/backend-test-dag.json +18 -18
  215. package/docs/templates/frontend-implementation-contract.schema.json +1 -4
  216. package/docs/templates/product-line/task.yaml +1 -1
  217. package/harness.json +3 -3
  218. package/package.json +3 -1
  219. package/skills/frontend-bounded-implement/SKILL.md +14 -15
  220. package/skills/frontend-design-review/SKILL.md +41 -22
  221. package/skills/frontend-implementation/SKILL.md +52 -0
  222. package/skills/frontend-implementation/references/code-standards.md +33 -0
  223. package/skills/frontend-implementation/references/design-spec.md +56 -0
  224. package/skills/frontend-implementation/references/node-contracts.md +31 -0
  225. package/skills/frontend-review/SKILL.md +15 -20
  226. package/skills/frontend-review/references/review-findings.md +7 -6
  227. package/skills/frontend-verification/SKILL.md +1 -1
  228. package/skills/loop-agent/references/command-reference.md +6 -1
  229. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  230. package/dist/commands/dag-follow-up.js +0 -138
  231. package/dist/executors/pi-read-budget-policy.js +0 -239
  232. package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
  233. package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
  234. package/dist/worker/console/operation-store.js +0 -389
  235. package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
  236. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
  237. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
  238. package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
  239. package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
  240. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
  241. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
  242. package/dist/worker/console/static-src/active-run-badge.js +0 -17
  243. package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
  244. package/dist/workflows/dag/dag-retry-schema.js +0 -138
  245. package/dist/workflows/dag/frontend-closeout.js +0 -221
  246. package/dist/workflows/dag/frontend-design-policy.js +0 -400
  247. package/dist/workflows/dag/frontend-human-decision.js +0 -182
  248. package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
  249. package/dist/workflows/dag/frontend-read-budgets.js +0 -12
  250. package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
  251. package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
  252. package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
  253. package/dist/workflows/dag/frontend-review-findings.js +0 -270
  254. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
  255. package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
  256. package/dist/workflows/dag/frontend-shape-facts.js +0 -409
  257. package/dist/workflows/dag/frontend-shape.js +0 -427
  258. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
  259. package/dist/workflows/dag/frontend-split-application-service.js +0 -203
  260. package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
  261. package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
  262. package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
  263. package/dist/workflows/dag/frontend-writer-admission.js +0 -285
  264. package/dist/workflows/dag/frontend-writer-status.js +0 -256
  265. package/dist/workflows/dag/recovery-lease.js +0 -80
  266. package/docs/templates/frontend-implementation-dag.json +0 -89
  267. package/docs/templates/spec-registry.schema.json +0 -45
  268. package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
  269. package/skills/frontend-contract/SKILL.md +0 -23
  270. package/skills/frontend-contract/references/contract-protocol.md +0 -34
  271. package/skills/frontend-plan/SKILL.md +0 -26
  272. package/skills/frontend-plan/references/decision-contract.md +0 -37
  273. package/skills/frontend-plan/references/design-decisions.md +0 -17
  274. package/skills/frontend-scout/SKILL.md +0 -25
  275. package/skills/frontend-scout/references/design-evidence.md +0 -16
  276. package/skills/frontend-scout/references/scout-evidence.md +0 -23
  277. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -51,6 +51,27 @@ function downloadTextFile(filename, text) {
51
51
  URL.revokeObjectURL(url);
52
52
  }
53
53
 
54
+ /** 抓取页面真实加载的样式表原文(导出内联用)。
55
+ * 不猜路径:直接取当前文档 <link rel="stylesheet"> 的 href(相对当前页面解析),
56
+ * 反代/子路径部署下也和页面渲染用的是同一份 CSS;fetch 失败时返回 "",
57
+ * 调用方回退 EXPORT_FALLBACK_CSS。 */
58
+ export async function fetchPageStylesheetText() {
59
+ try {
60
+ const links = [...document.querySelectorAll('link[rel="stylesheet"]')];
61
+ const texts = [];
62
+ for (const link of links) {
63
+ const href = link.getAttribute("href");
64
+ if (!href) continue;
65
+ const url = new URL(href, document.baseURI).href;
66
+ const res = await fetch(url, { cache: "no-cache" });
67
+ if (res.ok) texts.push(await res.text());
68
+ }
69
+ return texts.join("\n");
70
+ } catch {
71
+ return "";
72
+ }
73
+ }
74
+
54
75
  /** 兜底样式:仅在无法抓取页面 /inspect/styles.css 时使用(与页面 process-timeline 排版对齐)。 */
55
76
  const EXPORT_FALLBACK_CSS = `
56
77
  :root{--canvas:#f7f7f4;--surface:#ffffff;--canvas-soft:#fafaf7;--surface-muted:#f0efe9;--ink:#26251e;--body:#5a5852;--muted:#807d72;--muted-soft:#a09c92;--hairline:#e6e5e0;--hairline-strong:#cfcdc4;--orange:#f54e00;--orange-active:#d04200;--blue:#3f6ea4;--green:#1f8a65;--amber:#a66a00;--red:#c72d53;--lavender:#e4daf1;--radius-sm:6px;--space-2:8px;--space-3:12px;--space-5:20px}
@@ -103,19 +124,31 @@ body{margin:0;font-family:-apple-system,"Segoe UI",Roboto,"PingFang SC","Microso
103
124
  .markdown-output p,.markdown-output blockquote,.markdown-output ul,.markdown-output ol{margin:0;overflow-wrap:anywhere}
104
125
  .markdown-output ul,.markdown-output ol{padding-left:22px}
105
126
  .markdown-output blockquote{padding-left:12px}
127
+ .btn{display:inline-flex;align-items:center;gap:6px;min-height:28px;padding:0 12px;border:1px solid var(--hairline-strong);border-radius:var(--radius-sm);background:var(--surface);color:var(--body);font-size:12px;font-weight:600;cursor:pointer;user-select:none}
128
+ .btn-secondary{background:var(--surface-muted)}
129
+ .btn:hover{border-color:var(--orange);color:var(--orange-active)}
106
130
  `;
107
131
 
108
132
  const EXPORT_SHELL_CSS = `
109
- body{margin:0;font-family:-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--canvas)}
110
- .export-shell{max-width:1000px;margin:0 auto;padding:24px 28px;background:var(--surface);min-height:100vh;box-shadow:0 0 0 1px var(--hairline)}
111
- .export-head h1{font-size:15px;color:var(--ink);margin:0;font-weight:700}
112
- .export-sub{color:var(--muted);font-size:12px;margin:4px 0 16px}
133
+ /* 导出文档变量基础:页面样式把这些变量定义在 body[data-shell]/.inspect-workspace-root
134
+ * 下,独立导出的 HTML 既无该属性也无该包裹,变量会整体失效——深色系统的画布
135
+ * 兜底为黑底,叠加黑字后正文不可读。导出文档自带变量定义并强制 light 配色。 */
136
+ :root{color-scheme:light}
137
+ body{margin:0;font-family:-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--canvas,#f7f7f4)}
138
+ .export-shell{max-width:1000px;margin:0 auto;padding:24px 28px;background:var(--surface,#ffffff);min-height:100vh;box-shadow:0 0 0 1px var(--hairline,#e6e5e0)}
139
+ .export-head h1{font-size:15px;color:var(--ink,#26251e);margin:0;font-weight:700}
140
+ .export-sub{color:var(--muted,#807d72);font-size:12px;margin:4px 0 16px}
113
141
  .export-controls{display:flex;justify-content:flex-start;margin:0 0 12px}
114
- .export-toggle{min-height:28px;padding:0 12px;border:1px solid var(--hairline-strong);border-radius:var(--radius-sm);background:var(--surface);color:var(--body);font-size:11px;font-weight:600;cursor:pointer;user-select:none}
115
- .export-toggle:hover,.export-toggle:focus-visible{border-color:var(--orange);color:var(--orange-active);outline:none}
142
+ .export-toggle{min-height:28px;padding:0 12px;border:1px solid var(--hairline-strong,#cfcdc4);border-radius:var(--radius-sm,6px);background:var(--surface,#ffffff);color:var(--body,#5a5852);font-size:11px;font-weight:600;cursor:pointer;user-select:none}
143
+ .export-toggle:hover,.export-toggle:focus-visible{border-color:var(--orange,#f54e00);color:var(--orange-active,#d04200);outline:none}
116
144
  body.hide-protocol .process-timeline-item.process-timeline-protocol{display:none}
117
145
  `;
118
146
 
147
+ /** 导出文档自带的变量基础:与页面 styles.css 的 light 主题保持一致。
148
+ * 即使 fetch 到了页面 CSS,其变量也挂在 body[data-shell=…]/.inspect-workspace-root
149
+ * 选择器下而不生效,必须在导出文档内重新定义。 */
150
+ const EXPORT_ROOT_VARS_CSS = `:root{--canvas:#f7f7f4;--surface:#ffffff;--canvas-soft:#fafaf7;--surface-muted:#f0efe9;--surface-ink:#26251e;--ink:#26251e;--body:#5a5852;--muted:#807d72;--muted-soft:#a09c92;--hairline:#e6e5e0;--hairline-strong:#cfcdc4;--orange:#f54e00;--orange-active:#d04200;--blue:#3f6ea4;--green:#1f8a65;--amber:#a66a00;--red:#c72d53;--lavender:#e4daf1;--mint:#d8ead5;--peach:#f1d8cb;--gold:#d8b166;--radius-sm:6px;--radius-md:8px;--radius-lg:12px;--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px}`;
151
+
119
152
  /**
120
153
  * 把页面渲染出的时间线 innerHTML 封装成自包含 HTML。
121
154
  * @param {string} bodyHTML 页面渲染的 .process-timeline 等真实 DOM 序列化结果
@@ -126,14 +159,18 @@ export function buildPageStyledDoc(bodyHTML, meta = {}, cssText = "") {
126
159
  const now = new Date().toLocaleString();
127
160
  const title = meta.nodeId ? `节点执行过程 · ${meta.nodeId}` : "节点执行过程";
128
161
  const style = cssText.trim()
129
- ? `${cssText}\n\n/* 导出外壳 */\n${EXPORT_SHELL_CSS}`
162
+ ? `${EXPORT_ROOT_VARS_CSS}\n${cssText}\n\n/* 导出外壳 */\n${EXPORT_SHELL_CSS}`
130
163
  : `${EXPORT_FALLBACK_CSS}\n${EXPORT_SHELL_CSS}`;
131
164
  // 有协议行时,在左上角提供一个「展开/隐藏原始协议事件」按钮(与页面一致),默认隐藏。
132
165
  const hasProtocol = bodyHTML.includes("process-timeline-protocol");
133
166
  const controls = hasProtocol
134
167
  ? `<div class="export-controls"><button type="button" id="exportToggleProtocol" class="export-toggle">展开原始协议事件</button></div><script>(function(){var b=document.body,t=document.getElementById('exportToggleProtocol'),on=false;function apply(){b.classList.toggle('hide-protocol',!on);t.textContent=on?'隐藏原始协议事件':'展开原始协议事件';}if(t){t.addEventListener('click',function(){on=!on;apply();});apply();}})();</script>`
135
168
  : "";
136
- return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>${style}</style></head><body><div class="export-shell"><div class="export-head"><h1>${escapeHtml(title)}</h1></div><p class="export-sub">DAG ${escapeHtml(meta.dagRunId ?? "")} · 导出于 ${escapeHtml(now)}</p>${controls}<div class="export-timeline">${bodyHTML}</div></div></body></html>`;
169
+ // body 带 data-shell="observe-console":内联的页面 styles.css 把全部设计令牌
170
+ // (--canvas/--surface/--hairline-strong …) 与部分规则 (body[data-shell] …) 定义在
171
+ // 该作用域下,导出文档带上属性后令牌与前缀规则原生命中,与页面渲染一致。
172
+ // EXPORT_ROOT_VARS_CSS 仍保留:fetch 页面 CSS 失败的兜底路径令牌不缺。
173
+ return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>${style}</style></head><body data-shell="observe-console"><div class="export-shell"><div class="export-head"><h1>${escapeHtml(title)}</h1></div><p class="export-sub">DAG ${escapeHtml(meta.dagRunId ?? "")} · 导出于 ${escapeHtml(now)}</p>${controls}<div class="export-timeline">${bodyHTML}</div></div></body></html>`;
137
174
  }
138
175
 
139
176
  export function sessionEventResult(event) {
@@ -586,6 +623,35 @@ export function buildRichTimelineItems(events, options = {}) {
586
623
  return items;
587
624
  }
588
625
 
626
+ const EXECUTION_RECORD_FILTERS = Object.freeze([
627
+ ["all", "全部"],
628
+ ["model", "模型消息"],
629
+ ["tools", "工具与进程"],
630
+ ]);
631
+
632
+ function normalizeExecutionRecordFilter(value) {
633
+ return value === "model" || value === "tools" ? value : "all";
634
+ }
635
+
636
+ export function filterExecutionRecordItems(items, filter) {
637
+ const normalized = normalizeExecutionRecordFilter(filter);
638
+ if (normalized === "all") return items;
639
+ return items.filter((item) => {
640
+ const protocolEvents = Array.isArray(item.protocolEvents)
641
+ ? item.protocolEvents
642
+ : [item.event];
643
+ const toolProtocol =
644
+ item.kind === "protocol" &&
645
+ protocolEvents.some((event) => {
646
+ const role = event?.message?.role ?? event?.role;
647
+ return role === "tool" || role === "toolResult";
648
+ });
649
+ const modelFact =
650
+ item.kind === "turn" || (item.kind === "protocol" && !toolProtocol);
651
+ return normalized === "model" ? modelFact : !modelFact;
652
+ });
653
+ }
654
+
589
655
  function ensureOpenDetailsSet() {
590
656
  if (!(uiState.sessionTimelineOpenDetails instanceof Set)) {
591
657
  uiState.sessionTimelineOpenDetails = new Set();
@@ -829,7 +895,26 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
829
895
  }
830
896
 
831
897
  const showProtocol = Boolean(uiState.sessionTimelineShowProtocol);
898
+ const activeFilter = normalizeExecutionRecordFilter(
899
+ uiState.executionRecordFilter,
900
+ );
832
901
  const toolbar = el("div", "process-timeline-toolbar");
902
+ const filterGroup = el("div", "process-timeline-filter-group");
903
+ filterGroup.setAttribute("role", "group");
904
+ filterGroup.setAttribute("aria-label", "筛选执行记录");
905
+ for (const [filter, label] of EXECUTION_RECORD_FILTERS) {
906
+ const button = document.createElement("button");
907
+ button.type = "button";
908
+ button.className = "process-timeline-toggle process-timeline-filter";
909
+ button.setAttribute("aria-pressed", String(activeFilter === filter));
910
+ button.textContent = label;
911
+ button.addEventListener("click", () => {
912
+ uiState.executionRecordFilter = filter;
913
+ rerenderSessionTimeline(content, nodeId, options);
914
+ });
915
+ filterGroup.appendChild(button);
916
+ }
917
+ toolbar.appendChild(filterGroup);
833
918
  const protocolToggle = document.createElement("button");
834
919
  protocolToggle.type = "button";
835
920
  protocolToggle.className = "process-timeline-toggle";
@@ -839,25 +924,13 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
839
924
  : "展开原始协议事件";
840
925
  protocolToggle.addEventListener("click", () => {
841
926
  uiState.sessionTimelineShowProtocol = !showProtocol;
842
- const parent = content;
843
- const scrollTop = parent.scrollTop;
844
- const scrollLeft = parent.scrollLeft;
845
- while (parent.firstChild) parent.removeChild(parent.firstChild);
846
- renderSessionTimeline(parent, nodeId, options);
847
- parent.scrollTop = scrollTop;
848
- parent.scrollLeft = scrollLeft;
927
+ rerenderSessionTimeline(content, nodeId, options);
849
928
  });
850
929
  toolbar.appendChild(protocolToggle);
851
930
  const exportBtn = el("button", "btn btn-secondary", "导出 HTML");
852
931
  exportBtn.type = "button";
853
932
  exportBtn.addEventListener("click", async () => {
854
- let cssText = "";
855
- try {
856
- const res = await fetch("/inspect/styles.css", { cache: "no-cache" });
857
- if (res.ok) cssText = await res.text();
858
- } catch {
859
- /* 忽略:导出仍会用内联兜底样式 */
860
- }
933
+ const cssText = await fetchPageStylesheetText();
861
934
  // 复用页面真实渲染(含原始协议事件),保证导出的排版与页面一致。
862
935
  const prevShow = uiState.sessionTimelineShowProtocol;
863
936
  uiState.sessionTimelineShowProtocol = true;
@@ -879,9 +952,10 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
879
952
  toolbar.appendChild(exportBtn);
880
953
  content.appendChild(toolbar);
881
954
 
882
- const timelineItems = buildRichTimelineItems(visible.events, {
883
- showProtocol,
884
- });
955
+ const timelineItems = filterExecutionRecordItems(
956
+ buildRichTimelineItems(visible.events, { showProtocol }),
957
+ activeFilter,
958
+ );
885
959
  const hasEventTimes = visible.events.some((event) =>
886
960
  Boolean(sessionEventTimestamp(event)),
887
961
  );
@@ -895,6 +969,13 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
895
969
  );
896
970
  }
897
971
 
972
+ if (timelineItems.length === 0) {
973
+ content.appendChild(
974
+ el("p", "empty", "当前筛选下没有可显示的执行记录。"),
975
+ );
976
+ return;
977
+ }
978
+
898
979
  const ul = el("ul", "process-timeline process-timeline-rich");
899
980
  for (let index = 0; index < timelineItems.length; index += 1) {
900
981
  const item = timelineItems[index];
@@ -973,3 +1054,18 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
973
1054
  content.scrollTop = timelineState.scrollTop;
974
1055
  }
975
1056
  }
1057
+
1058
+ function rerenderSessionTimeline(content, nodeId, options) {
1059
+ const scrollTop = content.scrollTop;
1060
+ const scrollLeft = content.scrollLeft;
1061
+ const preserved = [...content.children].filter((child) =>
1062
+ String(child.className ?? "")
1063
+ .split(/\s+/)
1064
+ .includes("dag-inspector-attempt-bar"),
1065
+ );
1066
+ while (content.firstChild) content.removeChild(content.firstChild);
1067
+ for (const child of preserved) content.appendChild(child);
1068
+ renderSessionTimeline(content, nodeId, options);
1069
+ content.scrollTop = scrollTop;
1070
+ content.scrollLeft = scrollLeft;
1071
+ }
@@ -98,6 +98,24 @@ class FrontendImplementationAdapter {
98
98
  };
99
99
  }
100
100
  }
101
+ /**
102
+ * `backend-test-analysis` — read-only requirements/API analysis. It produces
103
+ * the strict Backend Test Analysis v2 contract without claiming that tests or
104
+ * a real integration ran.
105
+ */
106
+ class BackendTestAnalysisAdapter {
107
+ workflow = "backend-test-analysis";
108
+ project(input) {
109
+ return {
110
+ artifacts: [
111
+ ...canonicalArtifacts(input),
112
+ ...structuredArtifactsFromReport(input.dagReportRun),
113
+ ],
114
+ acceptanceCoverage: [...input.acceptanceRefs],
115
+ integrationStatus: { mock: false, real: false },
116
+ };
117
+ }
118
+ }
101
119
  /**
102
120
  * `backend-test` — generates + executes backend tests. Integration status is
103
121
  * `real: true` only when the report completed (a real test execution); mock is
@@ -139,6 +157,7 @@ class FrontendTestAdapter {
139
157
  const ADAPTERS = new Map([
140
158
  ["agent-dag", new AgentDagAdapter()],
141
159
  ["frontend-implementation", new FrontendImplementationAdapter()],
160
+ ["backend-test-analysis", new BackendTestAnalysisAdapter()],
142
161
  ["backend-test", new BackendTestAdapter()],
143
162
  ["frontend-test", new FrontendTestAdapter()],
144
163
  ]);
@@ -24,6 +24,7 @@ export const outcomeFailureCategorySchema = z.enum([
24
24
  export const OUTCOME_WORKFLOWS = [
25
25
  "agent-dag",
26
26
  "frontend-implementation",
27
+ "backend-test-analysis",
27
28
  "backend-test",
28
29
  "frontend-test",
29
30
  ];
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { mkdir, readFile, readdir, unlink } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { acquireDirLock } from "../../infrastructure/console/dir-lock.js";
4
5
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
5
6
  import { assertSafeTaskPoolId, getTaskPoolRoot, } from "./run-store.js";
6
7
  import { TASK_POOL_STATE_ERROR_CODES, TaskPoolStateError } from "./types.js";
@@ -19,6 +20,20 @@ export function getAttemptLeasePath(repoRoot, featureId, taskId) {
19
20
  }
20
21
  return leasePath;
21
22
  }
23
+ function getAttemptLeaseMutationLockPath(repoRoot, featureId, taskId) {
24
+ assertSafeTaskPoolId(featureId, "featureId");
25
+ assertSafeTaskPoolId(taskId, "taskId");
26
+ return path.join(getAttemptLeasesRoot(repoRoot), "_locks", `${featureId}__${taskId}.lock`);
27
+ }
28
+ async function withAttemptLeaseMutationLock(repoRoot, featureId, taskId, operation) {
29
+ const lock = await acquireDirLock(getAttemptLeaseMutationLockPath(repoRoot, featureId, taskId));
30
+ try {
31
+ return await operation();
32
+ }
33
+ finally {
34
+ await lock.release();
35
+ }
36
+ }
22
37
  function isLeaseRecord(value) {
23
38
  if (!value || typeof value !== "object" || Array.isArray(value))
24
39
  return false;
@@ -30,7 +45,11 @@ function isLeaseRecord(value) {
30
45
  typeof record.batchRunId === "string" &&
31
46
  typeof record.token === "string" &&
32
47
  typeof record.acquiredAt === "string" &&
33
- typeof record.renewedAt === "string");
48
+ typeof record.renewedAt === "string" &&
49
+ (record.revisionId === undefined || typeof record.revisionId === "string") &&
50
+ (record.controllerAnchor === undefined ||
51
+ typeof record.controllerAnchor === "string") &&
52
+ (record.claimId === undefined || typeof record.claimId === "string"));
34
53
  }
35
54
  export async function readAttemptLease(repoRoot, featureId, taskId) {
36
55
  const leasePath = getAttemptLeasePath(repoRoot, featureId, taskId);
@@ -47,82 +66,94 @@ export async function readAttemptLease(repoRoot, featureId, taskId) {
47
66
  }
48
67
  /**
49
68
  * Acquire Task-scoped attempt lease before beginWorkerAttempt.
50
- * Same identity+token reuse is idempotent; different active identity → conflict.
69
+ * Same identity+claim reuse is idempotent; a competing admission or active
70
+ * identity conflicts. The Task-scoped mutation lock closes cross-process
71
+ * read-before-rename and release-vs-successor races.
51
72
  */
52
73
  export async function acquireAttemptLease(input) {
53
- const now = input.now ?? new Date();
54
- const existing = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
55
- if (existing) {
56
- if (existing.workerRunId === input.workerRunId &&
57
- existing.batchRunId === input.batchRunId &&
58
- existing.featureId === input.featureId &&
59
- existing.taskId === input.taskId) {
60
- return { status: "idempotent-reuse", lease: existing };
74
+ return withAttemptLeaseMutationLock(input.repoRoot, input.featureId, input.taskId, async () => {
75
+ const now = input.now ?? new Date();
76
+ const existing = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
77
+ if (existing) {
78
+ if (existing.workerRunId === input.workerRunId &&
79
+ existing.batchRunId === input.batchRunId &&
80
+ existing.featureId === input.featureId &&
81
+ existing.taskId === input.taskId &&
82
+ existing.revisionId === input.revisionId &&
83
+ existing.controllerAnchor === input.controllerAnchor &&
84
+ existing.claimId === input.claimId) {
85
+ return { status: "idempotent-reuse", lease: existing };
86
+ }
87
+ throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `attempt-lease-conflict for ${input.featureId}/${input.taskId}: held by ${existing.workerRunId}`);
61
88
  }
62
- throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `attempt-lease-conflict for ${input.featureId}/${input.taskId}: held by ${existing.workerRunId}`);
63
- }
64
- const lease = {
65
- schemaVersion: 1,
66
- featureId: input.featureId,
67
- taskId: input.taskId,
68
- workerRunId: input.workerRunId,
69
- batchRunId: input.batchRunId,
70
- acquiredAt: now.toISOString(),
71
- renewedAt: now.toISOString(),
72
- token: `atk-${randomBytes(8).toString("hex")}`,
73
- ownerProcess: { pid: process.pid },
74
- ...(input.revisionId ? { revisionId: input.revisionId } : {}),
75
- ...(input.controllerAnchor
76
- ? { controllerAnchor: input.controllerAnchor }
77
- : {}),
78
- };
79
- await mkdir(path.dirname(getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId)), {
80
- recursive: true,
89
+ const lease = {
90
+ schemaVersion: 1,
91
+ featureId: input.featureId,
92
+ taskId: input.taskId,
93
+ workerRunId: input.workerRunId,
94
+ batchRunId: input.batchRunId,
95
+ acquiredAt: now.toISOString(),
96
+ renewedAt: now.toISOString(),
97
+ token: `atk-${randomBytes(8).toString("hex")}`,
98
+ ownerProcess: { pid: process.pid },
99
+ ...(input.revisionId ? { revisionId: input.revisionId } : {}),
100
+ ...(input.controllerAnchor
101
+ ? { controllerAnchor: input.controllerAnchor }
102
+ : {}),
103
+ ...(input.claimId ? { claimId: input.claimId } : {}),
104
+ };
105
+ await mkdir(path.dirname(getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId)), {
106
+ recursive: true,
107
+ });
108
+ await writeJsonAtomic(getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId), lease, { repoRoot: input.repoRoot });
109
+ const confirmed = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
110
+ if (!confirmed) {
111
+ throw new Error(`failed to persist attempt lease for ${input.featureId}/${input.taskId}`);
112
+ }
113
+ if (confirmed.token !== lease.token) {
114
+ throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `attempt-lease-conflict race for ${input.featureId}/${input.taskId}`);
115
+ }
116
+ return { status: "acquired", lease: confirmed };
81
117
  });
82
- await writeJsonAtomic(getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId), lease, { repoRoot: input.repoRoot });
83
- const confirmed = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
84
- if (!confirmed) {
85
- throw new Error(`failed to persist attempt lease for ${input.featureId}/${input.taskId}`);
86
- }
87
- if (confirmed.workerRunId !== input.workerRunId) {
88
- throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `attempt-lease-conflict race for ${input.featureId}/${input.taskId}`);
89
- }
90
- return { status: "acquired", lease: confirmed };
91
118
  }
92
119
  export async function renewAttemptLease(input) {
93
- const existing = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
94
- if (!existing) {
95
- throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `cannot renew missing lease for ${input.featureId}/${input.taskId}`);
96
- }
97
- if (existing.token !== input.token || existing.workerRunId !== input.workerRunId) {
98
- throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `cannot renew lease: CAS token/identity mismatch for ${input.featureId}/${input.taskId}`);
99
- }
100
- const renewed = {
101
- ...existing,
102
- renewedAt: (input.now ?? new Date()).toISOString(),
103
- };
104
- await writeJsonAtomic(getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId), renewed, { repoRoot: input.repoRoot });
105
- return renewed;
120
+ return withAttemptLeaseMutationLock(input.repoRoot, input.featureId, input.taskId, async () => {
121
+ const existing = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
122
+ if (!existing) {
123
+ throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `cannot renew missing lease for ${input.featureId}/${input.taskId}`);
124
+ }
125
+ if (existing.token !== input.token || existing.workerRunId !== input.workerRunId) {
126
+ throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `cannot renew lease: CAS token/identity mismatch for ${input.featureId}/${input.taskId}`);
127
+ }
128
+ const renewed = {
129
+ ...existing,
130
+ renewedAt: (input.now ?? new Date()).toISOString(),
131
+ };
132
+ await writeJsonAtomic(getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId), renewed, { repoRoot: input.repoRoot });
133
+ return renewed;
134
+ });
106
135
  }
107
136
  /**
108
137
  * Release active lease after terminal/recovery evidence. Moves to lease-history then deletes active locator.
109
138
  */
110
139
  export async function releaseAttemptLease(input) {
111
- const existing = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
112
- if (!existing)
113
- return;
114
- if (existing.token !== input.token || existing.workerRunId !== input.workerRunId) {
115
- throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `cannot release lease: CAS token/identity mismatch for ${input.featureId}/${input.taskId}`);
116
- }
117
- const historyRoot = path.join(getAttemptLeasesRoot(input.repoRoot), "_history");
118
- await mkdir(historyRoot, { recursive: true });
119
- const historyPath = path.join(historyRoot, `${input.featureId}__${input.taskId}__${existing.workerRunId}.json`);
120
- const activePath = getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId);
121
- await writeJsonAtomic(historyPath, {
122
- ...existing,
123
- releasedAt: (input.now ?? new Date()).toISOString(),
124
- }, { repoRoot: input.repoRoot });
125
- await unlink(activePath).catch(() => { });
140
+ await withAttemptLeaseMutationLock(input.repoRoot, input.featureId, input.taskId, async () => {
141
+ const existing = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
142
+ if (!existing)
143
+ return;
144
+ if (existing.token !== input.token || existing.workerRunId !== input.workerRunId) {
145
+ throw new TaskPoolStateError(TASK_POOL_STATE_ERROR_CODES.CONFLICT, `cannot release lease: CAS token/identity mismatch for ${input.featureId}/${input.taskId}`);
146
+ }
147
+ const historyRoot = path.join(getAttemptLeasesRoot(input.repoRoot), "_history");
148
+ await mkdir(historyRoot, { recursive: true });
149
+ const historyPath = path.join(historyRoot, `${input.featureId}__${input.taskId}__${existing.workerRunId}.json`);
150
+ const activePath = getAttemptLeasePath(input.repoRoot, input.featureId, input.taskId);
151
+ await writeJsonAtomic(historyPath, {
152
+ ...existing,
153
+ releasedAt: (input.now ?? new Date()).toISOString(),
154
+ }, { repoRoot: input.repoRoot });
155
+ await unlink(activePath).catch(() => { });
156
+ });
126
157
  }
127
158
  export function decideAttemptLeaseReclaim(input) {
128
159
  if (input.hasTerminalEvidence) {
@@ -19,6 +19,7 @@ export async function beginWorkerAttemptWithLease(input) {
19
19
  ...(input.controllerAnchor
20
20
  ? { controllerAnchor: input.controllerAnchor }
21
21
  : {}),
22
+ ...(input.leaseClaimId ? { claimId: input.leaseClaimId } : {}),
22
23
  ...(input.now ? { now: input.now } : {}),
23
24
  });
24
25
  const begun = await beginWorkerAttempt(input);
@@ -3,6 +3,8 @@ import path from "node:path";
3
3
  import { executeDagReconcileRun } from "../../workflows/dag/reconcile-run.js";
4
4
  import { assessDagRunLiveness, assessDagRunRecoveryEligibility, locateDagRun, readDagRunState, } from "../../workflows/dag/lifecycle.js";
5
5
  import { appendTaskPoolEvent, findRunByWorkerRunId, getTaskPoolRoot, readTaskPoolState, writeTaskPoolState, } from "./run-store.js";
6
+ import { readRunOwner } from "./run-owner-store.js";
7
+ import { readAttemptLease, releaseAttemptLease } from "./attempt-lease.js";
6
8
  const RECOVERABLE_LIVENESS = new Set([
7
9
  "active",
8
10
  "node-quiet",
@@ -110,6 +112,17 @@ export async function reconcileTaskPoolAbandon(input) {
110
112
  idempotent: false,
111
113
  evidencePath: operatorRecovery.evidencePath,
112
114
  });
115
+ const lease = await readAttemptLease(input.repoRoot, input.featureId, input.taskId);
116
+ if (lease?.workerRunId === input.workerRunId) {
117
+ await releaseAttemptLease({
118
+ repoRoot: input.repoRoot,
119
+ featureId: input.featureId,
120
+ taskId: input.taskId,
121
+ token: lease.token,
122
+ workerRunId: input.workerRunId,
123
+ now: input.now,
124
+ });
125
+ }
113
126
  return {
114
127
  schemaVersion: 1,
115
128
  status: "Failed",
@@ -201,7 +214,6 @@ export async function markTaskPoolFailed(input) {
201
214
  };
202
215
  }
203
216
  async function validateWorkerDagAssociation(input) {
204
- const run = await validateWorkerRunOwnership(input);
205
217
  const dagLocated = await locateDagRun(input.repoRoot, input.dagRunId);
206
218
  if (!dagLocated) {
207
219
  throw new Error(`dag run not found: ${input.dagRunId}`);
@@ -210,6 +222,39 @@ async function validateWorkerDagAssociation(input) {
210
222
  if (dagState.runId !== input.dagRunId) {
211
223
  throw new Error(`dag run identity mismatch: expected ${input.dagRunId}, found ${dagState.runId}`);
212
224
  }
225
+ let run = await findRunByWorkerRunId(input.repoRoot, input.workerRunId);
226
+ if (!run) {
227
+ const owner = await readRunOwner(input.repoRoot, input.workerRunId);
228
+ const association = dagState.workerAssociation;
229
+ const authority = dagState.continuation?.workerContinuationAuthority;
230
+ if (!owner ||
231
+ owner.featureId !== input.featureId ||
232
+ owner.taskId !== input.taskId ||
233
+ owner.workerRunId !== input.workerRunId ||
234
+ owner.revisionId === undefined ||
235
+ input.dagRunId !== input.workerRunId ||
236
+ association?.workerRunId !== input.workerRunId ||
237
+ association.featureId !== input.featureId ||
238
+ association.taskId !== input.taskId ||
239
+ authority?.childWorkerRunId !== input.workerRunId ||
240
+ authority.parentWorkerRunId === input.workerRunId ||
241
+ authority.revisionId !== owner.revisionId) {
242
+ throw new Error(`worker run not found: ${input.workerRunId}`);
243
+ }
244
+ run = {
245
+ schemaVersion: 1,
246
+ batchRunId: owner.batchRunId,
247
+ workerRunId: owner.workerRunId,
248
+ featureId: owner.featureId,
249
+ taskId: owner.taskId,
250
+ status: "run-error",
251
+ attemptKind: "artifact-continuation",
252
+ recordedAt: owner.registeredAt,
253
+ };
254
+ }
255
+ else if (run.featureId !== input.featureId || run.taskId !== input.taskId) {
256
+ throw new Error(`worker run ownership mismatch: expected ${input.featureId}/${input.taskId}, found ${run.featureId}/${run.taskId}`);
257
+ }
213
258
  const linked = await proveWorkerDagLink(input.repoRoot, run, input.workerRunId, input.dagRunId);
214
259
  if (!linked) {
215
260
  throw new Error(`worker run ${input.workerRunId} is not associated with dag run ${input.dagRunId}`);
@@ -350,6 +350,8 @@ function eventFromRun(run) {
350
350
  taskId: run.taskId,
351
351
  featureId: run.featureId,
352
352
  status: run.status,
353
+ ...(run.attemptKind ? { attemptKind: run.attemptKind } : {}),
354
+ ...(run.continuation ? { continuation: run.continuation } : {}),
353
355
  };
354
356
  }
355
357
  function isNotFound(error) {
@@ -47,6 +47,8 @@ export function stateFromRun(run) {
47
47
  status: projectTaskPoolStatusFromRun(run),
48
48
  updatedAt: run.recordedAt,
49
49
  workerRunId: run.workerRunId,
50
+ ...(run.attemptKind ? { attemptKind: run.attemptKind } : {}),
51
+ ...(run.continuation ? { continuation: run.continuation } : {}),
50
52
  ...(run.runRecordPath ? { lastRunRecordPath: run.runRecordPath } : {}),
51
53
  ...(run.failure ? { failure: run.failure } : {}),
52
54
  ...(run.failureV2 ? { failureV2: run.failureV2 } : {}),
@@ -11,6 +11,7 @@ export const workflowSchema = z.enum([
11
11
  "agent-dag",
12
12
  "frontend-implementation",
13
13
  "backend-test",
14
+ "backend-test-analysis",
14
15
  "frontend-test",
15
16
  ]);
16
17
  /**
@@ -23,6 +24,7 @@ export const WORKFLOW_TASK_KIND = {
23
24
  "agent-dag": "standard",
24
25
  "frontend-implementation": "frontend-implementation",
25
26
  "backend-test": "backend-test",
27
+ "backend-test-analysis": "backend-test-analysis",
26
28
  "frontend-test": "frontend-test",
27
29
  };
28
30
  /**
@@ -48,7 +50,7 @@ export function legacyDefaultWorkflow(type) {
48
50
  }
49
51
  /** Migration guidance surfaced whenever a legacy QA type lacks an explicit workflow. */
50
52
  export const LEGACY_QA_MIGRATION_GUIDANCE = "Legacy qa-casegen/qa-testcode/qa-execute tasks no longer auto-select a backend or frontend workflow. " +
51
- "Add `execution.workflow` with one of: agent-dag, frontend-implementation, backend-test, frontend-test. " +
53
+ "Add `execution.workflow` with one of: agent-dag, frontend-implementation, backend-test-analysis, backend-test, frontend-test. " +
52
54
  "Use backend-test for backend-only test generation/execution, frontend-test for browser/UI tests, " +
53
55
  "or agent-dag when the QA task is a generic agent study.";
54
56
  export class WorkflowRoutingError extends Error {
@@ -120,10 +122,13 @@ export function validateWorkflowCompatibility(taskSpec) {
120
122
  }
121
123
  const issues = [];
122
124
  const type = taskSpec.type;
123
- if (type === "backend-feature" && explicit !== "agent-dag" && explicit !== "backend-test") {
125
+ if (type === "backend-feature" &&
126
+ explicit !== "agent-dag" &&
127
+ explicit !== "backend-test-analysis" &&
128
+ explicit !== "backend-test") {
124
129
  issues.push({
125
130
  code: "workflow-type-incompatible",
126
- message: `backend-feature is incompatible with workflow "${explicit}" (expected agent-dag or backend-test)`,
131
+ message: `backend-feature is incompatible with workflow "${explicit}" (expected agent-dag, backend-test-analysis, or backend-test)`,
127
132
  path: "execution.workflow",
128
133
  });
129
134
  }