@tea-agent/loop-agent 0.39.3 → 0.40.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/CHANGELOG.md +135 -92
  2. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  3. package/dist/adapters/context-transfer/pi-session.js +61 -0
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +12 -0
  6. package/dist/cli/program.js +23 -1
  7. package/dist/commands/dag-artifact.js +284 -0
  8. package/dist/commands/dag-context.js +184 -0
  9. package/dist/commands/dag-rerun.js +206 -1
  10. package/dist/executors/dag-pi-executor.js +108 -19
  11. package/dist/executors/pi-executor.js +4 -2
  12. package/dist/executors/pi-sdk-executor.js +49 -1
  13. package/dist/executors/pi-writer-tool-policy.js +57 -1
  14. package/dist/executors/shell-executor.js +42 -2
  15. package/dist/executors/workspace-write-snapshot.js +68 -0
  16. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  17. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  18. package/dist/infrastructure/console/context-export-store.js +160 -0
  19. package/dist/infrastructure/console/dir-lock.js +132 -0
  20. package/dist/infrastructure/harness/artifact-store.js +10 -1
  21. package/dist/infrastructure/harness/atomic-write.js +35 -2
  22. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  23. package/dist/shared/context-transfer.js +418 -0
  24. package/dist/shared/operator/capabilities.js +14 -0
  25. package/dist/shared/operator/safe-run-summary.js +1 -0
  26. package/dist/shared/path-safety.js +93 -0
  27. package/dist/shared/preview.js +28 -4
  28. package/dist/task/config-types.js +12 -1
  29. package/dist/task/source-prepare/fragment-inventory.js +1 -14
  30. package/dist/task/task-demand-routing.js +3 -0
  31. package/dist/worker/console/chat/chat-event-store.js +77 -7
  32. package/dist/worker/console/chat/context-panel.js +7 -0
  33. package/dist/worker/console/chat/deferred-turn.js +12 -0
  34. package/dist/worker/console/chat/pi-runtime.js +331 -18
  35. package/dist/worker/console/chat/provider-error.js +98 -0
  36. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  37. package/dist/worker/console/chat/routes.js +389 -56
  38. package/dist/worker/console/chat/session-stats.js +179 -0
  39. package/dist/worker/console/chat/session-store.js +94 -64
  40. package/dist/worker/console/chat/todos.js +115 -0
  41. package/dist/worker/console/chat/tool-preview.js +42 -0
  42. package/dist/worker/console/chat/turn-process.js +20 -55
  43. package/dist/worker/console/chat/user-questions.js +266 -0
  44. package/dist/worker/console/chat/workspace-landing.js +79 -0
  45. package/dist/worker/console/console-update-runtime.js +1 -1
  46. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  47. package/dist/worker/console/dag-confirmation.js +1 -1
  48. package/dist/worker/console/dag-execution-receipt.js +1 -1
  49. package/dist/worker/console/doctor.js +1 -1
  50. package/dist/worker/console/draft-store.js +1 -1
  51. package/dist/worker/console/human-gate-token.js +1 -1
  52. package/dist/worker/console/index.js +3 -3
  53. package/dist/worker/console/interview/assessment.js +1 -1
  54. package/dist/worker/console/interview/session.js +1 -1
  55. package/dist/worker/console/native-directory-picker.js +13 -0
  56. package/dist/worker/console/operation-runner.js +45 -4
  57. package/dist/worker/console/operation-sse.js +1 -1
  58. package/dist/worker/console/operation-wait.js +1 -1
  59. package/dist/worker/console/operator-actions.js +104 -3
  60. package/dist/worker/console/operator-surface-health.js +1 -1
  61. package/dist/worker/console/operator-user-error.js +128 -0
  62. package/dist/worker/console/routes.js +927 -8
  63. package/dist/worker/console/security.js +39 -4
  64. package/dist/worker/console/server.js +532 -187
  65. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CpWvwARc.js} +1 -1
  66. package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DctXgO-g.js} +1 -1
  67. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-D4mIkoQl.js} +1 -1
  68. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-ClG0--KO.js} +1 -1
  69. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-CwdLk78T.js} +1 -1
  70. package/dist/worker/console/static/assets/channel-C9VQZ-ru.js +1 -0
  71. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DKczcu-O.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-BV6F-vBI.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtojVnvA.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-BjDQRGnO.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-I0fV6ngU.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-BM8C-mmC.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-BEK-a15e.js} +1 -1
  78. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-CQsg_fSR.js} +1 -1
  79. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-B-BcaUm3.js} +1 -1
  80. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D1Bu08fg.js} +1 -1
  81. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BsCkw5MF.js +1 -0
  82. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BsCkw5MF.js +1 -0
  83. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-q8nwtxBR.js} +1 -1
  84. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CruhpURw.js} +1 -1
  85. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-CJne6iYF.js} +1 -1
  86. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-CbeLvBU5.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-DmCxH1Pe.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-DJ5OYhtC.js} +1 -1
  89. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-B-UN-hln.js} +1 -1
  90. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-CcEro8Ey.js} +1 -1
  91. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BBBzDden.js} +1 -1
  92. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-xKEyH-oo.js} +1 -1
  93. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-C7xl63VC.js} +1 -1
  94. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-nthQQ9nR.js} +1 -1
  95. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CWN5mE33.js} +1 -1
  96. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-Wn86tQ6N.js} +1 -1
  97. package/dist/worker/console/static/assets/index-D-0NVyMk.css +1 -0
  98. package/dist/worker/console/static/assets/index-gwyOxQiS.js +407 -0
  99. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-_WQymGtW.js} +1 -1
  100. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-COBeivBm.js} +1 -1
  101. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-CwGOJl3Q.js} +1 -1
  102. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-Ci0CJ5Vi.js} +1 -1
  103. package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CPjMq6vN.js} +1 -1
  104. package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-Z0wGoe0b.js} +5 -5
  105. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-dISked0d.js} +1 -1
  106. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-BJxgof7Y.js} +1 -1
  107. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-CIblU8eW.js} +1 -1
  108. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-vwYiqh8E.js} +1 -1
  109. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-_iOsVeFm.js} +1 -1
  110. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Dk0MLxO_.js} +1 -1
  111. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-Dx7HtLVW.js} +1 -1
  112. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-DRuWjlpL.js} +1 -1
  113. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Bhy9DQAA.js} +1 -1
  114. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-DAx9twnU.js} +1 -1
  115. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dmdf8cGy.js +1 -0
  116. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-okMlvVrM.js} +2 -2
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-W1IcAFff.js +8 -0
  118. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DXqCKe37.js} +1 -1
  119. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CeIUkwBh.js} +1 -1
  120. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-BP76BKTN.js} +1 -1
  121. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-BpVI0OpF.js} +1 -1
  122. package/dist/worker/console/static/index.html +7 -2
  123. package/dist/worker/console/static-src/app/console-types.js +29 -4
  124. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  125. package/dist/worker/console/static-src/app/useOperatorActions.js +36 -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 +2 -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 +260 -0
  147. package/dist/worker/console/workspace-registry.js +214 -0
  148. package/dist/worker/continuation/worker-continuation.js +278 -0
  149. package/dist/worker/observe/health.js +1 -1
  150. package/dist/worker/observe/node-transparency.js +572 -0
  151. package/dist/worker/observe/routes.js +51 -1
  152. package/dist/worker/observe/server.js +4 -0
  153. package/dist/worker/observe/static/api.js +94 -10
  154. package/dist/worker/observe/static/app.js +18 -70
  155. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  156. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  157. package/dist/worker/observe/static/index.html +1 -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/router.d.ts +46 -0
  163. package/dist/worker/observe/static/router.js +25 -1
  164. package/dist/worker/observe/static/state.d.ts +50 -0
  165. package/dist/worker/observe/static/state.js +175 -1
  166. package/dist/worker/observe/static/styles.css +1039 -128
  167. package/dist/worker/observe/static/views/dag-inspector.js +1218 -60
  168. package/dist/worker/observe/static/views/dag.js +2 -1
  169. package/dist/worker/observe/static/views/dashboard.js +6 -4
  170. package/dist/worker/observe/static/views/night.js +2 -2
  171. package/dist/worker/observe/static/views/pool.js +1 -0
  172. package/dist/worker/observe/static/views/run.js +3 -1
  173. package/dist/worker/observe/static/views/session-timeline.js +75 -10
  174. package/dist/worker/observe/static/views/task.js +1 -1
  175. package/dist/worker/outcomes/adapters.js +19 -0
  176. package/dist/worker/outcomes/types.js +1 -0
  177. package/dist/worker/pool/attempt-lease.js +97 -66
  178. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  179. package/dist/worker/pool/reconcile.js +46 -1
  180. package/dist/worker/pool/run-store.js +2 -0
  181. package/dist/worker/pool/state-projection.js +2 -0
  182. package/dist/worker/task-spec/workflow-routing.js +8 -3
  183. package/dist/workflows/dag/artifact-bindings.js +149 -0
  184. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  185. package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
  186. package/dist/workflows/dag/backend-test-writer-completeness.js +7 -12
  187. package/dist/workflows/dag/context-receipt.js +305 -0
  188. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  189. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  190. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  191. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  192. package/dist/workflows/dag/frontend-implementation-contract.js +1 -89
  193. package/dist/workflows/dag/init-hybrid.js +259 -47
  194. package/dist/workflows/dag/interrupt-request.js +3 -3
  195. package/dist/workflows/dag/lifecycle.js +3 -3
  196. package/dist/workflows/dag/node-execution.js +112 -27
  197. package/dist/workflows/dag/path-safety.js +1 -0
  198. package/dist/workflows/dag/prompt.js +29 -4
  199. package/dist/workflows/dag/rerun-plan.js +272 -0
  200. package/dist/workflows/dag/rerun-run.js +390 -12
  201. package/dist/workflows/dag/skill-snapshot.js +1 -0
  202. package/dist/workflows/dag/types.js +203 -8
  203. package/docs/README.md +1 -0
  204. package/docs/architecture/runtime-boundaries.md +6 -6
  205. package/docs/architecture/worker-and-feature.md +1 -1
  206. package/docs/operations/local-development-environment.md +21 -0
  207. package/docs/templates/agent-dag.schema.json +50 -0
  208. package/docs/templates/backend-test-dag.json +265 -237
  209. package/docs/templates/product-line/task.yaml +1 -1
  210. package/harness.json +3 -3
  211. package/package.json +5 -1
  212. package/skills/loop-agent/references/command-reference.md +6 -0
  213. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
  214. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
  215. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
  216. package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
  217. package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
  218. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
  219. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
  220. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  221. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  222. /package/dist/{worker → infrastructure}/console/operation-store.js +0 -0
  223. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -0,0 +1,260 @@
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 "../../infrastructure/console/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 "../../infrastructure/console/operation-store.js";
38
+ import { reconcileContextExports } from "../../infrastructure/console/context-export-store.js";
39
+ import { reconcileArtifactRevisions } from "../../infrastructure/console/artifact-revision-store.js";
40
+ import { recordContextTransferEvent } from "./context-transfer-diagnostics.js";
41
+ import { probePiReadiness, } from "./pi-readiness.js";
42
+ import { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
43
+ import { NightAuxTicker } from "./night-aux-ticker.js";
44
+ import { createObserveRouteContext, } from "../observe/routes.js";
45
+ import { WorkspaceRegistry } from "./workspace-registry.js";
46
+ import { resolveConsoleDefaultTimeoutMs } from "./server.js";
47
+ /**
48
+ * Lazily create the full repo-scoped Console context for a registered
49
+ * workspace record. Pure factory — no global registry; the caller owns
50
+ * lifecycle (single creation per workspaceId, dispose on server close).
51
+ */
52
+ export async function createWorkspaceContext(record, deps) {
53
+ const repoRoot = record.repoRoot;
54
+ const appData = openConsoleAppData({
55
+ repoRoot,
56
+ ...(deps.appDataRoot ? { appDataRoot: deps.appDataRoot } : {}),
57
+ });
58
+ const registry = new WorkspaceRegistry(appData);
59
+ const readiness = await probePiReadiness({
60
+ override: deps.piReadinessProbe,
61
+ repoRoot,
62
+ });
63
+ let readinessCache = readiness;
64
+ const artifactRoot = path.join(appData.root, "client-artifacts", appData.fingerprint);
65
+ const client = deps.createClient?.(artifactRoot) ??
66
+ new LoopAgentClient({
67
+ loopAgentBin: resolveSiblingLoopAgentBin(),
68
+ artifactRoot,
69
+ resolveIdentity: true,
70
+ defaultTimeoutMs: resolveConsoleDefaultTimeoutMs(deps.defaultTimeoutMs),
71
+ });
72
+ const operations = new OperationStore(appData);
73
+ const events = createOperationEventStore({
74
+ persistenceDir: path.join(appData.operations, "events"),
75
+ });
76
+ const drafts = new DraftStore(appData);
77
+ const confirmations = new DagConfirmationStore(appData, deps.bootToken.confirmationToken);
78
+ const dagExecutionReceipts = new DagExecutionReceiptStore(appData);
79
+ const interviews = new InterviewSessionStore(appData);
80
+ const assessments = new AssessmentStore(appData);
81
+ if (!deps.skipReconcile) {
82
+ await operations.reconcileOnBoot();
83
+ const [expiredExports, expiredRevisions] = await Promise.all([
84
+ reconcileContextExports({ appData }),
85
+ reconcileArtifactRevisions({ appData }),
86
+ ]);
87
+ // Boot-time lifecycle evidence for what reconcile reclaimed. Only
88
+ // 32-hex ids are real export/revision identities; uploads/ and lock/
89
+ // entries are blob housekeeping and get no event.
90
+ await Promise.all([
91
+ ...expiredExports.removed
92
+ .filter((exportId) => /^[a-f0-9]{32}$/.test(exportId))
93
+ .map((exportId) => recordContextTransferEvent({
94
+ appData,
95
+ event: "context_export.expired",
96
+ fields: { exportId },
97
+ }).catch(() => undefined)),
98
+ ...expiredRevisions
99
+ .filter((revisionId) => /^[a-f0-9]{32}$/.test(revisionId))
100
+ .map((revisionId) => recordContextTransferEvent({
101
+ appData,
102
+ event: "artifact_revision.expired",
103
+ fields: { revisionId },
104
+ }).catch(() => undefined)),
105
+ ]);
106
+ }
107
+ const getReadiness = () => readinessCache;
108
+ const refreshReadiness = async () => {
109
+ readinessCache = await probePiReadiness({
110
+ override: deps.piReadinessProbe,
111
+ repoRoot,
112
+ });
113
+ actionContext.readiness = readinessCache;
114
+ return readinessCache;
115
+ };
116
+ const actionContext = {
117
+ repoRoot,
118
+ appData,
119
+ operatorSessionId: deps.operatorSessionId,
120
+ client,
121
+ operations,
122
+ events,
123
+ drafts,
124
+ confirmations,
125
+ dagExecutionReceipts,
126
+ humanGateSecret: deps.bootToken.confirmationToken,
127
+ interviews,
128
+ assessments,
129
+ readiness: readinessCache,
130
+ getReadiness,
131
+ refreshReadiness,
132
+ observeBaseUrl: deps.observeBaseUrl,
133
+ fetchImpl: deps.fetchImpl,
134
+ getConsoleOrigin: deps.getConsoleOrigin,
135
+ };
136
+ const chatStore = new ChatSessionStore(appData);
137
+ const chatEventEpoch = randomBytes(12).toString("hex");
138
+ const chatEvents = createChatEventStore({
139
+ appData,
140
+ epoch: chatEventEpoch,
141
+ });
142
+ const chatTurnExecutions = createChatTurnExecutionRegistry();
143
+ const questionRegistry = new UserQuestionRegistry(path.join(appData.chats, "user-questions"));
144
+ const todoRegistry = new TodoListRegistry();
145
+ questionRegistry.subscribe((change) => {
146
+ const question = change.question;
147
+ chatEvents.append(question.sessionId, question.turnId ?? `${question.sessionId}:question`, {
148
+ kind: `question_${change.type}`,
149
+ data: { question },
150
+ });
151
+ });
152
+ todoRegistry.subscribe((change) => {
153
+ chatEvents.append(change.sessionId, change.turnId ?? `${change.sessionId}:todo`, {
154
+ kind: change.type === "updated" ? "todo_updated" : "todo_cleared",
155
+ data: {
156
+ todos: change.todos,
157
+ ...(change.toolCallId ? { toolCallId: change.toolCallId } : {}),
158
+ },
159
+ });
160
+ });
161
+ const chatOperationLinker = createChatOperationLinker({
162
+ chatEvents,
163
+ operations,
164
+ operationEvents: events,
165
+ });
166
+ const chatRuntime = new ConsolePiRuntime({
167
+ cwd: repoRoot,
168
+ sessionDir: appData.chatSessions,
169
+ resourcePreferencesPath: appData.resourcePreferences,
170
+ actionContext,
171
+ questionRegistry,
172
+ todoRegistry,
173
+ });
174
+ const chatRouteDeps = {
175
+ runtime: chatRuntime,
176
+ store: chatStore,
177
+ events: chatEvents,
178
+ turnExecutions: chatTurnExecutions,
179
+ questionRegistry,
180
+ todoRegistry,
181
+ operationLinker: chatOperationLinker,
182
+ appData,
183
+ bootToken: deps.bootToken,
184
+ getConsoleOrigin: deps.getConsoleOrigin,
185
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
186
+ };
187
+ const { recoverDeferredTurns } = await import("./chat/routes.js");
188
+ const nightAuxTicker = new NightAuxTicker({ repoRoot, client });
189
+ const observeRouteContext = createObserveRouteContext(repoRoot, {
190
+ staticDir: deps.inspectStaticDir,
191
+ });
192
+ let disposed = false;
193
+ const context = {
194
+ workspaceId: record.workspaceId,
195
+ record,
196
+ repoRoot,
197
+ appData,
198
+ registry,
199
+ client,
200
+ operations,
201
+ events,
202
+ drafts,
203
+ confirmations,
204
+ dagExecutionReceipts,
205
+ interviews,
206
+ assessments,
207
+ chatRouteDeps,
208
+ chatOperationLinker,
209
+ chatRuntime,
210
+ nightAuxTicker,
211
+ actionContext,
212
+ observeRouteContext,
213
+ getReadiness,
214
+ refreshReadiness,
215
+ recoverDeferredTurns: () => recoverDeferredTurns(chatRouteDeps),
216
+ dispose: () => {
217
+ if (disposed)
218
+ return;
219
+ disposed = true;
220
+ try {
221
+ chatOperationLinker.dispose();
222
+ }
223
+ catch {
224
+ // best-effort
225
+ }
226
+ try {
227
+ chatRuntime.disposeAll();
228
+ }
229
+ catch {
230
+ // best-effort
231
+ }
232
+ try {
233
+ nightAuxTicker.dispose();
234
+ }
235
+ catch {
236
+ // best-effort
237
+ }
238
+ },
239
+ };
240
+ return context;
241
+ }
242
+ /** Inspect-facing readiness/capability summary for a registry record. */
243
+ export async function summarizeWorkspaceRecord(record, deps) {
244
+ let state;
245
+ try {
246
+ const report = await probePiReadiness({
247
+ override: deps.piReadinessProbe,
248
+ repoRoot: record.repoRoot,
249
+ });
250
+ state = report.state;
251
+ }
252
+ catch {
253
+ state = "setup-required";
254
+ }
255
+ return {
256
+ state,
257
+ summary: `pi=${state}; inspect=${deps.inspectReady ? "ready" : "unavailable"}`,
258
+ };
259
+ }
260
+ 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 "../../infrastructure/console/app-data.js";
22
+ import { normalizeWorktreeRealpath, repoFingerprintV1, } from "../../infrastructure/console/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
+ }