@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.11

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 (213) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +114 -109
  3. package/README.md +2 -2
  4. package/dist/application/dag/generate-task-dag.js +6 -2
  5. package/dist/application/task-lifecycle/advance.js +12 -0
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/update/runtime-activity.js +1 -29
  8. package/dist/commands/dag-rerun.js +1 -1
  9. package/dist/executors/dag-pi-executor.js +69 -7
  10. package/dist/executors/pi-executor.js +4 -2
  11. package/dist/executors/pi-sdk-executor.js +46 -1
  12. package/dist/executors/pi-writer-tool-policy.js +57 -1
  13. package/dist/executors/shell-executor.js +107 -20
  14. package/dist/executors/workspace-write-snapshot.js +68 -0
  15. package/dist/governance/checks.js +1 -0
  16. package/dist/infrastructure/harness/atomic-write.js +23 -0
  17. package/dist/shared/dag-prompt-override.js +4 -5
  18. package/dist/shared/update/npm-client.js +33 -11
  19. package/dist/task/config-types.js +10 -0
  20. package/dist/task/contract/apply.js +11 -1
  21. package/dist/task/contract/constants.js +2 -0
  22. package/dist/task/contract/hash.js +3 -0
  23. package/dist/task/contract/observe.js +25 -4
  24. package/dist/task/contract/paths.js +2 -1
  25. package/dist/task/contract/project.js +15 -1
  26. package/dist/task/contract/schema.js +2 -0
  27. package/dist/task/contract/transaction.js +12 -0
  28. package/dist/task/source-prepare/completeness.js +188 -0
  29. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  30. package/dist/task/source-prepare/index.js +3 -0
  31. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  32. package/dist/task/source-prepare/ledger-review.js +214 -0
  33. package/dist/task/source-prepare/ledger.js +545 -0
  34. package/dist/task/source-prepare/prepare.js +262 -1
  35. package/dist/task/source-prepare/semantic-intake.js +19 -3
  36. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  37. package/dist/task/source-prepare/types.js +22 -0
  38. package/dist/worker/cli.js +12 -26
  39. package/dist/worker/console/chat/chat-event-store.js +75 -5
  40. package/dist/worker/console/chat/context-panel.js +7 -0
  41. package/dist/worker/console/chat/deferred-turn.js +12 -0
  42. package/dist/worker/console/chat/pi-runtime.js +331 -18
  43. package/dist/worker/console/chat/provider-error.js +98 -0
  44. package/dist/worker/console/chat/routes.js +389 -56
  45. package/dist/worker/console/chat/session-stats.js +179 -0
  46. package/dist/worker/console/chat/session-store.js +93 -63
  47. package/dist/worker/console/chat/todos.js +115 -0
  48. package/dist/worker/console/chat/tool-preview.js +42 -0
  49. package/dist/worker/console/chat/turn-process.js +20 -55
  50. package/dist/worker/console/chat/user-questions.js +266 -0
  51. package/dist/worker/console/chat/workspace-landing.js +79 -0
  52. package/dist/worker/console/console-handoff.js +82 -0
  53. package/dist/worker/console/console-update-and-init.js +115 -0
  54. package/dist/worker/console/console-update-runtime.js +84 -0
  55. package/dist/worker/console/dag-execution-receipt.js +20 -2
  56. package/dist/worker/console/draft-store.js +49 -0
  57. package/dist/worker/console/native-directory-picker.js +13 -0
  58. package/dist/worker/console/operation-store.js +1 -1
  59. package/dist/worker/console/operator-actions.js +133 -7
  60. package/dist/worker/console/operator-surface-health.js +1 -0
  61. package/dist/worker/console/prd-intake-bridge.js +13 -0
  62. package/dist/worker/console/routes.js +312 -1
  63. package/dist/worker/console/security.js +4 -4
  64. package/dist/worker/console/server.js +583 -172
  65. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
  66. package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
  67. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
  68. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
  69. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
  70. package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
  71. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
  78. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
  79. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
  80. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
  81. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
  82. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
  83. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
  84. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
  85. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
  86. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
  89. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
  90. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
  91. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
  92. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
  93. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
  94. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
  95. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
  96. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
  97. package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
  98. package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
  99. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
  100. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
  101. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
  102. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
  103. package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
  104. package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
  105. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
  106. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
  107. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
  108. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
  109. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
  110. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
  111. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
  112. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
  113. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
  114. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
  115. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
  116. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
  118. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
  119. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
  120. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
  121. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
  122. package/dist/worker/console/static/index.html +2 -2
  123. package/dist/worker/console/static-src/app/console-types.js +68 -4
  124. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  125. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  126. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  127. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  128. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  129. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  130. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
  131. package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
  132. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  133. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  134. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  135. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  136. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
  137. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  138. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
  139. package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
  140. package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
  141. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  142. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  143. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  144. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  145. package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
  146. package/dist/worker/console/workspace-context.js +234 -0
  147. package/dist/worker/console/workspace-registry.js +214 -0
  148. package/dist/worker/observability/init-runtime-activity.js +26 -0
  149. package/dist/worker/observe/node-input.js +88 -2
  150. package/dist/worker/observe/routes.js +188 -0
  151. package/dist/worker/observe/server.js +4 -0
  152. package/dist/worker/observe/static/api.js +29 -9
  153. package/dist/worker/observe/static/app.js +18 -70
  154. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  155. package/dist/worker/observe/static/dag-node-purpose.js +203 -0
  156. package/dist/worker/observe/static/format.js +37 -0
  157. package/dist/worker/observe/static/index.html +6 -1
  158. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  159. package/dist/worker/observe/static/operator-chrome.css +145 -20
  160. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  161. package/dist/worker/observe/static/operator-chrome.js +330 -100
  162. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  163. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  164. package/dist/worker/observe/static/router.d.ts +46 -0
  165. package/dist/worker/observe/static/router.js +25 -1
  166. package/dist/worker/observe/static/state.d.ts +50 -0
  167. package/dist/worker/observe/static/state.js +174 -2
  168. package/dist/worker/observe/static/styles.css +357 -35
  169. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  170. package/dist/worker/observe/static/views/dag-inspector.js +533 -79
  171. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  172. package/dist/worker/observe/static/views/dag.js +6 -4
  173. package/dist/worker/observe/static/views/dashboard.js +6 -4
  174. package/dist/worker/observe/static/views/night.js +2 -2
  175. package/dist/worker/observe/static/views/pool.js +1 -0
  176. package/dist/worker/observe/static/views/run.js +3 -1
  177. package/dist/worker/observe/static/views/task.js +56 -1
  178. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +186 -8
  179. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  180. package/dist/workflows/dag/backend-test-pytest-collection.js +168 -16
  181. package/dist/workflows/dag/backend-test-result-contract.js +4 -0
  182. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  183. package/dist/workflows/dag/backend-test-writer-completeness.js +28 -17
  184. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  185. package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
  186. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  187. package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
  188. package/dist/workflows/dag/frontend-review-context.js +92 -10
  189. package/dist/workflows/dag/init-hybrid.js +239 -52
  190. package/dist/workflows/dag/interrupt-request.js +3 -3
  191. package/dist/workflows/dag/lifecycle.js +3 -3
  192. package/dist/workflows/dag/node-execution.js +45 -0
  193. package/dist/workflows/dag/types.js +62 -12
  194. package/docs/README.md +2 -0
  195. package/docs/architecture/evolution.md +45 -0
  196. package/docs/init-surface.manifest.json +1 -0
  197. package/docs/operations/local-development-environment.md +21 -0
  198. package/docs/templates/README.md +1 -1
  199. package/docs/templates/agent-dag.schema.json +18 -0
  200. package/docs/templates/backend-test-dag.json +267 -238
  201. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  202. package/harness.json +3 -3
  203. package/package.json +3 -1
  204. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  205. package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
  206. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
  207. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
  208. package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
  209. package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
  210. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
  211. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
  212. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  213. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Repo-scoped Workspace Context (2026-08-22 multi-workspace foundation, W1).
