@tea-agent/loop-agent 0.39.0-beta.0 → 0.39.0-beta.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 (331) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +248 -95
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +12 -0
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +23 -5
  9. package/dist/cli/program.js +21 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli/update/runtime-activity.js +1 -29
  12. package/dist/cli.js +2 -1
  13. package/dist/commands/client-recovery.js +3 -0
  14. package/dist/commands/dag-follow-up.js +138 -0
  15. package/dist/commands/dag-rerun.js +55 -1
  16. package/dist/commands/init-model-catalog.js +464 -0
  17. package/dist/commands/init-upgrade.js +265 -97
  18. package/dist/commands/init.js +455 -28
  19. package/dist/commands/inspect-next.js +8 -0
  20. package/dist/commands/task-advance.js +19 -0
  21. package/dist/executors/dag-pi-executor.js +2306 -61
  22. package/dist/executors/pi-executor.js +7 -0
  23. package/dist/executors/pi-extension-resolver.js +233 -0
  24. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  25. package/dist/executors/pi-read-budget-policy.js +239 -0
  26. package/dist/executors/pi-sdk-executor.js +180 -33
  27. package/dist/executors/shell-executor.js +1249 -224
  28. package/dist/executors/shell-write-guard.js +14 -0
  29. package/dist/executors/workspace-write-snapshot.js +68 -0
  30. package/dist/infrastructure/harness/atomic-write.js +23 -0
  31. package/dist/shared/dag-failure-category.js +150 -0
  32. package/dist/shared/dag-prompt-override.js +27 -0
  33. package/dist/shared/openspec-spec.js +70 -4
  34. package/dist/shared/operator/capabilities.js +39 -0
  35. package/dist/shared/playwright-cli-command-policy.js +15 -0
  36. package/dist/shared/update/console-notifier.js +100 -0
  37. package/dist/{cli → shared}/update/npm-client.js +70 -15
  38. package/dist/{cli → shared}/update/state.js +41 -13
  39. package/dist/task/config-types.js +50 -5
  40. package/dist/task/contract/apply.js +11 -1
  41. package/dist/task/contract/constants.js +2 -0
  42. package/dist/task/contract/hash.js +3 -0
  43. package/dist/task/contract/observe.js +25 -4
  44. package/dist/task/contract/paths.js +2 -1
  45. package/dist/task/contract/project.js +18 -1
  46. package/dist/task/contract/schema.js +4 -1
  47. package/dist/task/contract/transaction.js +12 -0
  48. package/dist/task/frontend-project-capability.js +199 -18
  49. package/dist/task/source-prepare/completeness.js +188 -0
  50. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  51. package/dist/task/source-prepare/index.js +3 -0
  52. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  53. package/dist/task/source-prepare/ledger-review.js +214 -0
  54. package/dist/task/source-prepare/ledger.js +545 -0
  55. package/dist/task/source-prepare/parse-intent.js +24 -4
  56. package/dist/task/source-prepare/prepare.js +298 -1
  57. package/dist/task/source-prepare/semantic-intake.js +25 -5
  58. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  59. package/dist/task/source-prepare/types.js +22 -0
  60. package/dist/task/source-references.js +22 -1
  61. package/dist/worker/cli.js +18 -2
  62. package/dist/worker/console/chat/chat-event-store.js +62 -4
  63. package/dist/worker/console/chat/context-panel.js +7 -0
  64. package/dist/worker/console/chat/deferred-turn.js +12 -0
  65. package/dist/worker/console/chat/operation-card.js +8 -1
  66. package/dist/worker/console/chat/pi-console-config.js +81 -10
  67. package/dist/worker/console/chat/pi-runtime.js +264 -16
  68. package/dist/worker/console/chat/provider-error.js +98 -0
  69. package/dist/worker/console/chat/routes.js +298 -32
  70. package/dist/worker/console/chat/session-stats.js +179 -0
  71. package/dist/worker/console/chat/session-store.js +93 -63
  72. package/dist/worker/console/chat/todos.js +115 -0
  73. package/dist/worker/console/chat/tool-preview.js +42 -0
  74. package/dist/worker/console/chat/turn-process.js +20 -55
  75. package/dist/worker/console/chat/user-questions.js +266 -0
  76. package/dist/worker/console/chat/workspace-landing.js +14 -0
  77. package/dist/worker/console/console-handoff.js +82 -0
  78. package/dist/worker/console/console-update-and-init.js +115 -0
  79. package/dist/worker/console/console-update-runtime.js +84 -0
  80. package/dist/worker/console/dag-execution-receipt.js +33 -0
  81. package/dist/worker/console/draft-store.js +49 -0
  82. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  83. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  84. package/dist/worker/console/index.js +3 -0
  85. package/dist/worker/console/inspect-split.js +18 -0
  86. package/dist/worker/console/native-directory-picker.js +13 -0
  87. package/dist/worker/console/operation-run-facts.js +19 -3
  88. package/dist/worker/console/operation-store.js +227 -7
  89. package/dist/worker/console/operator-actions.js +388 -0
  90. package/dist/worker/console/operator-surface-health.js +1 -0
  91. package/dist/worker/console/operator-user-error.js +2 -2
  92. package/dist/worker/console/prd-intake-bridge.js +13 -0
  93. package/dist/worker/console/routes.js +355 -3
  94. package/dist/worker/console/security.js +67 -13
  95. package/dist/worker/console/server.js +609 -164
  96. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-BAsjg7ew.js} +1 -1
  97. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-CyIlrwZw.js} +1 -1
  98. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-Bo4tobhH.js} +1 -1
  99. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dfpow9gu.js} +1 -1
  100. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-DPmzG_B4.js} +1 -1
  101. package/dist/worker/console/static/assets/channel-DSrJAn0-.js +1 -0
  102. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-pKQpB1UW.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-5n9YYquZ.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DP3CtA2a.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DYxMJPUy.js} +1 -1
  106. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-CS3D50Io.js} +1 -1
  107. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-gSdDfo5_.js} +1 -1
  108. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-A48q0ZNt.js} +1 -1
  109. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-ow8Mm3DY.js} +1 -1
  110. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-CMKfLWYE.js} +1 -1
  111. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BiTBdQPX.js} +1 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DeMmPl0_.js +1 -0
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DeMmPl0_.js +1 -0
  114. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-Cm7exRz_.js} +1 -1
  115. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-D9O5qGOu.js} +1 -1
  116. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-CkvQnako.js} +1 -1
  117. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-B1rzp7CB.js} +1 -1
  118. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BP07Ipcc.js} +1 -1
  119. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BlPzweQb.js} +1 -1
  120. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-CxPa7n0S.js} +1 -1
  121. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CMTZs53A.js} +1 -1
  122. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-CxG_X52r.js} +1 -1
  123. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D8axtJM0.js} +1 -1
  124. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DIsGzn7b.js} +1 -1
  125. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CU9zkDG0.js} +1 -1
  126. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-DCjTmz5P.js} +1 -1
  127. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BksdmFzg.js} +1 -1
  128. package/dist/worker/console/static/assets/index-B-O5ChYq.css +1 -0
  129. package/dist/worker/console/static/assets/index-CfnY8AQx.js +407 -0
  130. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-CGzzzJtL.js} +1 -1
  131. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BdseB80N.js} +1 -1
  132. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-DM9gsgha.js} +1 -1
  133. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-3q-_9R2b.js} +1 -1
  134. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-B8N-Mxsd.js} +1 -1
  135. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-EPIHUUsu.js} +5 -5
  136. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CxwO12aa.js} +1 -1
  137. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-bPqbVX6Y.js} +1 -1
  138. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-KzqIvzeX.js} +1 -1
  139. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-CyofpWiV.js} +1 -1
  140. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-cTWJvLTP.js} +1 -1
  141. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-BK--q27B.js} +1 -1
  142. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-C0SDOHXK.js} +1 -1
  143. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-Cong6YIo.js} +1 -1
  144. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DUVkgZx8.js} +1 -1
  145. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C7XgxuZg.js} +1 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dzbnd5oX.js +1 -0
  147. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-D9Ayvwcy.js} +2 -2
  148. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BY0hHk6G.js +8 -0
  149. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-Byb000cH.js} +1 -1
  150. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DMLegPt4.js} +1 -1
  151. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-ChMMeOsP.js} +1 -1
  152. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-bEMEPisW.js} +1 -1
  153. package/dist/worker/console/static/index.html +2 -2
  154. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  155. package/dist/worker/console/static-src/app/console-types.js +68 -4
  156. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  157. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  158. package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
  159. package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
  160. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  161. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  162. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  163. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  164. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  165. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  166. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  167. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  168. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  169. package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
  170. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  171. package/dist/worker/console/static-src/operator-chat/useChatStream.js +102 -21
  172. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  173. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  174. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  175. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  176. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  177. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  178. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  179. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  180. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  181. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  182. package/dist/worker/console/workspace-context.js +344 -0
  183. package/dist/worker/console/workspace-registry.js +214 -0
  184. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  185. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  186. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  187. package/dist/worker/observability/init-runtime-activity.js +26 -0
  188. package/dist/worker/observability/read-model.js +22 -0
  189. package/dist/worker/observe/node-input.js +160 -3
  190. package/dist/worker/observe/node-process.js +377 -0
  191. package/dist/worker/observe/routes.js +224 -3
  192. package/dist/worker/observe/server.js +4 -0
  193. package/dist/worker/observe/shell-handler-keys.js +34 -0
  194. package/dist/worker/observe/static/api.js +90 -4
  195. package/dist/worker/observe/static/app.js +18 -70
  196. package/dist/worker/observe/static/constants.js +8 -1
  197. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  198. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  199. package/dist/worker/observe/static/dag-node-purpose.js +208 -0
  200. package/dist/worker/observe/static/format.js +60 -0
  201. package/dist/worker/observe/static/index.html +6 -1
  202. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  203. package/dist/worker/observe/static/markdown-render.js +20 -1
  204. package/dist/worker/observe/static/operator-chrome.css +145 -20
  205. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  206. package/dist/worker/observe/static/operator-chrome.js +330 -96
  207. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  208. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  209. package/dist/worker/observe/static/router.d.ts +46 -0
  210. package/dist/worker/observe/static/router.js +25 -1
  211. package/dist/worker/observe/static/state.d.ts +50 -0
  212. package/dist/worker/observe/static/state.js +184 -1
  213. package/dist/worker/observe/static/styles.css +395 -0
  214. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  215. package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
  216. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  217. package/dist/worker/observe/static/views/dag.js +14 -5
  218. package/dist/worker/observe/static/views/dashboard.js +6 -4
  219. package/dist/worker/observe/static/views/failures.js +5 -2
  220. package/dist/worker/observe/static/views/night.js +2 -2
  221. package/dist/worker/observe/static/views/pool.js +1 -0
  222. package/dist/worker/observe/static/views/run.js +3 -1
  223. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  224. package/dist/worker/observe/static/views/task.js +56 -1
  225. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
  226. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  227. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  228. package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
  229. package/dist/workflows/dag/contract-output-registry.js +15 -0
  230. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  231. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  232. package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
  233. package/dist/workflows/dag/failure-category.js +7 -116
  234. package/dist/workflows/dag/frontend-closeout.js +221 -0
  235. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  236. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  237. package/dist/workflows/dag/frontend-implementation-contract.js +534 -178
  238. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  239. package/dist/workflows/dag/frontend-prewrite-gate.js +371 -393
  240. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  241. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  242. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  243. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  244. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  245. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  246. package/dist/workflows/dag/frontend-repair.js +7 -424
  247. package/dist/workflows/dag/frontend-review-context.js +288 -14
  248. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  249. package/dist/workflows/dag/frontend-risk.js +15 -2
  250. package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
  251. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  252. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  253. package/dist/workflows/dag/frontend-shape.js +427 -0
  254. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  255. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  256. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  257. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  258. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  259. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  260. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  261. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  262. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  263. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  264. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  265. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  266. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  267. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  268. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  269. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  270. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  271. package/dist/workflows/dag/frontend-writer-admission.js +272 -0
  272. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  273. package/dist/workflows/dag/init-hybrid.js +984 -618
  274. package/dist/workflows/dag/interrupt-request.js +10 -3
  275. package/dist/workflows/dag/lifecycle.js +3 -3
  276. package/dist/workflows/dag/node-execution.js +468 -34
  277. package/dist/workflows/dag/prompt.js +44 -19
  278. package/dist/workflows/dag/recovery-lease.js +80 -0
  279. package/dist/workflows/dag/report.js +37 -1
  280. package/dist/workflows/dag/rerun-feedback.js +124 -1
  281. package/dist/workflows/dag/rerun-plan.js +144 -5
  282. package/dist/workflows/dag/rerun-run.js +81 -6
  283. package/dist/workflows/dag/rerun-task.js +29 -0
  284. package/dist/workflows/dag/retry-policy.js +318 -8
  285. package/dist/workflows/dag/runner.js +429 -122
  286. package/dist/workflows/dag/scheduler.js +114 -16
  287. package/dist/workflows/dag/structured-output-repair.js +712 -0
  288. package/dist/workflows/dag/types.js +321 -31
  289. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  290. package/dist/workflows/dag/validate.js +92 -32
  291. package/docs/README.md +2 -0
  292. package/docs/architecture/evolution.md +45 -0
  293. package/docs/init-surface.manifest.json +9 -0
  294. package/docs/operations/README.md +1 -1
  295. package/docs/operations/local-development-environment.md +21 -0
  296. package/docs/templates/README.md +2 -1
  297. package/docs/templates/agent-dag-report.schema.json +8 -2
  298. package/docs/templates/agent-dag.schema.json +45 -1
  299. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  300. package/docs/templates/frontend-test-case-checklist.md +2 -2
  301. package/docs/templates/frontend-test-dag.json +8 -10
  302. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  303. package/docs/templates/init-managed-agents.md +13 -3
  304. package/docs/templates/spec-registry.schema.json +45 -0
  305. package/harness.json +3 -3
  306. package/package.json +7 -1
  307. package/scripts/next-info.mjs +356 -0
  308. package/scripts/next-publish-gate.mjs +238 -0
  309. package/scripts/release-source-binding.mjs +251 -0
  310. package/skills/codebase-scout/SKILL.md +1 -1
  311. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  312. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  313. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  314. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  315. package/skills/frontend-design-review/SKILL.md +20 -41
  316. package/skills/frontend-implementation/SKILL.md +3 -4
  317. package/skills/frontend-implementation/references/design-spec.md +15 -10
  318. package/skills/frontend-implementation/references/node-contracts.md +23 -13
  319. package/skills/frontend-review/SKILL.md +22 -14
  320. package/skills/frontend-review/references/review-findings.md +6 -7
  321. package/skills/loop-agent/references/command-reference.md +12 -4
  322. package/skills/loop-agent/references/hybrid-dag.md +8 -1
  323. package/skills/playwright-cli/SKILL.md +23 -1
  324. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  325. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  326. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  327. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  328. package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
  329. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  330. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  331. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -3,10 +3,12 @@ import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { dispatchOperatorAction } from "./operator-actions.js";
