@tea-agent/loop-agent 0.41.1 → 0.42.0-next.10

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 (302) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +397 -5
  3. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  4. package/dist/adapters/context-transfer/pi-session.js +61 -0
  5. package/dist/application/dag/generate-task-dag.js +75 -19
  6. package/dist/application/dag/run-dag.js +41 -0
  7. package/dist/application/task-lifecycle/advance.js +24 -5
  8. package/dist/application/task-lifecycle/observe.js +171 -17
  9. package/dist/application/task-lifecycle/plan-transitions.js +42 -7
  10. package/dist/application/task-lifecycle/recommendations.js +13 -2
  11. package/dist/build-stamp.json +3 -3
  12. package/dist/cli/command-definitions.js +19 -0
  13. package/dist/cli/program.js +29 -2
  14. package/dist/commands/client-recovery.js +3 -0
  15. package/dist/commands/dag-artifact.js +284 -0
  16. package/dist/commands/dag-context.js +184 -0
  17. package/dist/commands/dag-follow-up.js +138 -0
  18. package/dist/commands/dag-rerun-task.js +2 -0
  19. package/dist/commands/dag-rerun.js +206 -1
  20. package/dist/commands/init.js +27 -1
  21. package/dist/commands/task-advance.js +19 -0
  22. package/dist/executors/dag-pi-executor.js +4013 -86
  23. package/dist/executors/pi-executor.js +15 -4
  24. package/dist/executors/pi-read-budget-policy.js +239 -0
  25. package/dist/executors/pi-sdk-executor.js +3 -0
  26. package/dist/executors/shell-executor.js +978 -188
  27. package/dist/executors/shell-write-guard.js +7 -0
  28. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  29. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  30. package/dist/infrastructure/console/context-export-store.js +160 -0
  31. package/dist/infrastructure/console/dir-lock.js +132 -0
  32. package/dist/infrastructure/console/operation-store.js +417 -0
  33. package/dist/infrastructure/harness/artifact-store.js +10 -1
  34. package/dist/infrastructure/harness/atomic-write.js +12 -2
  35. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  36. package/dist/shared/context-transfer.js +418 -0
  37. package/dist/shared/dag-failure-category.js +12 -0
  38. package/dist/shared/openspec-spec.js +70 -4
  39. package/dist/shared/operator/capabilities.js +21 -0
  40. package/dist/shared/operator/safe-run-summary.js +1 -0
  41. package/dist/shared/path-safety.js +93 -0
  42. package/dist/shared/preview.js +28 -4
  43. package/dist/task/config-types.js +107 -6
  44. package/dist/task/contract/adopt.js +4 -0
  45. package/dist/task/contract/import-revision.js +4 -0
  46. package/dist/task/contract/project.js +3 -0
  47. package/dist/task/contract/schema.js +2 -1
  48. package/dist/task/frontend-project-capability.js +203 -20
  49. package/dist/task/runtime.js +5 -2
  50. package/dist/task/source-prepare/build-draft.js +3 -3
  51. package/dist/task/source-prepare/fragment-inventory.js +64 -25
  52. package/dist/task/source-prepare/prepare.js +126 -21
  53. package/dist/task/source-prepare/semantic-intake.js +6 -2
  54. package/dist/task/source-references.js +22 -1
  55. package/dist/task/task-demand-routing.js +3 -0
  56. package/dist/worker/console/chat/chat-event-store.js +2 -2
  57. package/dist/worker/console/chat/model-resolver.js +7 -4
  58. package/dist/worker/console/chat/pi-runtime.js +86 -5
  59. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  60. package/dist/worker/console/chat/routes.js +136 -53
  61. package/dist/worker/console/chat/sdd-data-alignment.js +222 -0
  62. package/dist/worker/console/chat/session-catalog.js +2 -0
  63. package/dist/worker/console/chat/session-store.js +43 -1
  64. package/dist/worker/console/chat/shortcuts.js +18 -2
  65. package/dist/worker/console/chat/user-questions.js +1 -1
  66. package/dist/worker/console/console-update-runtime.js +1 -1
  67. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  68. package/dist/worker/console/dag-confirmation.js +1 -1
  69. package/dist/worker/console/dag-execution-receipt.js +1 -1
  70. package/dist/worker/console/doctor.js +1 -1
  71. package/dist/worker/console/draft-store.js +1 -1
  72. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  73. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  74. package/dist/worker/console/human-gate-token.js +1 -1
  75. package/dist/worker/console/index.js +6 -3
  76. package/dist/worker/console/interview/assessment.js +1 -1
  77. package/dist/worker/console/interview/session.js +1 -1
  78. package/dist/worker/console/operation-runner.js +45 -4
  79. package/dist/worker/console/operation-sse.js +1 -1
  80. package/dist/worker/console/operation-wait.js +1 -1
  81. package/dist/worker/console/operator-actions.js +273 -10
  82. package/dist/worker/console/operator-surface-health.js +1 -1
  83. package/dist/worker/console/operator-user-error.js +169 -0
  84. package/dist/worker/console/pi-plugins.js +57 -0
  85. package/dist/worker/console/pi-readiness.js +3 -2
  86. package/dist/worker/console/routes.js +637 -8
  87. package/dist/worker/console/security.js +35 -0
  88. package/dist/worker/console/server.js +29 -4
  89. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-D7oU983t.js → abnfDiagram-N423BO3Z-CcS17TBr.js} +1 -1
  90. package/dist/worker/console/static/assets/{arc-BrKSRdFS.js → arc-COptKq2S.js} +1 -1
  91. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CiXV7L56.js → architectureDiagram-T3A2C74G-h4LKMHjP.js} +1 -1
  92. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D_Hs_gIG.js → blockDiagram-VBNYF7ZC-COA1MOH0.js} +1 -1
  93. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DbM-Lqqh.js → c4Diagram-5PPSVZJV-DJUf0QPm.js} +1 -1
  94. package/dist/worker/console/static/assets/channel-DdBCaOJ6.js +1 -0
  95. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D1M8uqJw.js → chunk-2GRJ4B5K-mtWfKrUX.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-4D3RFHT6.js → chunk-2Q5K7J3B-B8pXsxDQ.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CdwpY_n9.js → chunk-5RXB4S5H-ipKzByl1.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BLeLZ9PG.js → chunk-5VM5RSS4-DYi3Ald_.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CeRNEMvc.js → chunk-6Q2QTUOP-DQtGYoty.js} +1 -1
  100. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjkHP8Vy.js → chunk-GF5L2VYU-BU0qS2YV.js} +1 -1
  101. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-B54ZUQY-.js → chunk-JWPE2WC7-DVH9dIGE.js} +1 -1
  102. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-4bKaMVmx.js → chunk-KBJHAD2P-Dt60SmgM.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Cyjfznu_.js → chunk-RYQCIY6F-BoaUGuEY.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CDN-zGFV.js → chunk-XXDRQBXY-_WHFDTp2.js} +1 -1
  105. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DZFra1GO.js +1 -0
  106. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DZFra1GO.js +1 -0
  107. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-HxJmz411.js → cose-bilkent-JH36ORCC-BxkbTIRd.js} +1 -1
  108. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DtSt5K2w.js → cynefin-VYW2F7L2-Bf2UVnoG.js} +1 -1
  109. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-VtkSpRmG.js → cynefinDiagram-MW4NZA55-Bo23q1J_.js} +1 -1
  110. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bj4DT4du.js → dagre-VZM6K2ZE-Dd_UU49i.js} +1 -1
  111. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Bpuk6BmP.js → diagram-7IWD3JNH-ebUa1a9y.js} +1 -1
  112. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BHYGv_yO.js → diagram-B4RE2ZJO-BucthU8r.js} +1 -1
  113. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DEx3WwnT.js → diagram-LBJQPF4R-BgqNpAkm.js} +1 -1
  114. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Cj0dS5e8.js → diagram-Q27KOJAE-DC4q5NGa.js} +1 -1
  115. package/dist/worker/console/static/assets/{diagram-UB23O5K3-B97h8l21.js → diagram-UB23O5K3-CBqCaMeb.js} +1 -1
  116. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DMSFAhBd.js → ebnfDiagram-BXEA7PRR-CGtnbQ3-.js} +1 -1
  117. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DhVOCbt6.js → erDiagram-JOGREHBK-CG3LUao5.js} +1 -1
  118. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-GlakITWL.js → flowDiagram-UKHOOZJN-D2ZVgoFS.js} +1 -1
  119. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-VxPpu9eQ.js → ganttDiagram-PKOTCBZU-DoYFZiKt.js} +1 -1
  120. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bs_e5DRT.js → gitGraphDiagram-DS77QQ5N-CKoP1s6j.js} +1 -1
  121. package/dist/worker/console/static/assets/index-CAZ2fC_X.css +1 -0
  122. package/dist/worker/console/static/assets/index-vbTcFnFs.js +449 -0
  123. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BXYuU4Uw.js → infoDiagram-6WML65LV-Duofv8p2.js} +1 -1
  124. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C34erCbU.js → ishikawaDiagram-WSZJBQD7-D2nlCkA1.js} +1 -1
  125. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DZCAiOU3.js → journeyDiagram-NVQOT4AX-Dd4IHdus.js} +1 -1
  126. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C9gaTdUm.js → kanban-definition-27J2QSJJ-Bi20AOdt.js} +1 -1
  127. package/dist/worker/console/static/assets/{linear-CvI0Z17U.js → linear-D59YJ9kB.js} +1 -1
  128. package/dist/worker/console/static/assets/{mermaid.core-DMMSIx7j.js → mermaid.core-Bl13LpNa.js} +5 -5
  129. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BKKP0EO6.js → mindmap-definition-FAOFIHXS-C6DLP5eY.js} +1 -1
  130. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-C3LiFbl5.js → pegDiagram-VL7TDLO6-BjO4cy64.js} +1 -1
  131. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Bb3Om2Dz.js → pieDiagram-7S7Q4E2Y-D7i2qZTG.js} +1 -1
  132. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DNthMjzc.js → quadrantDiagram-CIZ2JOQS-Dx8o_h0y.js} +1 -1
  133. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-miFsr4Tv.js → railroadDiagram-AXF67PYL-B55orI_t.js} +1 -1
  134. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-yXUncmn6.js → requirementDiagram-LRYGKXZP-zWaehp6f.js} +1 -1
  135. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DLeWKLak.js → sankeyDiagram-W5VNT64P-CcA-pjvD.js} +1 -1
  136. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CI3he1R3.js → sequenceDiagram-SI44F4Z6-BOFbzNHI.js} +1 -1
  137. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B7jfDkS5.js → sizeCapture-X5ZJPWSS-BjAejah1.js} +1 -1
  138. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BmrZod7J.js → stateDiagram-OKZ733FA-DXYwxJwZ.js} +1 -1
  139. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Clg3V9t1.js +1 -0
  140. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CDOlHVSY.js → swimlanes-SLNWSIFB-OYiOch8n.js} +2 -2
  141. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DX1dxAAW.js +8 -0
  142. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cz-XX1gy.js → timeline-definition-Z64GVDOM-cZfH3nmU.js} +1 -1
  143. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-caV4cI8C.js → vennDiagram-T6HMQDX7-BDE7b3E1.js} +1 -1
  144. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-dImPlnIO.js → wardleyDiagram-T6FBY63Y-kyyGy9WJ.js} +1 -1
  145. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CtRYVJcn.js → xychartDiagram-ELKLHX3M-CJj6VTog.js} +1 -1
  146. package/dist/worker/console/static/index.html +7 -2
  147. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  148. package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
  149. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
  150. package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
  151. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
  152. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +258 -23
  153. package/dist/worker/console/static-src/operator-chat/useChatThread.js +112 -4
  154. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -4
  155. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
  156. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
  157. package/dist/worker/console/static-src/shell/console-update-reload.js +52 -0
  158. package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
  159. package/dist/worker/console/workspace-context.js +142 -2
  160. package/dist/worker/console/workspace-registry.js +2 -2
  161. package/dist/worker/continuation/worker-continuation.js +278 -0
  162. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  163. package/dist/worker/observe/health.js +1 -1
  164. package/dist/worker/observe/node-transparency.js +572 -0
  165. package/dist/worker/observe/routes.js +55 -1
  166. package/dist/worker/observe/static/api.js +69 -5
  167. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  168. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  169. package/dist/worker/observe/static/dag-helpers.js +4 -2
  170. package/dist/worker/observe/static/dag-node-purpose.js +5 -0
  171. package/dist/worker/observe/static/inspect-workspace.js +23 -0
  172. package/dist/worker/observe/static/kpi.js +2 -2
  173. package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
  174. package/dist/worker/observe/static/operator-chrome.js +37 -23
  175. package/dist/worker/observe/static/relations.js +2 -2
  176. package/dist/worker/observe/static/router.d.ts +12 -1
  177. package/dist/worker/observe/static/router.js +48 -6
  178. package/dist/worker/observe/static/run-processing.js +4 -2
  179. package/dist/worker/observe/static/shell-chrome.js +2 -2
  180. package/dist/worker/observe/static/state.js +8 -1
  181. package/dist/worker/observe/static/styles.css +1092 -86
  182. package/dist/worker/observe/static/views/dag-inspector.js +1228 -64
  183. package/dist/worker/observe/static/views/dag.js +10 -2
  184. package/dist/worker/observe/static/views/dags.js +3 -1
  185. package/dist/worker/observe/static/views/dashboard.js +14 -6
  186. package/dist/worker/observe/static/views/pool.js +7 -2
  187. package/dist/worker/observe/static/views/run.js +12 -3
  188. package/dist/worker/observe/static/views/session-timeline.js +142 -18
  189. package/dist/worker/observe/static/views/task.js +7 -2
  190. package/dist/worker/outcomes/adapters.js +19 -0
  191. package/dist/worker/outcomes/types.js +1 -0
  192. package/dist/worker/pool/attempt-lease.js +97 -66
  193. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  194. package/dist/worker/pool/reconcile.js +46 -1
  195. package/dist/worker/pool/run-store.js +2 -0
  196. package/dist/worker/pool/state-projection.js +2 -0
  197. package/dist/worker/task-spec/workflow-routing.js +8 -3
  198. package/dist/workflows/dag/artifact-bindings.js +149 -0
  199. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  200. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +215 -28
  201. package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
  202. package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
  203. package/dist/workflows/dag/backend-test-scenario-param.js +172 -25
  204. package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
  205. package/dist/workflows/dag/backend-test-writer-completeness.js +104 -12
  206. package/dist/workflows/dag/budget-enforcement.js +10 -0
  207. package/dist/workflows/dag/context-receipt.js +305 -0
  208. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  209. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  210. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  211. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  212. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  213. package/dist/workflows/dag/frontend-closeout.js +221 -0
  214. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  215. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  216. package/dist/workflows/dag/frontend-implementation-contract.js +1237 -192
  217. package/dist/workflows/dag/frontend-plan-render.js +2 -1
  218. package/dist/workflows/dag/frontend-prewrite-gate.js +256 -350
  219. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  220. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  221. package/dist/workflows/dag/frontend-recovery-controller.js +226 -0
  222. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  223. package/dist/workflows/dag/frontend-recovery-plan.js +21 -10
  224. package/dist/workflows/dag/frontend-recovery-run.js +166 -34
  225. package/dist/workflows/dag/frontend-repair.js +1 -432
  226. package/dist/workflows/dag/frontend-review-context.js +261 -15
  227. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  228. package/dist/workflows/dag/frontend-shadow-dual-write.js +941 -0
  229. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  230. package/dist/workflows/dag/frontend-shape-facts.js +419 -0
  231. package/dist/workflows/dag/frontend-shape.js +435 -0
  232. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  233. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  234. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  235. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  236. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  237. package/dist/workflows/dag/frontend-verification-trace.js +252 -25
  238. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  239. package/dist/workflows/dag/frontend-writer-admission.js +319 -0
  240. package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
  241. package/dist/workflows/dag/frontend-writer-status.js +256 -0
  242. package/dist/workflows/dag/init-hybrid.js +1174 -562
  243. package/dist/workflows/dag/interrupt-request.js +7 -0
  244. package/dist/workflows/dag/node-execution.js +940 -31
  245. package/dist/workflows/dag/path-safety.js +1 -0
  246. package/dist/workflows/dag/prompt.js +99 -7
  247. package/dist/workflows/dag/recovery-lease.js +170 -0
  248. package/dist/workflows/dag/report.js +37 -1
  249. package/dist/workflows/dag/rerun-feedback.js +315 -1
  250. package/dist/workflows/dag/rerun-plan.js +568 -3
  251. package/dist/workflows/dag/rerun-run.js +642 -18
  252. package/dist/workflows/dag/rerun-task.js +251 -13
  253. package/dist/workflows/dag/retry-policy.js +219 -104
  254. package/dist/workflows/dag/runner.js +596 -121
  255. package/dist/workflows/dag/scheduler.js +133 -20
  256. package/dist/workflows/dag/skill-snapshot.js +17 -0
  257. package/dist/workflows/dag/types.js +426 -22
  258. package/dist/workflows/dag/validate.js +28 -12
  259. package/docs/architecture/runtime-boundaries.md +6 -6
  260. package/docs/architecture/worker-and-feature.md +1 -1
  261. package/docs/examples/README.md +5 -0
  262. package/docs/init-surface.manifest.json +30 -12
  263. package/docs/skills/vetted-skill-registry.md +4 -2
  264. package/docs/templates/README.md +2 -0
  265. package/docs/templates/agent-dag-report.schema.json +8 -2
  266. package/docs/templates/agent-dag.schema.json +33 -1
  267. package/docs/templates/backend-test-dag.json +23 -20
  268. package/docs/templates/frontend-implementation-contract.schema.json +4 -1
  269. package/docs/templates/frontend-implementation-dag.json +89 -0
  270. package/docs/templates/product-line/task.yaml +1 -1
  271. package/docs/templates/spec-registry.schema.json +45 -0
  272. package/harness.json +1 -1
  273. package/package.json +6 -3
  274. package/skills/frontend-bounded-implement/SKILL.md +15 -14
  275. package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
  276. package/skills/frontend-contract/SKILL.md +23 -0
  277. package/skills/frontend-contract/references/contract-protocol.md +34 -0
  278. package/skills/frontend-design-review/SKILL.md +22 -41
  279. package/skills/frontend-plan/SKILL.md +26 -0
  280. package/skills/frontend-plan/references/decision-contract.md +37 -0
  281. package/skills/frontend-plan/references/design-decisions.md +17 -0
  282. package/skills/frontend-review/SKILL.md +20 -15
  283. package/skills/frontend-review/references/review-findings.md +6 -7
  284. package/skills/frontend-scout/SKILL.md +25 -0
  285. package/skills/frontend-scout/references/design-evidence.md +16 -0
  286. package/skills/frontend-scout/references/scout-evidence.md +23 -0
  287. package/skills/frontend-verification/SKILL.md +1 -1
  288. package/skills/loop-agent/references/command-reference.md +7 -0
  289. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  290. package/dist/worker/console/operation-store.js +0 -169
  291. package/dist/worker/console/static/assets/channel-D1xajGpe.js +0 -1
  292. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BmuGTfRr.js +0 -1
  293. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BmuGTfRr.js +0 -1
  294. package/dist/worker/console/static/assets/index-BLzcAykx.js +0 -451
  295. package/dist/worker/console/static/assets/index-Cs9JUSPs.css +0 -1
  296. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-D9ykKEWP.js +0 -1
  297. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BvPOXALT.js +0 -8
  298. package/skills/frontend-implementation/SKILL.md +0 -52
  299. package/skills/frontend-implementation/references/code-standards.md +0 -33
  300. package/skills/frontend-implementation/references/design-spec.md +0 -56
  301. package/skills/frontend-implementation/references/node-contracts.md +0 -31
  302. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -35,7 +35,12 @@ import {
35
35
  parseMarkdownBlocks,
36
36
  markdownLinkHref,
37
37
  } from "../format.js";