3
+ *
4
+ * One lazily-created ConsoleContext per workspaceId. Each context INDEPENDENTLY
5
+ * owns the full operator runtime for its repo root: fingerprint-isolated
6
+ * appData (via openConsoleAppData), operation/event stores, draft /
7
+ * confirmation / receipt / interview / assessment stores, Chat session store +
8
+ * events + deferred Turn registry + ConsolePiRuntime(cwd=repoRoot),
9
+ * OperatorActionContext, Observe route context, and readiness.
10
+ *
11
+ * Invariants:
12
+ * - Selecting (viewing) another workspace NEVER disposes a context; running
13
+ * Turns / Operations / DAGs continue in the background.
14
+ * - dispose() is invoked exactly once per context by the owning server's
15
+ * close().
16
+ * - bootToken is shared per Console boot: it binds the startup fingerprint
17
+ * and bind host, and its confirmationToken signs human-gate tokens for all
18
+ * workspaces in this process (single Console security domain).
19
+ */
20
+ import { randomBytes } from "node:crypto";
21
+ import { existsSync } from "node:fs";
22
+ import path from "node:path";
23
+ import { LoopAgentClient } from "../loop-agent/loop-agent-client.js";
24
+ import { openConsoleAppData } from "./app-data.js";
25
+ import { DagConfirmationStore } from "./dag-confirmation.js";
26
+ import { DagExecutionReceiptStore } from "./dag-execution-receipt.js";
27
+ import { DraftStore } from "./draft-store.js";
28
+ import { AssessmentStore } from "./interview/assessment.js";
29
+ import { InterviewSessionStore } from "./interview/session.js";
30
+ import { ChatSessionStore } from "./chat/session-store.js";
31
+ import { createChatEventStore, createChatOperationLinker, } from "./chat/chat-event-store.js";
32
+ import { createChatTurnExecutionRegistry } from "./chat/turn-execution-registry.js";
33
+ import { ConsolePiRuntime } from "./chat/pi-runtime.js";
34
+ import { UserQuestionRegistry } from "./chat/user-questions.js";
35
+ import { TodoListRegistry } from "./chat/todos.js";
36
+ import { createOperationEventStore, } from "./operation-sse.js";
37
+ import { OperationStore } from "./operation-store.js";
38
+ import { probePiReadiness, } from "./pi-readiness.js";
39
+ import { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
40
+ import { NightAuxTicker } from "./night-aux-ticker.js";
41
+ import { createObserveRouteContext, } from "../observe/routes.js";
42
+ import { WorkspaceRegistry } from "./workspace-registry.js";
43
+ import { resolveConsoleDefaultTimeoutMs } from "./server.js";
44
+ /**
45
+ * Lazily create the full repo-scoped Console context for a registered
46
+ * workspace record. Pure factory — no global registry; the caller owns
47
+ * lifecycle (single creation per workspaceId, dispose on server close).
48
+ */
49
+ export async function createWorkspaceContext(record, deps) {
50
+ const repoRoot = record.repoRoot;
51
+ const appData = openConsoleAppData({
52
+ repoRoot,
53
+ ...(deps.appDataRoot ? { appDataRoot: deps.appDataRoot } : {}),
54
+ });
55
+ const registry = new WorkspaceRegistry(appData);
56
+ const readiness = await probePiReadiness({
57
+ override: deps.piReadinessProbe,
58
+ repoRoot,
59
+ });
60
+ let readinessCache = readiness;
61
+ const artifactRoot = path.join(appData.root, "client-artifacts", appData.fingerprint);
62
+ const client = deps.createClient?.(artifactRoot) ??
63
+ new LoopAgentClient({
64
+ loopAgentBin: resolveSiblingLoopAgentBin(),
65
+ artifactRoot,
66
+ resolveIdentity: true,
67
+ defaultTimeoutMs: resolveConsoleDefaultTimeoutMs(deps.defaultTimeoutMs),
68
+ });
69
+ const operations = new OperationStore(appData);
70
+ const events = createOperationEventStore({
71
+ persistenceDir: path.join(appData.operations, "events"),
72
+ });
73
+ const drafts = new DraftStore(appData);
74
+ const confirmations = new DagConfirmationStore(appData, deps.bootToken.confirmationToken);
75
+ const dagExecutionReceipts = new DagExecutionReceiptStore(appData);
76
+ const interviews = new InterviewSessionStore(appData);
77
+ const assessments = new AssessmentStore(appData);
78
+ if (!deps.skipReconcile) {
79
+ await operations.reconcileOnBoot();
80
+ }
81
+ const getReadiness = () => readinessCache;
82
+ const refreshReadiness = async () => {
83
+ readinessCache = await probePiReadiness({
84
+ override: deps.piReadinessProbe,
85
+ repoRoot,
86
+ });
87
+ actionContext.readiness = readinessCache;
88
+ return readinessCache;
89
+ };
90
+ const actionContext = {
91
+ repoRoot,
92
+ appData,
93
+ operatorSessionId: deps.operatorSessionId,
94
+ client,
95
+ operations,
96
+ events,
97
+ drafts,
98
+ confirmations,
99
+ dagExecutionReceipts,
100
+ humanGateSecret: deps.bootToken.confirmationToken,
101
+ interviews,
102
+ assessments,
103
+ readiness: readinessCache,
104
+ getReadiness,
105
+ refreshReadiness,
106
+ observeBaseUrl: deps.observeBaseUrl,
107
+ fetchImpl: deps.fetchImpl,
108
+ getConsoleOrigin: deps.getConsoleOrigin,
109
+ };
110
+ const chatStore = new ChatSessionStore(appData);
111
+ const chatEventEpoch = randomBytes(12).toString("hex");
112
+ const chatEvents = createChatEventStore({
113
+ appData,
114
+ epoch: chatEventEpoch,
115
+ });
116
+ const chatTurnExecutions = createChatTurnExecutionRegistry();
117
+ const questionRegistry = new UserQuestionRegistry(path.join(appData.chats, "user-questions"));
118
+ const todoRegistry = new TodoListRegistry();
119
+ questionRegistry.subscribe((change) => {
120
+ const question = change.question;
121
+ chatEvents.append(question.sessionId, question.turnId ?? `${question.sessionId}:question`, {
122
+ kind: `question_${change.type}`,
123
+ data: { question },
124
+ });
125
+ });
126
+ todoRegistry.subscribe((change) => {
127
+ chatEvents.append(change.sessionId, change.turnId ?? `${change.sessionId}:todo`, {
128
+ kind: change.type === "updated" ? "todo_updated" : "todo_cleared",
129
+ data: {
130
+ todos: change.todos,
131
+ ...(change.toolCallId ? { toolCallId: change.toolCallId } : {}),
132
+ },
133
+ });
134
+ });
135
+ const chatOperationLinker = createChatOperationLinker({
136
+ chatEvents,
137
+ operations,
138
+ operationEvents: events,
139
+ });
140
+ const chatRuntime = new ConsolePiRuntime({
141
+ cwd: repoRoot,
142
+ sessionDir: appData.chatSessions,
143
+ resourcePreferencesPath: appData.resourcePreferences,
144
+ actionContext,
145
+ questionRegistry,
146
+ todoRegistry,
147
+ });
148
+ const chatRouteDeps = {
149
+ runtime: chatRuntime,
150
+ store: chatStore,
151
+ events: chatEvents,
152
+ turnExecutions: chatTurnExecutions,
153
+ questionRegistry,
154
+ todoRegistry,
155
+ operationLinker: chatOperationLinker,
156
+ appData,
157
+ bootToken: deps.bootToken,
158
+ getConsoleOrigin: deps.getConsoleOrigin,
159
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
160
+ };
161
+ const { recoverDeferredTurns } = await import("./chat/routes.js");
162
+ const nightAuxTicker = new NightAuxTicker({ repoRoot, client });
163
+ const observeRouteContext = createObserveRouteContext(repoRoot, {
164
+ staticDir: deps.inspectStaticDir,
165
+ });
166
+ let disposed = false;
167
+ const context = {
168
+ workspaceId: record.workspaceId,
169
+ record,
170
+ repoRoot,
171
+ appData,
172
+ registry,
173
+ client,
174
+ operations,
175
+ events,
176
+ drafts,
177
+ confirmations,
178
+ dagExecutionReceipts,
179
+ interviews,
180
+ assessments,
181
+ chatRouteDeps,
182
+ chatOperationLinker,
183
+ chatRuntime,
184
+ nightAuxTicker,
185
+ actionContext,
186
+ observeRouteContext,
187
+ getReadiness,
188
+ refreshReadiness,
189
+ recoverDeferredTurns: () => recoverDeferredTurns(chatRouteDeps),
190
+ dispose: () => {
191
+ if (disposed)
192
+ return;
193
+ disposed = true;
194
+ try {
195
+ chatOperationLinker.dispose();
196
+ }
197
+ catch {
198
+ // best-effort
199
+ }
200
+ try {
201
+ chatRuntime.disposeAll();
202
+ }
203
+ catch {
204
+ // best-effort
205
+ }
206
+ try {
207
+ nightAuxTicker.dispose();
208
+ }
209
+ catch {
210
+ // best-effort
211
+ }
212
+ },
213
+ };
214
+ return context;
215
+ }
216
+ /** Inspect-facing readiness/capability summary for a registry record. */
217
+ export async function summarizeWorkspaceRecord(record, deps) {
218
+ let state;
219
+ try {
220
+ const report = await probePiReadiness({
221
+ override: deps.piReadinessProbe,
222
+ repoRoot: record.repoRoot,
223
+ });
224
+ state = report.state;
225
+ }
226
+ catch {
227
+ state = "setup-required";
228
+ }
229
+ return {
230
+ state,
231
+ summary: `pi=${state}; inspect=${deps.inspectReady ? "ready" : "unavailable"}`,
232
+ };
233
+ }
234
+ void existsSync;
@@ -0,0 +1,214 @@
1
+ /**
2
+ * User-level Workspace Registry (2026-08-22 multi-workspace foundation, W1).
3
+ *
4
+ * Persists the set of registered Console workspaces at
5
+ * `<appDataRoot>/workspaces.json` (user level, shared across Console boots),
6
+ * independent of per-repo fingerprint namespaces.
7
+ *
8
+ * Invariants:
9
+ * - workspaceId is stable per normalized realpath: it is derived from
10
+ * sha256("loop-console-workspace-v1\0" + realpath) and never minted twice.
11
+ * - REMOVE is soft (hidden=true); on-disk directories, AppData namespaces,
12
+ * Sessions, Operations and DAGs are never deleted.
13
+ * - Re-adding a previously removed workspace RESTORES the original record
14
+ * (same workspaceId + repoFingerprint; only lastAccessedAt advances).
15
+ * - Duplicate visible realpath is rejected; add failures leave the registry
16
+ * byte-identical (same ids and order).
17
+ */
18
+ import { createHash, randomBytes } from "node:crypto";
19
+ import { realpathSync, statSync } from "node:fs";
20
+ import path from "node:path";
21
+ import { readJsonIfExists, writeSecureJson, } from "./app-data.js";
22
+ import { normalizeWorktreeRealpath, repoFingerprintV1, } from "./repo-fingerprint.js";
23
+ export const WORKSPACE_REGISTRY_SCHEMA_VERSION = 1;
24
+ /** Stable workspaceId: sha256("loop-console-workspace-v1\0" + normalizedRealpath). */
25
+ export function workspaceIdForRealpath(normalizedRealpath) {
26
+ return createHash("sha256")
27
+ .update(`loop-console-workspace-v1\0${normalizedRealpath}`, "utf8")
28
+ .digest("hex");
29
+ }
30
+ export class WorkspaceRegistry {
31
+ appData;
32
+ options;
33
+ file;
34
+ cache;
35
+ constructor(appData, options = {}) {
36
+ this.appData = appData;
37
+ this.options = options;
38
+ this.file = path.join(appData.root, "workspaces.json");
39
+ }
40
+ /** Auto-register the startup repoRoot (idempotent). */
41
+ async ensureStartupWorkspace(repoRoot) {
42
+ const record = await this.add(repoRoot);
43
+ if (record.ok)
44
+ return record.record;
45
+ // Startup root may already be visible (duplicate) or previously hidden —
46
+ // re-adding a hidden record restores it via add(); the only expected
47
+ // failure left is DUPLICATE (already visible), which returns the existing.
48
+ const existing = await this.findByRealpath(repoRoot);
49
+ if (existing)
50
+ return existing;
51
+ throw new Error(`workspace registry: startup root rejected: ${record.code}`);
52
+ }
53
+ /**
54
+ * Register (or restore) a workspace directory.
55
+ * Failures never mutate the persisted registry.
56
+ * `options.name` overrides the default directory-name label.
57
+ */
58
+ async add(rawPath, options = {}) {
59
+ const trimmed = rawPath.trim();
60
+ if (!trimmed) {
61
+ return {
62
+ ok: false,
63
+ code: "INVALID_PATH",
64
+ message: "path is required",
65
+ };
66
+ }
67
+ let real;
68
+ try {
69
+ real = realpathSync(path.resolve(trimmed));
70
+ }
71
+ catch {
72
+ return {
73
+ ok: false,
74
+ code: "NOT_A_DIRECTORY",
75
+ message: `path does not exist: ${trimmed}`,
76
+ };
77
+ }
78
+ let st;
79
+ try {
80
+ st = statSync(real);
81
+ }
82
+ catch {
83
+ return {
84
+ ok: false,
85
+ code: "UNREADABLE",
86
+ message: `path is not readable: ${trimmed}`,
87
+ };
88
+ }
89
+ if (!st.isDirectory()) {
90
+ return {
91
+ ok: false,
92
+ code: "NOT_A_DIRECTORY",
93
+ message: `path is not a directory: ${trimmed}`,
94
+ };
95
+ }
96
+ const normalized = normalizeWorktreeRealpath(real);
97
+ const existing = await this.findByRealpathNormalized(normalized);
98
+ if (existing && !existing.hidden) {
99
+ return {
100
+ ok: false,
101
+ code: "DUPLICATE",
102
+ message: `workspace already registered: ${existing.displayPath}`,
103
+ };
104
+ }
105
+ const nowIso = (this.options.now ?? (() => new Date()))().toISOString();
106
+ if (existing) {
107
+ // Restore the soft-removed record; identity (workspaceId,
108
+ // repoFingerprint, createdAt, order) is preserved.
109
+ existing.hidden = undefined;
110
+ existing.lastAccessedAt = nowIso;
111
+ if (options.name && options.name.trim()) {
112
+ existing.name = options.name.trim();
113
+ }
114
+ const registry = await this.load();
115
+ await this.persist(registry);
116
+ return { ok: true, record: existing, registryUnchanged: false };
117
+ }
118
+ const record = {
119
+ workspaceId: workspaceIdForRealpath(normalized),
120
+ name: options.name?.trim() || path.basename(normalized) || normalized,
121
+ displayPath: normalized,
122
+ repoRoot: real,
123
+ repoFingerprint: repoFingerprintV1(real),
124
+ createdAt: nowIso,
125
+ lastAccessedAt: nowIso,
126
+ };
127
+ const registry = await this.load();
128
+ registry.workspaces.push(record);
129
+ await this.persist(registry);
130
+ return { ok: true, record, registryUnchanged: false };
131
+ }
132
+ /** Visible list in canonical insertion order. */
133
+ async listVisible() {
134
+ const registry = await this.load();
135
+ return registry.workspaces.filter((workspace) => {
136
+ if (workspace.hidden)
137
+ return false;
138
+ try {
139
+ return statSync(workspace.repoRoot).isDirectory();
140
+ }
141
+ catch {
142
+ // Keep historical registry data, but do not project stale test roots
143
+ // into the Workspace browser after their directories disappear.
144
+ return false;
145
+ }
146
+ });
147
+ }
148
+ async get(workspaceId) {
149
+ const registry = await this.load();
150
+ return registry.workspaces.find((w) => w.workspaceId === workspaceId);
151
+ }
152
+ /** Get a VISIBLE workspace by id (removed workspaces are not operable). */
153
+ async getVisible(workspaceId) {
154
+ const record = await this.get(workspaceId);
155
+ if (!record || record.hidden)
156
+ return undefined;
157
+ try {
158
+ return statSync(record.repoRoot).isDirectory() ? record : undefined;
159
+ }
160
+ catch {
161
+ return undefined;
162
+ }
163
+ }
164
+ /** Soft-remove: hides the record; never touches directories or AppData. */
165
+ async remove(workspaceId) {
166
+ const registry = await this.load();
167
+ const record = registry.workspaces.find((w) => w.workspaceId === workspaceId);
168
+ if (!record || record.hidden)
169
+ return false;
170
+ record.hidden = true;
171
+ await this.persist(registry);
172
+ return true;
173
+ }
174
+ async findByRealpath(rawPath) {
175
+ let normalized;
176
+ try {
177
+ normalized = normalizeWorktreeRealpath(path.resolve(rawPath.trim()));
178
+ }
179
+ catch {
180
+ return undefined;
181
+ }
182
+ return this.findByRealpathNormalized(normalized);
183
+ }
184
+ async findByRealpathNormalized(normalized) {
185
+ const registry = await this.load();
186
+ return registry.workspaces.find((w) => w.displayPath === normalized);
187
+ }
188
+ async load() {
189
+ if (this.cache)
190
+ return this.cache;
191
+ const raw = await readJsonIfExists(this.file).catch(() => undefined);
192
+ const registry = {
193
+ schemaVersion: 1,
194
+ workspaces: raw && Array.isArray(raw.workspaces)
195
+ ? raw.workspaces.filter((w) => Boolean(w) &&
196
+ typeof w === "object" &&
197
+ typeof w.workspaceId === "string" &&
198
+ typeof w.displayPath === "string" &&
199
+ typeof w.repoRoot === "string")
200
+ : [],
201
+ };
202
+ this.cache = registry;
203
+ return registry;
204
+ }
205
+ async persist(registry) {
206
+ // writeSecureJson is atomic; a reader mid-write sees the previous bytes.
207
+ await writeSecureJson(this.file, registry);
208
+ this.cache = registry;
209
+ }
210
+ }
211
+ /** Deterministic-looking nonce reserved for tests requiring unique roots. */
212
+ export function mintWorkspaceNonce() {
213
+ return randomBytes(8).toString("hex");
214
+ }
@@ -0,0 +1,26 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { buildGlobalSnapshot } from "./read-model.js";
4
+ /**
5
+ * Read-only runtime guard shared by Console maintenance and init-surface
6
+ * writes. Worker activity remains owned by the Observe projection so both
7
+ * callers use one canonical liveness interpretation.
8
+ */
9
+ export async function readInitRuntimeActivity(repoRoot) {
10
+ const resolvedRoot = path.resolve(repoRoot);
11
+ const activeDagRoot = path.join(resolvedRoot, ".harness", "dag-runs", "active");
12
+ const entries = await readdir(activeDagRoot, { withFileTypes: true }).catch(() => []);
13
+ const activeDagRunIds = entries
14
+ .filter((entry) => entry.isDirectory() && entry.name !== ".gitkeep")
15
+ .map((entry) => entry.name)
16
+ .sort();
17
+ const snapshot = await buildGlobalSnapshot({ repoRoot: resolvedRoot });
18
+ return {
19
+ activeDagRunIds,
20
+ activeWorkerBatches: snapshot.health.worker.activeBatches,
21
+ activeWorkerTasks: snapshot.health.worker.activeTasks,
22
+ ...(snapshot.projectionError
23
+ ? { workerProjectionError: snapshot.projectionError.message }
24
+ : {}),
25
+ };
26
+ }
@@ -31,6 +31,7 @@ function unavailableBody(dagRunId, nodeId, reason, warnings = []) {
31
31
  summary: null,
32
32
  dependency: null,
33
33
  prompt: null,
34
+ promptCandidates: [],
34
35
  executorInput: null,
35
36
  boundaries: null,
36
37
  promptFingerprint: null,
@@ -139,7 +140,7 @@ function resolveModelHint(spec, task) {
139
140
  }
140
141
  return null;
141
142
  }
142
- function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExtensionRunFacts, warnings) {
143
+ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExtensionRunFacts, promptCandidates, warnings) {
143
144
  const defaults = spec.defaults && typeof spec.defaults === "object"
144
145
  ? spec.defaults
145
146
  : undefined;
@@ -267,6 +268,7 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExt
267
268
  maxBytes: promptBody.maxBytes,
268
269
  source: promptSource,
269
270
  },