5
5
  import { openSseResponse, parseLastEventId, writeSseEvent, } from "./operation-sse.js";
6
+ import { probePiReadiness } from "./pi-readiness.js";
6
7
  import { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
7
8
  import { ROUTES } from "../observe/routes.js";
8
9
  import { evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
9
10
  import { listDagRunsForRecovery } from "./resolve-dag-run-for-task.js";
11
+ import { pickNativeWorkspaceDirectory } from "./native-directory-picker.js";
10
12
  export function sendJson(res, status, body, extraHeaders) {
11
13
  const payload = JSON.stringify(body);
12
14
  res.writeHead(status, {
@@ -38,6 +40,10 @@ function contentTypeFor(pathname) {
38
40
  return "application/octet-stream";
39
41
  }
40
42
  async function readJsonBody(req, maxBytes = 10 * 1024 * 1024) {
43
+ // Body pre-read by the multi-workspace dispatcher (workspaceId resolution).
44
+ const preRead = req.__preReadBody;
45
+ if (preRead !== undefined)
46
+ return preRead;
41
47
  const chunks = [];
42
48
  let total = 0;
43
49
  for await (const chunk of req) {
@@ -67,6 +73,7 @@ export async function handleConsoleHealthRequest(_req, res, ctx) {
67
73
  piReadiness: ctx.operator?.getReadiness().state ?? "setup-required",
68
74
  inspectReady: ctx.inspect.ready,
69
75
  routes: ROUTES,
76
+ consoleInstanceId: ctx.consoleInstanceId,
70
77
  }));
71
78
  }
72
79
  catch {
@@ -77,6 +84,7 @@ export async function handleConsoleHealthRequest(_req, res, ctx) {
77
84
  repoFingerprint: "unavailable",
78
85
  packageName: "unknown",
79
86
  packageVersion: "0.0.0",
87
+ consoleInstanceId: ctx.consoleInstanceId,
80
88
  piReadiness: ctx.operator?.getReadiness().state ?? "setup-required",
81
89
  inspect: {
82
90
  ready: ctx.inspect.ready,
@@ -116,6 +124,7 @@ async function handleRecoveryCandidates(_req, res, ctx, url) {
116
124
  }
117
125
  async function handleSessionPing(req, res, ctx) {
118
126
  const failure = evaluateMutationGate(req, {
127
+ cookieName: ctx.bootToken.cookieName,
119
128
  bootToken: ctx.bootToken.value,
120
129
  confirmationToken: ctx.bootToken.confirmationToken,
121
130
  consoleOrigin: ctx.getConsoleOrigin(),
@@ -130,6 +139,16 @@ async function handleSessionPing(req, res, ctx) {
130
139
  }
131
140
  sendJson(res, 200, { ok: true, pong: true });
132
141
  }
142
+ /**
143
+ * Actions the inspect-scoped mutation token may dispatch. Deliberately
144
+ * minimal: the experimental prompt-override rerun tab needs plan preflight +
145
+ * execution only. Everything else (task creation, runDag, reconcile, chat,
146
+ * worker actions…) requires the general Console token.
147
+ */
148
+ export const INSPECT_MUTATION_ALLOWED_ACTIONS = new Set([
149
+ "dagRerunPlan",
150
+ "dagRerun",
151
+ ]);
133
152
  async function handleCreateOperation(req, res, ctx) {
134
153
  if (!ctx.operator) {
135
154
  sendJson(res, 503, {
@@ -141,12 +160,31 @@ async function handleCreateOperation(req, res, ctx) {
141
160
  });
142
161
  return;
143
162
  }
144
- const failure = evaluateMutationGate(req, {
163
+ const generalGateContext = {
164
+ cookieName: ctx.bootToken.cookieName,
145
165
  bootToken: ctx.bootToken.value,
146
166
  confirmationToken: ctx.bootToken.confirmationToken,
147
167
  consoleOrigin: ctx.getConsoleOrigin(),
148
168
  allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
149
- });
169
+ };
170
+ let failure = evaluateMutationGate(req, generalGateContext);
171
+ let inspectScoped = false;
172
+ if (failure && ctx.inspectBootToken) {
173
+ // Scoped /inspect credentials: try the inspect gate before failing. The
174
+ // general gate already rejected, so passing here only ever yields the
175
+ // allowlisted actions below — never general write capability.
176
+ const scopedFailure = evaluateMutationGate(req, {
177
+ cookieName: ctx.inspectBootToken.cookieName,
178
+ bootToken: ctx.inspectBootToken.value,
179
+ confirmationToken: ctx.inspectBootToken.confirmationToken,
180
+ consoleOrigin: ctx.getConsoleOrigin(),
181
+ allowNonLoopbackAccess: ctx.allowNonLoopbackAccess === true,
182
+ });
183
+ if (!scopedFailure) {
184
+ inspectScoped = true;
185
+ failure = undefined;
186
+ }
187
+ }
150
188
  if (failure) {
151
189
  sendJson(res, failure.status, {
152
190
  ok: false,
@@ -173,10 +211,21 @@ async function handleCreateOperation(req, res, ctx) {
173
211
  ? body.actionParams
174
212
  : {};
175
213
  const clientRequestId = typeof body.clientRequestId === "string" ? body.clientRequestId : undefined;
214
+ if (inspectScoped && !INSPECT_MUTATION_ALLOWED_ACTIONS.has(action)) {
215
+ sendJson(res, 403, {
216
+ ok: false,
217
+ error: {
218
+ code: "ACTION_NOT_IN_INSPECT_SCOPE",
219
+ message: `inspect-scoped token only allows: ${[...INSPECT_MUTATION_ALLOWED_ACTIONS].join(", ")}`,
220
+ },
221
+ });
222
+ return;
223
+ }
176
224
  const result = await dispatchOperatorAction(ctx.operator.actionContext, {
177
225
  action,
178
226
  actionParams,
179
227
  clientRequestId,
228
+ source: "human-ui",
180
229
  });
181
230
  sendJson(res, result.status, result.body);
182
231
  }
@@ -202,7 +251,35 @@ async function handleGetOperation(_req, res, ctx, operationId) {
202
251
  });
203
252
  return;
204
253
  }
205
- sendJson(res, 200, { ok: true, operation: op });
254
+ sendJson(res, 200, {
255
+ ok: true,
256
+ workspaceId: ctx.workspace?.workspaceId,
257
+ operation: { ...op, workspaceId: ctx.workspace?.workspaceId },
258
+ });
259
+ }
260
+ /**
261
+ * GET /api/operator/v1/operations?workspaceId= — NEW per-workspace list route
262
+ * (multi-workspace foundation). Returns only the resolved Context's Operations
263
+ * and injects the owning workspaceId. operation-store.ts is unchanged.
264
+ */
265
+ async function handleListOperations(_req, res, ctx) {
266
+ if (!ctx.operator) {
267
+ sendJson(res, 503, {
268
+ ok: false,
269
+ error: {
270
+ code: "INTERNAL_ERROR",
271
+ message: "operator runtime unavailable",
272
+ },
273
+ });
274
+ return;
275
+ }
276
+ const operations = await ctx.operator.operations.list();
277
+ sendJson(res, 200, {
278
+ ok: true,
279
+ schemaVersion: 1,
280
+ workspaceId: ctx.workspace?.workspaceId,
281
+ ops: operations.map((op) => ({ ...op, workspaceId: ctx.workspace?.workspaceId })),
282
+ });
206
283
  }
207
284
  async function handleOperationEvents(req, res, ctx, operationId) {
208
285
  if (!ctx.operator) {
@@ -410,6 +487,10 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
410
487
  await handleCreateOperation(req, res, ctx);
411
488
  return true;
412
489
  }
490
+ if (method === "GET" && pathname === "/api/operator/v1/operations") {
491
+ await handleListOperations(req, res, ctx);
492
+ return true;
493
+ }
413
494
  if (method === "GET" && pathname === "/api/operator/v1/night-aux-ticker") {
414
495
  await handleNightAuxTickerGet(req, res, ctx);
415
496
  return true;
@@ -428,6 +509,7 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
428
509
  }
429
510
  if (isMutationMethod(method)) {
430
511
  const failure = evaluateMutationGate(req, {
512
+ cookieName: ctx.bootToken.cookieName,
431
513
  bootToken: ctx.bootToken.value,
432
514
  confirmationToken: ctx.bootToken.confirmationToken,
433
515
  consoleOrigin: ctx.getConsoleOrigin(),
@@ -473,3 +555,273 @@ export async function handleConsoleRequest(req, res, ctx) {
473
555
  }
474
556
  return false;
475
557
  }
558
+ export function createWorkspaceReadinessCache() {
559
+ const entries = new Map();
560
+ return {
561
+ get: (repoRoot) => entries.get(repoRoot),
562
+ set: (repoRoot, entry) => {
563
+ entries.set(repoRoot, entry);
564
+ },
565
+ };
566
+ }
567
+ const defaultWorkspaceReadinessCache = createWorkspaceReadinessCache();
568
+ const READY_STATE_TTL_MS = 60_000;
569
+ const NON_READY_STATE_TTL_MS = 15_000;
570
+ function isCacheFresh(entry, now) {
571
+ const ttl = entry.state === "ready" ? READY_STATE_TTL_MS : NON_READY_STATE_TTL_MS;
572
+ return now - entry.probedAt < ttl;
573
+ }
574
+ /** Readiness placeholder for a never-probed workspace (AC-006): the list is
575
+ * still returned immediately with a pending marker, refreshed by the
576
+ * background probe. */
577
+ function pendingReadinessSummary(inspectReady) {
578
+ return {
579
+ state: "pending",
580
+ summary: `pi=pending; inspect=${inspectReady ? "ready" : "unavailable"}`,
581
+ };
582
+ }
583
+ /**
584
+ * Process-level Workspace Registry HTTP surface (2026-08-22 multi-workspace
585
+ * foundation):
586
+ * GET /api/operator/v1/workspaces — visible list
587
+ * POST /api/operator/v1/workspaces/pick — host native folder chooser
588
+ * POST /api/operator/v1/workspaces — body { "name": "<label>",
589
+ * "directoryPath": "<dir>" }
590
+ * DELETE /api/operator/v1/workspaces/:workspaceId — soft remove (restore on re-add)
591
+ *
592
+ * POST/DELETE are mutations and pass the Console mutation gate. GET carries no
593
+ * workspace selection parameter.
594
+ *
595
+ * AC-006 (2026-08-23): GET is NON-BLOCKING — registry + process-level
596
+ * readiness cache answer immediately (first sight may be a pending
597
+ * placeholder); probes refresh the cache in the background for subsequent
598
+ * requests. A hanging probe can never delay the 200 list.
599
+ */
600
+ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathname, method, readinessDeps) {
601
+ if (method === "GET" && pathname === "/api/operator/v1/workspaces") {
602
+ const records = await registry.listVisible();
603
+ const inspectReady = readinessDeps.inspectReady;
604
+ const readinessCache = readinessDeps.readinessCache ?? defaultWorkspaceReadinessCache;
605
+ const workspaces = records.map((record) => {
606
+ const cached = readinessCache.get(record.repoRoot);
607
+ return {
608
+ workspaceId: record.workspaceId,
609
+ name: record.name,
610
+ displayPath: record.displayPath,
611
+ repoFingerprint: record.repoFingerprint,
612
+ createdAt: record.createdAt,
613
+ lastAccessedAt: record.lastAccessedAt,
614
+ readiness: cached
615
+ ? {
616
+ state: cached.state,
617
+ summary: `pi=${cached.state}; inspect=${inspectReady ? "ready" : "unavailable"}`,
618
+ }
619
+ : pendingReadinessSummary(inspectReady),
620
+ };
621
+ });
622
+ // Respond FIRST, then refresh readiness in the background (AC-006).
623
+ sendJson(res, 200, {
624
+ ok: true,
625
+ schemaVersion: 1,
626
+ workspaces,
627
+ });
628
+ void Promise.all(records.map(async (record) => {
629
+ const cached = readinessCache.get(record.repoRoot);
630
+ if (cached && isCacheFresh(cached, Date.now()))
631
+ return;
632
+ let state;
633
+ try {
634
+ const report = await probePiReadiness({
635
+ override: deps.piReadinessProbe,
636
+ repoRoot: record.repoRoot,
637
+ });
638
+ state = report.state;
639
+ }
640
+ catch {
641
+ state = "setup-required";
642
+ }
643
+ readinessCache.set(record.repoRoot, {
644
+ state,
645
+ probedAt: Date.now(),
646
+ });
647
+ })).catch(() => undefined);
648
+ return;
649
+ }
650
+ if (method === "POST" && pathname === "/api/operator/v1/workspaces/pick") {
651
+ const failure = evaluateMutationGate(req, {
652
+ cookieName: deps.bootToken.cookieName,
653
+ bootToken: deps.bootToken.value,
654
+ confirmationToken: deps.bootToken.confirmationToken,
655
+ consoleOrigin: deps.getConsoleOrigin(),
656
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
657
+ });
658
+ if (failure) {
659
+ sendJson(res, failure.status, {
660
+ ok: false,
661
+ error: { code: failure.code, message: failure.message },
662
+ });
663
+ return;
664
+ }
665
+ if (deps.allowNonLoopbackAccess) {
666
+ sendJson(res, 409, {
667
+ ok: false,
668
+ error: {
669
+ code: "NATIVE_PICKER_UNAVAILABLE",
670
+ message: "原生目录选择仅在本机回环地址启动的 Console 中可用。",
671
+ },
672
+ });
673
+ return;
674
+ }
675
+ const abortController = new AbortController();
676
+ const abort = () => abortController.abort();
677
+ req.once("aborted", abort);
678
+ res.once("close", abort);
679
+ try {
680
+ const directoryPath = await (deps.pickDirectory ?? pickNativeWorkspaceDirectory)(abortController.signal);
681
+ if (!res.writableEnded && !abortController.signal.aborted) {
682
+ sendJson(res, 200, { ok: true, directoryPath });
683
+ }
684
+ }
685
+ catch (error) {
686
+ if (!res.writableEnded && !abortController.signal.aborted) {
687
+ sendJson(res, 500, {
688
+ ok: false,
689
+ error: {
690
+ code: "DIRECTORY_PICKER_FAILED",
691
+ message: error instanceof Error ? error.message : String(error),
692
+ },
693
+ });
694
+ }
695
+ }
696
+ finally {
697
+ req.off("aborted", abort);
698
+ res.off("close", abort);
699
+ }
700
+ return;
701
+ }
702
+ if (method === "POST" && pathname === "/api/operator/v1/workspaces") {
703
+ const failure = evaluateMutationGate(req, {
704
+ cookieName: deps.bootToken.cookieName,
705
+ bootToken: deps.bootToken.value,
706
+ confirmationToken: deps.bootToken.confirmationToken,
707
+ consoleOrigin: deps.getConsoleOrigin(),
708
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
709
+ });
710
+ if (failure) {
711
+ sendJson(res, failure.status, {
712
+ ok: false,
713
+ error: { code: failure.code, message: failure.message },
714
+ });
715
+ return;
716
+ }
717
+ let body;
718
+ try {
719
+ body = (await readJsonBody(req));
720
+ }
721
+ catch (error) {
722
+ sendJson(res, 400, {
723
+ ok: false,
724
+ error: {
725
+ code: "INVALID_INPUT",
726
+ message: error instanceof Error ? error.message : String(error),
727
+ },
728
+ });
729
+ return;
730
+ }
731
+ const directoryPath = typeof body.directoryPath === "string"
732
+ ? body.directoryPath
733
+ : typeof body.path === "string"
734
+ ? body.path
735
+ : "";
736
+ if (!directoryPath.trim()) {
737
+ sendJson(res, 400, {
738
+ ok: false,
739
+ error: {
740
+ code: "INVALID_INPUT",
741
+ message: "directoryPath (string) is required",
742
+ },
743
+ });
744
+ return;
745
+ }
746
+ if (body.name !== undefined &&
747
+ (typeof body.name !== "string" || !body.name.trim())) {
748
+ sendJson(res, 400, {
749
+ ok: false,
750
+ error: {
751
+ code: "INVALID_INPUT",
752
+ message: "name must be a non-empty string when provided",
753
+ },
754
+ });
755
+ return;
756
+ }
757
+ const name = typeof body.name === "string" ? body.name : undefined;
758
+ const result = await registry.add(directoryPath, name ? { name } : {});
759
+ if (!result.ok) {
760
+ const status = result.code === "DUPLICATE"
761
+ ? 409
762
+ : result.code === "INVALID_PATH"
763
+ ? 400
764
+ : 400;
765
+ sendJson(res, status, {
766
+ ok: false,
767
+ error: {
768
+ code: result.code,
769
+ message: result.message,
770
+ },
771
+ });
772
+ return;
773
+ }
774
+ const record = result.record;
775
+ sendJson(res, 201, {
776
+ ok: true,
777
+ workspace: {
778
+ workspaceId: record.workspaceId,
779
+ name: record.name,
780
+ displayPath: record.displayPath,
781
+ repoFingerprint: record.repoFingerprint,
782
+ createdAt: record.createdAt,
783
+ lastAccessedAt: record.lastAccessedAt,
784
+ readiness: {
785
+ state: "setup-required",
786
+ summary: `pi=unknown; inspect=${readinessDeps.inspectReady ? "ready" : "unavailable"}`,
787
+ },
788
+ },
789
+ });
790
+ return;
791
+ }
792
+ const removeMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
793
+ if (method === "DELETE" && removeMatch) {
794
+ const failure = evaluateMutationGate(req, {
795
+ cookieName: deps.bootToken.cookieName,
796
+ bootToken: deps.bootToken.value,
797
+ confirmationToken: deps.bootToken.confirmationToken,
798
+ consoleOrigin: deps.getConsoleOrigin(),
799
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
800
+ });
801
+ if (failure) {
802
+ sendJson(res, failure.status, {
803
+ ok: false,
804
+ error: { code: failure.code, message: failure.message },
805
+ });
806
+ return;
807
+ }
808
+ const workspaceId = decodeURIComponent(removeMatch[1]);
809
+ const removed = await registry.remove(workspaceId);
810
+ if (!removed) {
811
+ sendJson(res, 404, {
812
+ ok: false,
813
+ error: {
814
+ code: "WORKSPACE_NOT_FOUND",
815
+ message: `unknown workspace: ${workspaceId}`,
816
+ },
817
+ });
818
+ return;
819
+ }
820
+ sendJson(res, 200, { ok: true, workspaceId, removed: true });
821
+ return;
822
+ }
823
+ sendJson(res, 404, {
824
+ ok: false,
825
+ error: { code: "NOT_FOUND", message: `unknown workspace route: ${pathname}` },
826
+ });
827
+ }
@@ -1,24 +1,78 @@
1
- import { randomBytes, timingSafeEqual } from "node:crypto";
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import { isLoopbackHost } from "./loopback.js";
3
+ /** Prefix for instance-scoped Cookie names; not a lookup name. */
3
4
  export const BOOT_CAPABILITY_COOKIE = "loop_console_capability";
4
5
  /** Custom mutation confirmation header (design §9). JS-readable; not the HttpOnly boot cookie. */
5
6
  export const CONFIRMATION_TOKEN_HEADER = "x-loop-console-confirmation";
6
- /** Issue per-process unguessable boot capability + confirmation tokens. */
7
- export function issueBootCapabilityToken() {
7
+ function normalizeInstanceScope(scope) {
8
+ const repoFingerprint = scope.repoFingerprint.trim();
9
+ if (!repoFingerprint) {
10
+ throw new Error("console instance scope requires a non-empty repoFingerprint");
11
+ }
12
+ const bindHost = scope.bindHost.trim();
13
+ if (!bindHost) {
14
+ throw new Error("console instance scope requires a non-empty bindHost");
15
+ }
16
+ if (!Number.isInteger(scope.configuredPort) || scope.configuredPort < 0) {
17
+ throw new Error(`console instance scope configuredPort must be an integer >= 0, got ${String(scope.configuredPort)}`);
18
+ }
19
+ const instanceNonce = scope.instanceNonce?.trim();
20
+ if (scope.configuredPort === 0) {
21
+ if (!instanceNonce) {
22
+ throw new Error("console instance scope requires a non-empty instanceNonce when configuredPort is 0");
23
+ }
24
+ return {
25
+ repoFingerprint,
26
+ bindHost,
27
+ configuredPort: 0,
28
+ instanceNonce,
29
+ };
30
+ }
31
+ return {
32
+ repoFingerprint,
33
+ bindHost,
34
+ configuredPort: scope.configuredPort,
35
+ };
36
+ }
37
+ /** Deterministic Cookie name for one Console instance; never includes repo path or token. */
38
+ export function deriveBootCapabilityCookieName(scope, variant) {
39
+ const normalized = normalizeInstanceScope(scope);
40
+ const digest = createHash("sha256")
41
+ .update("loop-console-capability-v1\0", "utf8")
42
+ .update(normalized.repoFingerprint, "utf8")
43
+ .update("\0", "utf8")
44
+ .update(normalized.bindHost, "utf8")
45
+ .update("\0", "utf8")
46
+ .update(String(normalized.configuredPort), "utf8")
47
+ .update("\0", "utf8")
48
+ .update(normalized.instanceNonce ?? "", "utf8")
49
+ .update("\0", "utf8")
50
+ .update(variant ?? "", "utf8")
51
+ .digest("hex")
52
+ .slice(0, 32);
53
+ return `${BOOT_CAPABILITY_COOKIE}_${digest}`;
54
+ }
55
+ /** Issue per-process unguessable boot capability + confirmation tokens.
56
+ * `variant` (e.g. "inspect") derives a separate cookie name so scoped
57
+ * credentials cannot be replayed against the general mutation surface. */
58
+ export function issueBootCapabilityToken(scope, options = {}) {
59
+ const cookieName = deriveBootCapabilityCookieName(scope, options.variant);
8
60
  const value = randomBytes(32).toString("base64url");
9
61
  const confirmationToken = randomBytes(32).toString("base64url");
10
62
  const setCookieHeader = [
11
- `${BOOT_CAPABILITY_COOKIE}=${value}`,
63
+ `${cookieName}=${value}`,
12
64
  "HttpOnly",
13
65
  "SameSite=Strict",
14
66
  "Path=/",
15
67
  ].join("; ");
16
- return { value, setCookieHeader, confirmationToken };
68
+ return { cookieName, value, setCookieHeader, confirmationToken };
17
69
  }
18
- /** Inject confirmation token into HTML so the SPA can send the mutation header. */
19
- export function injectConfirmationTokenScript(html, confirmationToken) {
70
+ /** Inject confirmation token into HTML so the SPA can send the mutation
71
+ * header. `varName` separates the general Console token from the scoped
72
+ * inspect token on the read-only /inspect surface. */
73
+ export function injectConfirmationTokenScript(html, confirmationToken, varName = "__CONSOLE_CONFIRMATION_TOKEN__") {
20
74
  const source = typeof html === "string" ? html : html.toString("utf8");
21
- const script = `<script>window.__CONSOLE_CONFIRMATION_TOKEN__=${JSON.stringify(confirmationToken)};</script>`;
75
+ const script = `<script>window.${varName}=${JSON.stringify(confirmationToken)};</script>`;
22
76
  if (source.includes("</head>")) {
23
77
  return Buffer.from(source.replace("</head>", `${script}</head>`), "utf8");
24
78
  }
@@ -65,16 +119,16 @@ function safeEqualToken(a, b) {
65
119
  function loopbackOriginMatches(origin, consoleOrigin) {
66
120
  if (origin === consoleOrigin)
67
121
  return true;
68
- const parseLoopback = (value) => {
122
+ const parseLoopbackPort = (value) => {
69
123
  const match = /^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/i.exec(value);
70
- return match ? `${match[1].toLowerCase()}${match[2] ?? ""}` : null;
124
+ return match ? (match[2] ?? ":80") : null;
71
125
  };
72
- const originKey = parseLoopback(origin);
73
- const consoleKey = parseLoopback(consoleOrigin);
126
+ const originKey = parseLoopbackPort(origin);
127
+ const consoleKey = parseLoopbackPort(consoleOrigin);
74
128
  return originKey !== null && consoleKey !== null && originKey === consoleKey;
75
129
  }
76
130
  export function evaluateMutationGate(req, ctx) {
77
- const cookieToken = parseCookieHeader(req.headers.cookie, BOOT_CAPABILITY_COOKIE);
131
+ const cookieToken = parseCookieHeader(req.headers.cookie, ctx.cookieName);
78
132
  if (!cookieToken) {
79
133
  return {
80
134
  status: 403,