38
- import { parseRoute, navigate, buildHashPath } from "../router.js";
38
+ import {
39
+ parseRoute,
40
+ navigate,
41
+ buildHashPath,
42
+ withWorkspaceQuery,
43
+ } from "../router.js";
39
44
  import {
40
45
  fetchJson,
41
46
  artifactUrl,
@@ -248,6 +253,7 @@ export async function renderDagDetail(dagRunId, initial = true) {
248
253
  uiState.dagInspectorAttempt = null;
249
254
  uiState.dagGraphViewportState = null;
250
255
  uiState.dagTimelineViewportState = null;
256
+ uiState.dagTimelineAutoScrollEnabled = true;
251
257
  }
252
258
  // AC-001:同会话内重新进入同一 dagRunId 保持当前图视图模式(不切回逻辑拓扑)。
253
259
  uiState.dagGraphViewMode = dagGraphViewModes.get(dagRunId) ?? "topology";
@@ -364,7 +370,9 @@ export async function renderDagDetail(dagRunId, initial = true) {
364
370
  "page-action-btn",
365
371
  "在操作台中止此运行 →",
366
372
  );
367
- interruptLink.href = `/#/operate/recovery?dagRunId=${encodeURIComponent(dag.dagRunId)}&intent=interrupt`;
373
+ interruptLink.href = withWorkspaceQuery(
374
+ `/#/operate/recovery?dagRunId=${encodeURIComponent(dag.dagRunId)}&intent=interrupt`,
375
+ );
368
376
  interruptLink.setAttribute("data-testid", "inspect-dag-interrupt-link");
369
377
  interruptLink.title = "只跳转到操作台 Recovery,不会在观测页执行中止";
370
378
  primaryEntries.push(["协作中止", interruptLink]);
@@ -327,7 +327,9 @@ function buildRow(run) {
327
327
 
328
328
  const nameTd = document.createElement("td");
329
329
  const nameLink = el("a", "dags-name-link", run.title || run.dagRunId);
330
- nameLink.href = `#/dag/${encodeURIComponent(run.dagRunId)}`;
330
+ nameLink.href = inspectPathToCanonicalHref(
331
+ `/dag/${encodeURIComponent(run.dagRunId)}`,
332
+ );
331
333
  nameLink.addEventListener("click", (event) => {
332
334
  event.preventDefault();
333
335
  event.stopPropagation();
@@ -35,7 +35,13 @@ import {
35
35
  parseMarkdownBlocks,
36
36
  markdownLinkHref,
37
37
  } from "../format.js";
38
- import { parseRoute, navigate, buildHashPath } from "../router.js";
38
+ import {
39
+ parseRoute,
40
+ navigate,
41
+ buildHashPath,
42
+ inspectPathToCanonicalHref,
43
+ withWorkspaceQuery,
44
+ } from "../router.js";
39
45
  import { fetchJson, artifactUrl, parseArtifactPreviewResponse } from "../api.js";
40
46
  import {
41
47
  objectRouteLink,
@@ -118,14 +124,14 @@ async function renderNightStrip() {
118
124
 
119
125
  const actions = el("div", "night-strip-actions");
120
126
  const detail = el("a", "page-action-btn", "夜间详情");
121
- detail.href = "/#/inspect/night";
127
+ detail.href = inspectPathToCanonicalHref("/night");
122
128
  detail.addEventListener("click", (e) => {
123
129
  e.preventDefault();
124
130
  navigate("/night");
125
131
  });
126
132
  actions.appendChild(detail);
127
133
  const manage = el("a", "page-action-btn night-strip-manage", "晨审处置 →");
128
- manage.href = "/#/operate/night";
134
+ manage.href = withWorkspaceQuery("/#/operate/night");
129
135
  manage.title = "夜间计划的管理与晨审处置在操作台完成(Inspect 保持只读)";
130
136
  actions.appendChild(manage);
131
137
  strip.appendChild(actions);
@@ -528,7 +534,7 @@ export async function renderDashboard(scrollTo) {
528
534
  workerBlock.appendChild(el("div", "risk-total", String(block.total)));
529
535
  workerBlock.appendChild(el("p", "risk-summary", block.summary));
530
536
  const link = el("a", "risk-link", "查看异常 Task →");
531
- link.href = "#/failures";
537
+ link.href = inspectPathToCanonicalHref("/failures");
532
538
  link.addEventListener("click", (event) => {
533
539
  event.preventDefault();
534
540
  navigate("/failures");
@@ -556,7 +562,7 @@ export async function renderDashboard(scrollTo) {
556
562
  const head = el("div", "section-heading-row");
557
563
  head.appendChild(el("h3", "section-heading", UI_TEXT.recentDags));
558
564
  const allLink = el("a", "section-heading-action", UI_TEXT.viewAllDags);
559
- allLink.href = "#/dags?page=1&pageSize=20";
565
+ allLink.href = inspectPathToCanonicalHref("/dags?page=1&pageSize=20");
560
566
  allLink.setAttribute("aria-label", "查看全部 DAG 运行历史");
561
567
  allLink.addEventListener("click", (event) => {
562
568
  event.preventDefault();
@@ -686,7 +692,9 @@ function syncTableRow(row, cells) {
686
692
  function buildDagTimelineItem(dag) {
687
693
  const progress = dagProgress(dag);
688
694
  const item = el("a", "dag-run-timeline-item");
689
- item.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
695
+ item.href = inspectPathToCanonicalHref(
696
+ `/dag/${encodeURIComponent(dag.dagRunId)}`,
697
+ );
690
698
  item.title = dag.title || dag.dagRunId;
691
699
  item.addEventListener("click", (event) => {
692
700
  event.preventDefault();
@@ -8,7 +8,12 @@ import {
8
8
  livenessBadge,
9
9
  truncateText,
10
10
  } from "../format.js";
11
- import { parseRoute, navigate, buildHashPath } from "../router.js";
11
+ import {
12
+ parseRoute,
13
+ navigate,
14
+ buildHashPath,
15
+ inspectPathToCanonicalHref,
16
+ } from "../router.js";
12
17
  import { fetchJson } from "../api.js";
13
18
  import { objectRouteLink } from "../relations.js";
14
19
  import {
@@ -118,7 +123,7 @@ function taskPath(task) {
118
123
 
119
124
  function linkTo(path, label, className) {
120
125
  const a = el("a", className || "object-link", label);
121
- a.href = `#${path}`;
126
+ a.href = inspectPathToCanonicalHref(path);
122
127
  a.addEventListener("click", (e) => {
123
128
  e.preventDefault();
124
129
  e.stopPropagation();
@@ -35,7 +35,12 @@ import {
35
35
  parseMarkdownBlocks,
36
36
  markdownLinkHref,
37
37
  } from "../format.js";
38
- import { parseRoute, navigate, buildHashPath } from "../router.js";
38
+ import {
39
+ parseRoute,
40
+ navigate,
41
+ buildHashPath,
42
+ inspectPathToCanonicalHref,
43
+ } from "../router.js";
39
44
  import {
40
45
  fetchJson,
41
46
  artifactUrl,
@@ -176,7 +181,9 @@ export function renderRunDagProgress(task, snapshot) {
176
181
  "详情",
177
182
  (() => {
178
183
  const a = el("a", "link", "查看 DAG 详情 →");
179
- a.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
184
+ a.href = inspectPathToCanonicalHref(
185
+ `/dag/${encodeURIComponent(dag.dagRunId)}`,
186
+ );
180
187
  a.addEventListener("click", (e) => {
181
188
  e.preventDefault();
182
189
  navigate(`/dag/${encodeURIComponent(dag.dagRunId)}`);
@@ -295,7 +302,9 @@ export function renderRunPanels(task, events, snapshot) {
295
302
  li.appendChild(marker);
296
303
  if (step.label === "run-dag" && dag?.dagRunId) {
297
304
  const link = document.createElement("a");
298
- link.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
305
+ link.href = inspectPathToCanonicalHref(
306
+ `/dag/${encodeURIComponent(dag.dagRunId)}`,
307
+ );
299
308
  link.className = "step-link";
300
309
  link.textContent = "DAG →";
301
310
  link.addEventListener("click", (e) => {
@@ -7,12 +7,72 @@ import { fetchJson } from "../api.js";
7
7
  import {
8
8
  uiState,
9
9
  updateDagTimelineViewportState,
10
- computeFollowLatest,
11
10
  } from "../state.js";
12
11
 
13
12
  /** Default visible lines for tool result preview before expand. */
14
13
  export const TOOL_RESULT_PREVIEW_LINES = 20;
15
14
 
15
+ function isTimelineAutoScrollEnabled() {
16
+ return uiState.dagTimelineAutoScrollEnabled !== false;
17
+ }
18
+
19
+ function scrollTimelineToLatest(content) {
20
+ content.scrollLeft = 0;
21
+ content.scrollTop = content.scrollHeight;
22
+ uiState.dagTimelineViewportState = updateDagTimelineViewportState(
23
+ uiState.dagTimelineViewportState,
24
+ uiState.currentDagRunId,
25
+ content.dataset.nodeId,
26
+ content.scrollLeft,
27
+ content.scrollTop,
28
+ true,
29
+ );
30
+ }
31
+
32
+ /** Floating, explicit live-follow control for the execution-record viewport. */
33
+ export function createTimelineAutoScrollControl(content) {
34
+ const dock = el("div", "process-timeline-autoscroll-dock");
35
+ const button = document.createElement("button");
36
+ button.type = "button";
37
+ const signal = el("span", "process-timeline-autoscroll-signal");
38
+ signal.setAttribute("aria-hidden", "true");
39
+ const icon = el("i", "ri-arrow-down-line");
40
+ icon.setAttribute("aria-hidden", "true");
41
+ button.appendChild(signal);
42
+ button.appendChild(icon);
43
+
44
+ const sync = () => {
45
+ const enabled = isTimelineAutoScrollEnabled();
46
+ const actionLabel = enabled ? "关闭自动滚动" : "自动滚动";
47
+ button.className = `process-timeline-autoscroll${enabled ? " is-active" : ""}`;
48
+ button.setAttribute("aria-pressed", String(enabled));
49
+ button.setAttribute("aria-label", actionLabel);
50
+ button.setAttribute("data-tooltip", actionLabel);
51
+ icon.className = enabled ? "ri-arrow-down-line" : "ri-pause-mini-fill";
52
+ };
53
+
54
+ button.addEventListener("click", () => {
55
+ const enabled = !isTimelineAutoScrollEnabled();
56
+ uiState.dagTimelineAutoScrollEnabled = enabled;
57
+ if (enabled) scrollTimelineToLatest(content);
58
+ else {
59
+ uiState.dagTimelineViewportState = updateDagTimelineViewportState(
60
+ uiState.dagTimelineViewportState,
61
+ uiState.currentDagRunId,
62
+ content.dataset.nodeId,
63
+ content.scrollLeft,
64
+ content.scrollTop,
65
+ false,
66
+ );
67
+ }
68
+ sync();
69
+ });
70
+
71
+ sync();
72
+ dock.appendChild(button);
73
+ return dock;
74
+ }
75
+
16
76
  export function normalizeSessionEventFields(event) {
17
77
  if (!event || typeof event !== "object") return {};
18
78
  const args = event.args ?? event.input ?? event.toolInput;
@@ -623,6 +683,35 @@ export function buildRichTimelineItems(events, options = {}) {
623
683
  return items;
624
684
  }
625
685
 
686
+ const EXECUTION_RECORD_FILTERS = Object.freeze([
687
+ ["all", "全部"],
688
+ ["model", "模型消息"],
689
+ ["tools", "工具与进程"],
690
+ ]);
691
+
692
+ function normalizeExecutionRecordFilter(value) {
693
+ return value === "model" || value === "tools" ? value : "all";
694
+ }
695
+
696
+ export function filterExecutionRecordItems(items, filter) {
697
+ const normalized = normalizeExecutionRecordFilter(filter);
698
+ if (normalized === "all") return items;
699
+ return items.filter((item) => {
700
+ const protocolEvents = Array.isArray(item.protocolEvents)
701
+ ? item.protocolEvents
702
+ : [item.event];
703
+ const toolProtocol =
704
+ item.kind === "protocol" &&
705
+ protocolEvents.some((event) => {
706
+ const role = event?.message?.role ?? event?.role;
707
+ return role === "tool" || role === "toolResult";
708
+ });
709
+ const modelFact =
710
+ item.kind === "turn" || (item.kind === "protocol" && !toolProtocol);
711
+ return normalized === "model" ? modelFact : !modelFact;
712
+ });
713
+ }
714
+
626
715
  function ensureOpenDetailsSet() {
627
716
  if (!(uiState.sessionTimelineOpenDetails instanceof Set)) {
628
717
  uiState.sessionTimelineOpenDetails = new Set();
@@ -866,7 +955,26 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
866
955
  }
867
956
 
868
957
  const showProtocol = Boolean(uiState.sessionTimelineShowProtocol);
958
+ const activeFilter = normalizeExecutionRecordFilter(
959
+ uiState.executionRecordFilter,
960
+ );
869
961
  const toolbar = el("div", "process-timeline-toolbar");
962
+ const filterGroup = el("div", "process-timeline-filter-group");
963
+ filterGroup.setAttribute("role", "group");
964
+ filterGroup.setAttribute("aria-label", "筛选执行记录");
965
+ for (const [filter, label] of EXECUTION_RECORD_FILTERS) {
966
+ const button = document.createElement("button");
967
+ button.type = "button";
968
+ button.className = "process-timeline-toggle process-timeline-filter";
969
+ button.setAttribute("aria-pressed", String(activeFilter === filter));
970
+ button.textContent = label;
971
+ button.addEventListener("click", () => {
972
+ uiState.executionRecordFilter = filter;
973
+ rerenderSessionTimeline(content, nodeId, options);
974
+ });
975
+ filterGroup.appendChild(button);
976
+ }
977
+ toolbar.appendChild(filterGroup);
870
978
  const protocolToggle = document.createElement("button");
871
979
  protocolToggle.type = "button";
872
980
  protocolToggle.className = "process-timeline-toggle";
@@ -876,13 +984,7 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
876
984
  : "展开原始协议事件";
877
985
  protocolToggle.addEventListener("click", () => {
878
986
  uiState.sessionTimelineShowProtocol = !showProtocol;
879
- const parent = content;
880
- const scrollTop = parent.scrollTop;
881
- const scrollLeft = parent.scrollLeft;
882
- while (parent.firstChild) parent.removeChild(parent.firstChild);
883
- renderSessionTimeline(parent, nodeId, options);
884
- parent.scrollTop = scrollTop;
885
- parent.scrollLeft = scrollLeft;
987
+ rerenderSessionTimeline(content, nodeId, options);
886
988
  });
887
989
  toolbar.appendChild(protocolToggle);
888
990
  const exportBtn = el("button", "btn btn-secondary", "导出 HTML");
@@ -909,10 +1011,12 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
909
1011
  });
910
1012
  toolbar.appendChild(exportBtn);
911
1013
  content.appendChild(toolbar);
1014
+ content.appendChild(createTimelineAutoScrollControl(content));
912
1015
 
913
- const timelineItems = buildRichTimelineItems(visible.events, {
914
- showProtocol,
915
- });
1016
+ const timelineItems = filterExecutionRecordItems(
1017
+ buildRichTimelineItems(visible.events, { showProtocol }),
1018
+ activeFilter,
1019
+ );
916
1020
  const hasEventTimes = visible.events.some((event) =>
917
1021
  Boolean(sessionEventTimestamp(event)),
918
1022
  );
@@ -926,6 +1030,13 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
926
1030
  );
927
1031
  }
928
1032
 
1033
+ if (timelineItems.length === 0) {
1034
+ content.appendChild(
1035
+ el("p", "empty", "当前筛选下没有可显示的执行记录。"),
1036
+ );
1037
+ return;
1038
+ }
1039
+
929
1040
  const ul = el("ul", "process-timeline process-timeline-rich");
930
1041
  for (let index = 0; index < timelineItems.length; index += 1) {
931
1042
  const item = timelineItems[index];
@@ -980,23 +1091,18 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
980
1091
  content.addEventListener(
981
1092
  "scroll",
982
1093
  () => {
983
- const followLatest = computeFollowLatest(
984
- content.scrollTop,
985
- content.scrollHeight,
986
- content.clientHeight,
987
- );
988
1094
  uiState.dagTimelineViewportState = updateDagTimelineViewportState(
989
1095
  uiState.dagTimelineViewportState,
990
1096
  uiState.currentDagRunId,
991
1097
  nodeId,
992
1098
  content.scrollLeft,
993
1099
  content.scrollTop,
994
- followLatest,
1100
+ isTimelineAutoScrollEnabled(),
995
1101
  );
996
1102
  },
997
1103
  { passive: true },
998
1104
  );
999
- if (timelineState?.followLatest ?? true) {
1105
+ if (isTimelineAutoScrollEnabled()) {
1000
1106
  content.scrollLeft = timelineState?.scrollLeft ?? 0;
1001
1107
  content.scrollTop = content.scrollHeight;
1002
1108
  } else {
@@ -1004,3 +1110,21 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
1004
1110
  content.scrollTop = timelineState.scrollTop;
1005
1111
  }
1006
1112
  }
1113
+
1114
+ function rerenderSessionTimeline(content, nodeId, options) {
1115
+ const scrollTop = content.scrollTop;
1116
+ const scrollLeft = content.scrollLeft;
1117
+ const autoScrollEnabled = isTimelineAutoScrollEnabled();
1118
+ const preserved = [...content.children].filter((child) =>
1119
+ String(child.className ?? "")
1120
+ .split(/\s+/)
1121
+ .includes("dag-inspector-attempt-bar"),
1122
+ );
1123
+ while (content.firstChild) content.removeChild(content.firstChild);
1124
+ for (const child of preserved) content.appendChild(child);
1125
+ renderSessionTimeline(content, nodeId, options);
1126
+ if (!autoScrollEnabled) {
1127
+ content.scrollTop = scrollTop;
1128
+ content.scrollLeft = scrollLeft;
1129
+ }
1130
+ }
@@ -36,7 +36,12 @@ import {
36
36
  markdownLinkHref,
37
37
  formatLedgerSummary,
38
38
  } from "../format.js";
39
- import { parseRoute, navigate, buildHashPath } from "../router.js";
39
+ import {
40
+ parseRoute,
41
+ navigate,
42
+ buildHashPath,
43
+ inspectPathToCanonicalHref,
44
+ } from "../router.js";
40
45
  import {
41
46
  fetchJson,
42
47
  fetchJsonResult,
@@ -133,7 +138,7 @@ export async function renderTaskDetail(featureId, taskId) {
133
138
  if (!c?.featureId || !c?.taskId) continue;
134
139
  const path = `/feature/${encodeURIComponent(c.featureId)}/task/${encodeURIComponent(c.taskId)}`;
135
140
  const a = el("a", "object-link", `${c.featureId}::${c.taskId}`);
136
- a.href = `#${path}`;
141
+ a.href = inspectPathToCanonicalHref(path);
137
142
  a.addEventListener("click", (e) => {
138
143
  e.preventDefault();
139
144
  navigate(path);
@@ -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) {