271
+ promptCandidates,
270
272
  executorInput: {
271
273
  kind,
272
274
  skills,
@@ -292,6 +294,89 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExt
292
294
  warnings,
293
295
  };
294
296
  }
297
+ async function loadFailedPromptCandidates(repoRoot, dagRunId, nodeId, warnings) {
298
+ const stateResolved = resolveDagRunArtifact(repoRoot, dagRunId, ["state.json"]);
299
+ if (!stateResolved.ok)
300
+ return [];
301
+ let state;
302
+ try {
303
+ state = JSON.parse(await readFile(stateResolved.result.absolutePath, "utf-8"));
304
+ }
305
+ catch {
306
+ warnings.push("state.json could not be read for failed prompt candidates.");
307
+ return [];
308
+ }
309
+ const nodes = state.nodes && typeof state.nodes === "object" && !Array.isArray(state.nodes)
310
+ ? state.nodes
311
+ : null;
312
+ const node = nodes?.[nodeId] &&
313
+ typeof nodes[nodeId] === "object" &&
314
+ !Array.isArray(nodes[nodeId])
315
+ ? nodes[nodeId]
316
+ : null;
317
+ if (!node)
318
+ return [];
319
+ const failedAttempts = new Set();
320
+ if (Array.isArray(node.attempts)) {
321
+ for (const entry of node.attempts) {
322
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
323
+ continue;
324
+ const record = entry;
325
+ if (record.ok === false &&
326
+ typeof record.attempt === "number" &&
327
+ Number.isSafeInteger(record.attempt) &&
328
+ record.attempt > 0) {
329
+ failedAttempts.add(record.attempt);
330
+ }
331
+ }
332
+ }
333
+ if (node.status === "ERROR" &&
334
+ typeof node.currentAttempt === "number" &&
335
+ Number.isSafeInteger(node.currentAttempt) &&
336
+ node.currentAttempt > 0) {
337
+ failedAttempts.add(node.currentAttempt);
338
+ }
339
+ const candidates = [];
340
+ for (const attempt of [...failedAttempts].sort((a, b) => a - b)) {
341
+ const fileName = `prompt-candidate-attempt-${attempt}.json`;
342
+ const artifact = path.posix.join(nodeId, fileName);
343
+ const resolved = resolveDagRunArtifact(repoRoot, dagRunId, [nodeId, fileName]);
344
+ if (!resolved.ok) {
345
+ warnings.push(`Failed prompt candidate missing for attempt ${attempt}.`);
346
+ continue;
347
+ }
348
+ try {
349
+ const raw = JSON.parse(await readFile(resolved.result.absolutePath, "utf-8"));
350
+ if (raw.schemaVersion !== 1 ||
351
+ raw.attempt !== attempt ||
352
+ typeof raw.failureCategory !== "string" ||
353
+ typeof raw.prompt !== "string" ||
354
+ typeof raw.truncated !== "boolean" ||
355
+ typeof raw.maxBytes !== "number" ||
356
+ !Number.isSafeInteger(raw.maxBytes) ||
357
+ raw.maxBytes < 1 ||
358
+ raw.maxBytes > NODE_INPUT_PREVIEW_MAX_BYTES ||
359
+ typeof raw.sha256 !== "string" ||
360
+ !/^([a-f0-9]{64})$/.test(raw.sha256)) {
361
+ throw new Error("invalid candidate schema");
362
+ }
363
+ const prompt = truncatedField(raw.prompt, raw.maxBytes);
364
+ candidates.push({
365
+ attempt,
366
+ failureCategory: redactSecrets(raw.failureCategory),
367
+ prompt: prompt.text,
368
+ truncated: raw.truncated || prompt.truncated,
369
+ maxBytes: raw.maxBytes,
370
+ sha256Prefix: raw.sha256.slice(0, 12),
371
+ artifact,
372
+ });
373
+ }
374
+ catch {
375
+ warnings.push(`Failed prompt candidate is invalid for attempt ${attempt}.`);
376
+ }
377
+ }
378
+ return candidates;
379
+ }
295
380
  /**
296
381
  * Load run-time Pi extension facts from <node>/pi-extensions.json (plan
297
382
  * 2026-08-22 R2). Returns null when the node declares no piExtensions
@@ -486,8 +571,9 @@ export async function buildDagNodeInput(repoRoot, dagRunId, nodeId) {
486
571
  ? task.piExtensions.filter((item) => typeof item === "string")
487
572
  : [];
488
573
  const piExtensionRunFacts = await loadPiExtensionRunFacts(repoRoot, dagRunId, nodeId, declaredPiExtensions, warnings);
574
+ const promptCandidates = await loadFailedPromptCandidates(repoRoot, dagRunId, nodeId, warnings);
489
575
  // Reject unknown nested leakage by projecting only through allowlist helpers.
490
- const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, piExtensionRunFacts, warnings);
576
+ const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, piExtensionRunFacts, promptCandidates, warnings);
491
577
  // Defense-in-depth: ensure raw task never sneaks in.
492
578
  if ("task" in body) {
493
579
  return {