@oscharko-dev/keiko-server 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/chat-handlers.d.ts +18 -2
  3. package/dist/chat-handlers.d.ts.map +1 -1
  4. package/dist/chat-handlers.js +185 -3
  5. package/dist/command-runner-errors.d.ts +17 -0
  6. package/dist/command-runner-errors.d.ts.map +1 -0
  7. package/dist/command-runner-errors.js +37 -0
  8. package/dist/command-runner-evidence.d.ts +23 -0
  9. package/dist/command-runner-evidence.d.ts.map +1 -0
  10. package/dist/command-runner-evidence.js +69 -0
  11. package/dist/command-runner-routes.d.ts +7 -0
  12. package/dist/command-runner-routes.d.ts.map +1 -0
  13. package/dist/command-runner-routes.js +175 -0
  14. package/dist/command-runner.d.ts +29 -0
  15. package/dist/command-runner.d.ts.map +1 -0
  16. package/dist/command-runner.js +348 -0
  17. package/dist/conversation-prompt.d.ts +2 -2
  18. package/dist/conversation-prompt.d.ts.map +1 -1
  19. package/dist/conversation-prompt.js +17 -1
  20. package/dist/csp.d.ts.map +1 -1
  21. package/dist/csp.js +3 -0
  22. package/dist/deps.d.ts +28 -1
  23. package/dist/deps.d.ts.map +1 -1
  24. package/dist/deps.js +288 -13
  25. package/dist/discussion-prompt.d.ts +4 -0
  26. package/dist/discussion-prompt.d.ts.map +1 -0
  27. package/dist/discussion-prompt.js +19 -0
  28. package/dist/editor/agentActionAudit.d.ts +18 -0
  29. package/dist/editor/agentActionAudit.d.ts.map +1 -0
  30. package/dist/editor/agentActionAudit.js +80 -0
  31. package/dist/editor/agentRoutes.d.ts +1 -0
  32. package/dist/editor/agentRoutes.d.ts.map +1 -1
  33. package/dist/editor/agentRoutes.js +292 -55
  34. package/dist/editor/agentSessionRegistry.d.ts +35 -0
  35. package/dist/editor/agentSessionRegistry.d.ts.map +1 -0
  36. package/dist/editor/agentSessionRegistry.js +243 -0
  37. package/dist/editor/completionRoutes.d.ts.map +1 -1
  38. package/dist/editor/completionRoutes.js +5 -10
  39. package/dist/editor/languageRoutes.d.ts +12 -1
  40. package/dist/editor/languageRoutes.d.ts.map +1 -1
  41. package/dist/editor/languageRoutes.js +71 -8
  42. package/dist/editor/languageService.d.ts +3 -2
  43. package/dist/editor/languageService.d.ts.map +1 -1
  44. package/dist/editor/languageService.js +41 -3
  45. package/dist/editor/languageServiceHost.d.ts.map +1 -1
  46. package/dist/editor/languageServiceHost.js +2 -2
  47. package/dist/editor/lsp/hostLanguageOperation.d.ts +17 -0
  48. package/dist/editor/lsp/hostLanguageOperation.d.ts.map +1 -0
  49. package/dist/editor/lsp/hostLanguageOperation.js +436 -0
  50. package/dist/editor/lsp/hostLanguageProviders.d.ts +26 -0
  51. package/dist/editor/lsp/hostLanguageProviders.d.ts.map +1 -0
  52. package/dist/editor/lsp/hostLanguageProviders.js +161 -0
  53. package/dist/editor/lsp/lspFrameCodec.d.ts +13 -0
  54. package/dist/editor/lsp/lspFrameCodec.d.ts.map +1 -0
  55. package/dist/editor/lsp/lspFrameCodec.js +164 -0
  56. package/dist/editor/lsp/lspJsonRpcClient.d.ts +34 -0
  57. package/dist/editor/lsp/lspJsonRpcClient.d.ts.map +1 -0
  58. package/dist/editor/lsp/lspJsonRpcClient.js +173 -0
  59. package/dist/editor/lsp/lspLanguageProvider.d.ts +7 -0
  60. package/dist/editor/lsp/lspLanguageProvider.d.ts.map +1 -0
  61. package/dist/editor/lsp/lspLanguageProvider.js +29 -0
  62. package/dist/editor/lsp/lspLifecycleLedger.d.ts +5 -0
  63. package/dist/editor/lsp/lspLifecycleLedger.d.ts.map +1 -0
  64. package/dist/editor/lsp/lspLifecycleLedger.js +37 -0
  65. package/dist/editor/lsp/lspNodeAdapter.d.ts +31 -0
  66. package/dist/editor/lsp/lspNodeAdapter.d.ts.map +1 -0
  67. package/dist/editor/lsp/lspNodeAdapter.js +230 -0
  68. package/dist/editor/lsp/lspProcessManager.d.ts +24 -0
  69. package/dist/editor/lsp/lspProcessManager.d.ts.map +1 -0
  70. package/dist/editor/lsp/lspProcessManager.js +255 -0
  71. package/dist/editor/lsp/lspRestartThrottle.d.ts +6 -0
  72. package/dist/editor/lsp/lspRestartThrottle.d.ts.map +1 -0
  73. package/dist/editor/lsp/lspRestartThrottle.js +24 -0
  74. package/dist/editor/lsp/lspStatusRoute.d.ts +8 -0
  75. package/dist/editor/lsp/lspStatusRoute.d.ts.map +1 -0
  76. package/dist/editor/lsp/lspStatusRoute.js +22 -0
  77. package/dist/editor/lsp/lspTransport.d.ts +19 -0
  78. package/dist/editor/lsp/lspTransport.d.ts.map +1 -0
  79. package/dist/editor/lsp/lspTransport.js +55 -0
  80. package/dist/editor/lsp/testing/fakeLspProcess.d.ts +23 -0
  81. package/dist/editor/lsp/testing/fakeLspProcess.d.ts.map +1 -0
  82. package/dist/editor/lsp/testing/fakeLspProcess.js +132 -0
  83. package/dist/files.d.ts +63 -0
  84. package/dist/files.d.ts.map +1 -1
  85. package/dist/files.js +799 -1
  86. package/dist/gateway-readiness.d.ts +6 -0
  87. package/dist/gateway-readiness.d.ts.map +1 -0
  88. package/dist/gateway-readiness.js +624 -0
  89. package/dist/gateway-setup.d.ts +2 -0
  90. package/dist/gateway-setup.d.ts.map +1 -1
  91. package/dist/gateway-setup.js +275 -11
  92. package/dist/gitDelivery/actionSheetProjection.d.ts +30 -0
  93. package/dist/gitDelivery/actionSheetProjection.d.ts.map +1 -0
  94. package/dist/gitDelivery/actionSheetProjection.js +206 -0
  95. package/dist/gitDelivery/actionSheetRoutes.d.ts +29 -0
  96. package/dist/gitDelivery/actionSheetRoutes.d.ts.map +1 -0
  97. package/dist/gitDelivery/actionSheetRoutes.js +293 -0
  98. package/dist/gitDelivery/agentOperationsRoutes.d.ts +33 -0
  99. package/dist/gitDelivery/agentOperationsRoutes.d.ts.map +1 -0
  100. package/dist/gitDelivery/agentOperationsRoutes.js +405 -0
  101. package/dist/gitDelivery/commitRoutes.d.ts +23 -0
  102. package/dist/gitDelivery/commitRoutes.d.ts.map +1 -0
  103. package/dist/gitDelivery/commitRoutes.js +204 -0
  104. package/dist/gitDelivery/evidenceRoutes.d.ts +9 -0
  105. package/dist/gitDelivery/evidenceRoutes.d.ts.map +1 -0
  106. package/dist/gitDelivery/evidenceRoutes.js +101 -0
  107. package/dist/gitDelivery/execution.d.ts +38 -0
  108. package/dist/gitDelivery/execution.d.ts.map +1 -0
  109. package/dist/gitDelivery/execution.js +117 -0
  110. package/dist/gitDelivery/localMutationRoutes.d.ts +30 -0
  111. package/dist/gitDelivery/localMutationRoutes.d.ts.map +1 -0
  112. package/dist/gitDelivery/localMutationRoutes.js +165 -0
  113. package/dist/gitDelivery/mergeExecution.d.ts +63 -0
  114. package/dist/gitDelivery/mergeExecution.d.ts.map +1 -0
  115. package/dist/gitDelivery/mergeExecution.js +168 -0
  116. package/dist/gitDelivery/mergeRoutes.d.ts +12 -0
  117. package/dist/gitDelivery/mergeRoutes.d.ts.map +1 -0
  118. package/dist/gitDelivery/mergeRoutes.js +218 -0
  119. package/dist/gitDelivery/mutationEvidenceLedger.d.ts +23 -0
  120. package/dist/gitDelivery/mutationEvidenceLedger.d.ts.map +1 -0
  121. package/dist/gitDelivery/mutationEvidenceLedger.js +87 -0
  122. package/dist/gitDelivery/prExecution.d.ts +54 -0
  123. package/dist/gitDelivery/prExecution.d.ts.map +1 -0
  124. package/dist/gitDelivery/prExecution.js +192 -0
  125. package/dist/gitDelivery/prRoutes.d.ts +12 -0
  126. package/dist/gitDelivery/prRoutes.d.ts.map +1 -0
  127. package/dist/gitDelivery/prRoutes.js +256 -0
  128. package/dist/gitDelivery/pushExecution.d.ts +43 -0
  129. package/dist/gitDelivery/pushExecution.d.ts.map +1 -0
  130. package/dist/gitDelivery/pushExecution.js +124 -0
  131. package/dist/gitDelivery/pushRoutes.d.ts +12 -0
  132. package/dist/gitDelivery/pushRoutes.d.ts.map +1 -0
  133. package/dist/gitDelivery/pushRoutes.js +200 -0
  134. package/dist/gitDelivery/requestGuards.d.ts +15 -0
  135. package/dist/gitDelivery/requestGuards.d.ts.map +1 -0
  136. package/dist/gitDelivery/requestGuards.js +97 -0
  137. package/dist/gitDelivery/syncEvidence.d.ts +37 -0
  138. package/dist/gitDelivery/syncEvidence.d.ts.map +1 -0
  139. package/dist/gitDelivery/syncEvidence.js +85 -0
  140. package/dist/gitDelivery/syncExecution.d.ts +30 -0
  141. package/dist/gitDelivery/syncExecution.d.ts.map +1 -0
  142. package/dist/gitDelivery/syncExecution.js +266 -0
  143. package/dist/gitDelivery/syncRoutes.d.ts +13 -0
  144. package/dist/gitDelivery/syncRoutes.d.ts.map +1 -0
  145. package/dist/gitDelivery/syncRoutes.js +200 -0
  146. package/dist/gitPorcelainStatus.d.ts +15 -0
  147. package/dist/gitPorcelainStatus.d.ts.map +1 -0
  148. package/dist/gitPorcelainStatus.js +104 -0
  149. package/dist/gitRepositoryReads.d.ts +10 -0
  150. package/dist/gitRepositoryReads.d.ts.map +1 -0
  151. package/dist/gitRepositoryReads.js +314 -0
  152. package/dist/gitRepositoryRoutes.d.ts +7 -0
  153. package/dist/gitRepositoryRoutes.d.ts.map +1 -0
  154. package/dist/gitRepositoryRoutes.js +221 -0
  155. package/dist/gitRoutes.d.ts +66 -0
  156. package/dist/gitRoutes.d.ts.map +1 -0
  157. package/dist/gitRoutes.js +543 -0
  158. package/dist/governed-workflow.d.ts +2 -0
  159. package/dist/governed-workflow.d.ts.map +1 -1
  160. package/dist/governed-workflow.js +4 -0
  161. package/dist/grounded-qa-hybrid.d.ts.map +1 -1
  162. package/dist/grounded-qa-hybrid.js +2 -0
  163. package/dist/grounded-qa-multi-source.d.ts.map +1 -1
  164. package/dist/grounded-qa-multi-source.js +1 -0
  165. package/dist/grounded-qa.d.ts +11 -0
  166. package/dist/grounded-qa.d.ts.map +1 -1
  167. package/dist/grounded-qa.js +14 -4
  168. package/dist/headers.d.ts +4 -1
  169. package/dist/headers.d.ts.map +1 -1
  170. package/dist/headers.js +11 -4
  171. package/dist/index.d.ts +8 -1
  172. package/dist/index.d.ts.map +1 -1
  173. package/dist/index.js +9 -1
  174. package/dist/local-knowledge-grounded-qa.d.ts.map +1 -1
  175. package/dist/local-knowledge-grounded-qa.js +11 -2
  176. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts +1 -1
  177. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts.map +1 -1
  178. package/dist/qualityIntelligence/figmaSnapshotRoutes.js +1 -1
  179. package/dist/read-handlers.d.ts +5 -0
  180. package/dist/read-handlers.d.ts.map +1 -1
  181. package/dist/read-handlers.js +57 -1
  182. package/dist/routes.d.ts.map +1 -1
  183. package/dist/routes.js +260 -12
  184. package/dist/run-engine.d.ts.map +1 -1
  185. package/dist/run-engine.js +3 -0
  186. package/dist/run-handlers.d.ts +0 -1
  187. package/dist/run-handlers.d.ts.map +1 -1
  188. package/dist/run-handlers.js +64 -211
  189. package/dist/run-request.d.ts +11 -0
  190. package/dist/run-request.d.ts.map +1 -1
  191. package/dist/run-request.js +158 -10
  192. package/dist/runtime/capabilityDetector.d.ts +38 -0
  193. package/dist/runtime/capabilityDetector.d.ts.map +1 -0
  194. package/dist/runtime/capabilityDetector.js +443 -0
  195. package/dist/runtime/capabilityRoutes.d.ts +9 -0
  196. package/dist/runtime/capabilityRoutes.d.ts.map +1 -0
  197. package/dist/runtime/capabilityRoutes.js +45 -0
  198. package/dist/runtime/containerEngineDetector.d.ts +17 -0
  199. package/dist/runtime/containerEngineDetector.d.ts.map +1 -0
  200. package/dist/runtime/containerEngineDetector.js +222 -0
  201. package/dist/runtime/containerRoutes.d.ts +8 -0
  202. package/dist/runtime/containerRoutes.d.ts.map +1 -0
  203. package/dist/runtime/containerRoutes.js +207 -0
  204. package/dist/runtime/containerRunner-errors.d.ts +18 -0
  205. package/dist/runtime/containerRunner-errors.d.ts.map +1 -0
  206. package/dist/runtime/containerRunner-errors.js +42 -0
  207. package/dist/runtime/containerRunner-evidence.d.ts +24 -0
  208. package/dist/runtime/containerRunner-evidence.d.ts.map +1 -0
  209. package/dist/runtime/containerRunner-evidence.js +74 -0
  210. package/dist/runtime/containerRunner.d.ts +37 -0
  211. package/dist/runtime/containerRunner.d.ts.map +1 -0
  212. package/dist/runtime/containerRunner.js +443 -0
  213. package/dist/server.d.ts.map +1 -1
  214. package/dist/server.js +24 -4
  215. package/dist/store/db.d.ts.map +1 -1
  216. package/dist/store/db.js +2 -1
  217. package/dist/store/index.d.ts +1 -1
  218. package/dist/store/index.d.ts.map +1 -1
  219. package/dist/store/messages.d.ts +2 -1
  220. package/dist/store/messages.d.ts.map +1 -1
  221. package/dist/store/messages.js +46 -4
  222. package/dist/store/schema.d.ts +1 -1
  223. package/dist/store/schema.d.ts.map +1 -1
  224. package/dist/store/schema.js +68 -1
  225. package/dist/store/types.d.ts +3 -2
  226. package/dist/store/types.d.ts.map +1 -1
  227. package/dist/task-workspace/active-store.d.ts +21 -0
  228. package/dist/task-workspace/active-store.d.ts.map +1 -0
  229. package/dist/task-workspace/active-store.js +55 -0
  230. package/dist/task-workspace/authorization.d.ts +7 -0
  231. package/dist/task-workspace/authorization.d.ts.map +1 -0
  232. package/dist/task-workspace/authorization.js +54 -0
  233. package/dist/task-workspace/binding.d.ts +3 -0
  234. package/dist/task-workspace/binding.d.ts.map +1 -0
  235. package/dist/task-workspace/binding.js +22 -0
  236. package/dist/task-workspace/cleanup.d.ts +4 -0
  237. package/dist/task-workspace/cleanup.d.ts.map +1 -0
  238. package/dist/task-workspace/cleanup.js +428 -0
  239. package/dist/task-workspace/errors.d.ts +14 -0
  240. package/dist/task-workspace/errors.d.ts.map +1 -0
  241. package/dist/task-workspace/errors.js +81 -0
  242. package/dist/task-workspace/evidence.d.ts +32 -0
  243. package/dist/task-workspace/evidence.d.ts.map +1 -0
  244. package/dist/task-workspace/evidence.js +52 -0
  245. package/dist/task-workspace/field-safety.d.ts +3 -0
  246. package/dist/task-workspace/field-safety.d.ts.map +1 -0
  247. package/dist/task-workspace/field-safety.js +42 -0
  248. package/dist/task-workspace/health.d.ts +4 -0
  249. package/dist/task-workspace/health.d.ts.map +1 -0
  250. package/dist/task-workspace/health.js +163 -0
  251. package/dist/task-workspace/lifecycle.d.ts +3 -0
  252. package/dist/task-workspace/lifecycle.d.ts.map +1 -0
  253. package/dist/task-workspace/lifecycle.js +248 -0
  254. package/dist/task-workspace/locks.d.ts +13 -0
  255. package/dist/task-workspace/locks.d.ts.map +1 -0
  256. package/dist/task-workspace/locks.js +44 -0
  257. package/dist/task-workspace/managed-root.d.ts +7 -0
  258. package/dist/task-workspace/managed-root.d.ts.map +1 -0
  259. package/dist/task-workspace/managed-root.js +98 -0
  260. package/dist/task-workspace/mutex.d.ts +8 -0
  261. package/dist/task-workspace/mutex.d.ts.map +1 -0
  262. package/dist/task-workspace/mutex.js +82 -0
  263. package/dist/task-workspace/naming.d.ts +15 -0
  264. package/dist/task-workspace/naming.d.ts.map +1 -0
  265. package/dist/task-workspace/naming.js +0 -0
  266. package/dist/task-workspace/provisioning.d.ts +3 -0
  267. package/dist/task-workspace/provisioning.d.ts.map +1 -0
  268. package/dist/task-workspace/provisioning.js +528 -0
  269. package/dist/task-workspace/reconciliation.d.ts +15 -0
  270. package/dist/task-workspace/reconciliation.d.ts.map +1 -0
  271. package/dist/task-workspace/reconciliation.js +274 -0
  272. package/dist/task-workspace/repair.d.ts +3 -0
  273. package/dist/task-workspace/repair.d.ts.map +1 -0
  274. package/dist/task-workspace/repair.js +286 -0
  275. package/dist/task-workspace/routes.d.ts +19 -0
  276. package/dist/task-workspace/routes.d.ts.map +1 -0
  277. package/dist/task-workspace/routes.js +481 -0
  278. package/dist/task-workspace/store.d.ts +12 -0
  279. package/dist/task-workspace/store.d.ts.map +1 -0
  280. package/dist/task-workspace/store.js +128 -0
  281. package/dist/task-workspace/types.d.ts +170 -0
  282. package/dist/task-workspace/types.d.ts.map +1 -0
  283. package/dist/task-workspace/types.js +5 -0
  284. package/dist/voice-action-governance.d.ts +23 -0
  285. package/dist/voice-action-governance.d.ts.map +1 -0
  286. package/dist/voice-action-governance.js +126 -0
  287. package/dist/voice-handlers.d.ts +6 -0
  288. package/dist/voice-handlers.d.ts.map +1 -0
  289. package/dist/voice-handlers.js +570 -0
  290. package/dist/voice-realtime-grounded-tool.d.ts +31 -0
  291. package/dist/voice-realtime-grounded-tool.d.ts.map +1 -0
  292. package/dist/voice-realtime-grounded-tool.js +322 -0
  293. package/dist/voice-realtime.d.ts +69 -0
  294. package/dist/voice-realtime.d.ts.map +1 -0
  295. package/dist/voice-realtime.js +787 -0
  296. package/dist/workspace-state-handlers.d.ts +5 -0
  297. package/dist/workspace-state-handlers.d.ts.map +1 -0
  298. package/dist/workspace-state-handlers.js +106 -0
  299. package/package.json +20 -19
  300. package/dist/grounded-handoff.d.ts +0 -4
  301. package/dist/grounded-handoff.d.ts.map +0 -1
  302. package/dist/grounded-handoff.js +0 -445
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutex.d.ts","sourceRoot":"","sources":["../../src/task-workspace/mutex.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,sBAAsB;IAKrC,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CAC7F;AAMD,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEzE;AAeD,wBAAgB,4BAA4B,IAAI,sBAAsB,CAoCrE"}
@@ -0,0 +1,82 @@
1
+ // In-process keyed async mutex for managed task-workspace mutations (Issue #449, Epic #443, ADR-0093 D1).
2
+ //
3
+ // Every mutating workspace flow is optimistic check-then-write: it reads the instance, evaluates the
4
+ // advisory lock, `await`s a Git-adapter spawn, then `store.upsert`s. The window between the live check
5
+ // and the persisted write is a TOCTOU gap — two concurrent `provision()` calls for the same (repo, task)
6
+ // both pass the gates and both race `git worktree add`. This registry closes that window by SERIALIZING
7
+ // the whole critical section within the single server process: `runExclusive(keys, fn)` queues a flow
8
+ // behind any in-flight holder of an overlapping key and runs it only once it has exclusive access.
9
+ //
10
+ // It composes with — never replaces — the persisted advisory `WorkspaceLock`. The mutex grants TURN
11
+ // ORDER (same-process serialization); the advisory lock grants OWNERSHIP (across-restart / across-actor).
12
+ // The existing cross-actor `LOCK_CONTENTION` rejection therefore stays INSIDE the wrapped section: two
13
+ // requests from the same process queue and run one at a time, while a request from a different actor
14
+ // still hits the advisory check after the queue drains and is still rejected. The mutex is pure
15
+ // in-process JavaScript: no spawn, no filesystem, no new adapter verb, no allowlist entry (SC3). On a
16
+ // process crash it simply vanishes — nothing it protected survives either, and the durable record is the
17
+ // persisted advisory lock + visible lifecycle state (#447 reconciliation/repair resolve any stale lock).
18
+ // ─── key scheme ────────────────────────────────────────────────────────────────────────────────────
19
+ // Three contended resources, three key prefixes. The canonical acquisition order is the tier order
20
+ // below (active → ws → prov), then lexicographic, so a multi-key flow never deadlocks against another.
21
+ export function activePointerKey(repositoryId) {
22
+ return `active:${repositoryId}`;
23
+ }
24
+ export function workspaceKey(workspaceId) {
25
+ return `ws:${workspaceId}`;
26
+ }
27
+ export function provisionKey(repositoryId, taskId) {
28
+ return `prov:${repositoryId}:${taskId}`;
29
+ }
30
+ function keyTier(key) {
31
+ if (key.startsWith("active:"))
32
+ return 0;
33
+ if (key.startsWith("ws:"))
34
+ return 1;
35
+ if (key.startsWith("prov:"))
36
+ return 2;
37
+ return 3;
38
+ }
39
+ function compareKeys(a, b) {
40
+ const tierDelta = keyTier(a) - keyTier(b);
41
+ if (tierDelta !== 0)
42
+ return tierDelta;
43
+ return a < b ? -1 : a > b ? 1 : 0;
44
+ }
45
+ export function createWorkspaceMutexRegistry() {
46
+ // One tail promise per held key. The tail resolves when the current holder of that key releases. The
47
+ // map only holds keys with an active or queued holder; a key's entry is deleted once its chain drains,
48
+ // so the map can never grow unbounded.
49
+ const tails = new Map();
50
+ const runExclusive = (keys, fn) => {
51
+ const ordered = [...new Set(keys)].sort(compareKeys);
52
+ // Synchronous critical region: capture the predecessor tails of every requested key and install our
53
+ // release gate as their new tail in ONE uninterrupted step (no `await` here), so two concurrent
54
+ // callers cannot interleave their capture/install and both think they acquired the same key. The
55
+ // Promise executor runs synchronously, so `releaseGate` is assigned before any use (definite-assign).
56
+ let releaseGate;
57
+ const gate = new Promise((resolve) => {
58
+ releaseGate = resolve;
59
+ });
60
+ const predecessors = [];
61
+ for (const key of ordered) {
62
+ const prev = tails.get(key);
63
+ if (prev !== undefined)
64
+ predecessors.push(prev);
65
+ tails.set(key, gate);
66
+ }
67
+ return (async () => {
68
+ try {
69
+ await Promise.all(predecessors);
70
+ return await fn();
71
+ }
72
+ finally {
73
+ releaseGate();
74
+ for (const key of ordered) {
75
+ if (tails.get(key) === gate)
76
+ tails.delete(key);
77
+ }
78
+ }
79
+ })();
80
+ };
81
+ return { runExclusive };
82
+ }
@@ -0,0 +1,15 @@
1
+ export declare const MANAGED_ROOT_MARKER_FILENAME = ".keiko-managed-root";
2
+ export declare function deriveRepositoryId(repositoryRoot: string): string;
3
+ export declare function deriveWorkspaceId(input: {
4
+ readonly repositoryId: string;
5
+ readonly taskId: string;
6
+ }): string;
7
+ export declare function deriveTaskBranchName(input: {
8
+ readonly taskId: string;
9
+ }): string;
10
+ export declare function deriveManagedWorktreePath(input: {
11
+ readonly managedRoot: string;
12
+ readonly repositoryId: string;
13
+ readonly workspaceId: string;
14
+ }): string;
15
+ //# sourceMappingURL=naming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"naming.d.ts","sourceRoot":"","sources":["../../src/task-workspace/naming.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,4BAA4B,wBAAwB,CAAC;AAQlE,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAEjE;AAID,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GAAG,MAAM,CAKT;AAmBD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAG/E;AAKD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE;IAC/C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,GAAG,MAAM,CAET"}
Binary file
@@ -0,0 +1,3 @@
1
+ import type { WorkspaceProvisioningService, WorkspaceProvisioningServiceDeps } from "./types.js";
2
+ export declare function createWorkspaceProvisioningService(deps: WorkspaceProvisioningServiceDeps): WorkspaceProvisioningService;
3
+ //# sourceMappingURL=provisioning.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provisioning.d.ts","sourceRoot":"","sources":["../../src/task-workspace/provisioning.ts"],"names":[],"mappings":"AAuDA,OAAO,KAAK,EAGV,4BAA4B,EAC5B,gCAAgC,EAGjC,MAAM,YAAY,CAAC;AA2qBpB,wBAAgB,kCAAkC,CAChD,IAAI,EAAE,gCAAgC,GACrC,4BAA4B,CAU9B"}
@@ -0,0 +1,528 @@
1
+ // Managed task-workspace provisioning + activation service (Issue #445, Epic #443).
2
+ //
3
+ // This is the missing `git worktree` lifecycle AUTHORITY: it creates a dedicated task branch and a
4
+ // managed worktree from an approved base branch, walks the #444 lifecycle (provisioning → active),
5
+ // persists the durable WorkspaceInstance, and yields the WorkspaceBinding that Studio/editor/runtime/
6
+ // Git-Delivery surfaces bind to (#446 consumes it). It REUSES, never duplicates: Git mutation runs
7
+ // through the narrow keiko-tools worktree adapter (the single governed runCommand spawn boundary),
8
+ // path containment is delegated to @oscharko-dev/keiko-workspace, and branch/commit/publish/PR/merge
9
+ // stay owned by #470. No generic shell, no generic Git runner (SC1).
10
+ //
11
+ // Failure handling is deterministic and leaves a CLASSIFIED, visible state (SC4): pre-write rejections
12
+ // (invalid base, conflict, unsafe path, existing-unmanaged, lock contention) throw BEFORE any worktree
13
+ // or instance row is created; a failure DURING the worktree mutation transitions the persisted
14
+ // instance to `failed`/`recovery-required`, rolls the partial worktree back, and emits the matching
15
+ // content-free evidence.
16
+ import { readFileSync, statSync } from "node:fs";
17
+ import { createHash } from "node:crypto";
18
+ import { join } from "node:path";
19
+ import { detectWorkspaceAt } from "@oscharko-dev/keiko-workspace";
20
+ import { isSafeGitRefName } from "@oscharko-dev/keiko-tools/internal/git-mutation";
21
+ import { TASK_WORKSPACE_SCHEMA_VERSION, validateTaskWorkspaceTransition, } from "@oscharko-dev/keiko-contracts";
22
+ import { buildBinding } from "./binding.js";
23
+ import { assertSafeFieldValue, containsUnsafeFieldChars } from "./field-safety.js";
24
+ import { lockIsLive, makeWorkspaceLock, resolveLockTtl } from "./locks.js";
25
+ import { provisionKey, workspaceKey } from "./mutex.js";
26
+ import { deriveManagedWorktreePath, deriveRepositoryId, deriveTaskBranchName, deriveWorkspaceId, } from "./naming.js";
27
+ import { assertManagedRootOwned, assertManagedTargetContained, ensureManagedWorktreeParent, managedTargetExists, } from "./managed-root.js";
28
+ import { TaskWorkspaceError } from "./errors.js";
29
+ import { appendWorkspaceLifecycleEvidence, buildWorkspaceEvent, WORKSPACE_LIFECYCLE_EVIDENCE_KIND, } from "./evidence.js";
30
+ const MAX_FIELD_LENGTH = 512;
31
+ const RESUMABLE_STATES = [
32
+ "active",
33
+ "paused",
34
+ "handoff-ready",
35
+ ];
36
+ const COMPLETABLE_STATES = [
37
+ "provisioning",
38
+ "failed",
39
+ "recovery-required",
40
+ ];
41
+ // ─── pure helpers ────────────────────────────────────────────────────────────────────────────────
42
+ function isBoundedNonEmpty(value) {
43
+ return typeof value === "string" && value.length > 0 && value.length <= MAX_FIELD_LENGTH;
44
+ }
45
+ function isoFrom(nowMs) {
46
+ return new Date(nowMs).toISOString();
47
+ }
48
+ function gitdirIdentity(worktreePath) {
49
+ let raw;
50
+ try {
51
+ const dotGit = join(worktreePath, ".git");
52
+ if (statSync(dotGit).isDirectory()) {
53
+ throw new Error("`.git` is a directory, not a linked-worktree pointer");
54
+ }
55
+ raw = readFileSync(dotGit, "utf8");
56
+ }
57
+ catch {
58
+ throw new TaskWorkspaceError("POINTER_DRIFT", "managed worktree git pointer is missing");
59
+ }
60
+ const match = /^gitdir:\s*(.+)\s*$/mu.exec(raw);
61
+ if (match?.[1] === undefined || match[1].length === 0) {
62
+ throw new TaskWorkspaceError("POINTER_DRIFT", "managed worktree git pointer is malformed");
63
+ }
64
+ // Content-free, stable identity of the worktree's admin dir. The raw target stays in-process.
65
+ return createHash("sha256").update(match[1].trim(), "utf8").digest("hex").slice(0, 32);
66
+ }
67
+ function assertPersistedManagedPath(ctx, instance) {
68
+ assertManagedTargetContained(ctx.deps.managedRoot, instance.managedWorktreePath);
69
+ const expected = deriveManagedWorktreePath({
70
+ managedRoot: ctx.deps.managedRoot,
71
+ repositoryId: instance.repositoryId,
72
+ workspaceId: instance.workspaceId,
73
+ });
74
+ if (instance.managedWorktreePath !== expected) {
75
+ throw new TaskWorkspaceError("POINTER_DRIFT", "persisted managed worktree path does not match its workspace identity");
76
+ }
77
+ }
78
+ // ─── lock helpers ──────────────────────────────────────────────────────────────────────────────
79
+ // Lock liveness + the advisory-lock builder are the consolidated #449 helpers (locks.ts); this thin
80
+ // wrapper binds the provisioning ctx's TTL so the call sites stay terse.
81
+ function provisioningLockLive(ctx, lock, nowMs) {
82
+ return lockIsLive(lock, nowMs, ctx.lockTtlMs);
83
+ }
84
+ function makeLock(ctx, owner, reason, nowMs) {
85
+ return makeWorkspaceLock({ newId: ctx.deps.newId, owner, reason, nowMs, ttlMs: ctx.lockTtlMs });
86
+ }
87
+ // ─── evidence ───────────────────────────────────────────────────────────────────────────────────
88
+ function emit(ctx, input) {
89
+ const event = buildWorkspaceEvent({
90
+ eventId: ctx.deps.newId(),
91
+ workspaceId: input.workspaceId,
92
+ taskId: input.taskId,
93
+ type: input.type,
94
+ at: isoFrom(input.nowMs),
95
+ correlationId: input.workspaceId,
96
+ ...(input.fromState !== undefined ? { fromState: input.fromState } : {}),
97
+ ...(input.toState !== undefined ? { toState: input.toState } : {}),
98
+ ...(input.lockId !== undefined ? { lockId: input.lockId } : {}),
99
+ });
100
+ appendWorkspaceLifecycleEvidence(ctx.deps.evidenceStore, {
101
+ kind: WORKSPACE_LIFECYCLE_EVIDENCE_KIND,
102
+ schemaVersion: TASK_WORKSPACE_SCHEMA_VERSION,
103
+ recordedAt: input.nowMs,
104
+ operation: input.operation,
105
+ outcome: input.outcome,
106
+ attempt: 1,
107
+ durationMs: 0,
108
+ worktreeCount: 0,
109
+ event,
110
+ }, ctx.deps.redactString);
111
+ }
112
+ function emitOutcomeForCode(ctx, operation, outcome, workspaceId, taskId, nowMs) {
113
+ emit(ctx, { operation, outcome, type: "transition-rejected", workspaceId, taskId, nowMs });
114
+ }
115
+ // ─── repository resolution ─────────────────────────────────────────────────────────────────────
116
+ function validateProvisionRequest(request) {
117
+ const reasons = [];
118
+ if (!isBoundedNonEmpty(request.repositoryRequestPath))
119
+ reasons.push("repository path required");
120
+ if (!isBoundedNonEmpty(request.taskId))
121
+ reasons.push("taskId required");
122
+ else if (containsUnsafeFieldChars(request.taskId))
123
+ reasons.push("taskId contains forbidden characters");
124
+ if (!isBoundedNonEmpty(request.requestedBy))
125
+ reasons.push("requestedBy required");
126
+ else if (containsUnsafeFieldChars(request.requestedBy)) {
127
+ reasons.push("requestedBy contains forbidden characters");
128
+ }
129
+ if (!isBoundedNonEmpty(request.baseBranch) || !isSafeGitRefName(request.baseBranch)) {
130
+ reasons.push("baseBranch must be a safe git ref name");
131
+ }
132
+ if (reasons.length > 0) {
133
+ throw new TaskWorkspaceError("INVALID_REQUEST", "invalid provision request", reasons);
134
+ }
135
+ }
136
+ async function resolveRepositoryContext(ctx, request) {
137
+ const requestWorkspace = detectWorkspaceAt(request.repositoryRequestPath);
138
+ const repositoryRoot = await ctx.deps.createAdapter(requestWorkspace).resolveRepositoryRoot();
139
+ if (repositoryRoot === undefined) {
140
+ throw new TaskWorkspaceError("MISSING_REPOSITORY", "path is not inside a git repository");
141
+ }
142
+ const repositoryId = deriveRepositoryId(repositoryRoot);
143
+ const workspaceId = deriveWorkspaceId({ repositoryId, taskId: request.taskId });
144
+ const repoWorkspace = repositoryRoot === requestWorkspace.root ? requestWorkspace : detectWorkspaceAt(repositoryRoot);
145
+ return {
146
+ repositoryRoot,
147
+ repositoryId,
148
+ workspaceId,
149
+ taskBranch: deriveTaskBranchName({ taskId: request.taskId }),
150
+ worktreePath: deriveManagedWorktreePath({
151
+ managedRoot: ctx.deps.managedRoot,
152
+ repositoryId,
153
+ workspaceId,
154
+ }),
155
+ adapter: ctx.deps.createAdapter(repoWorkspace),
156
+ };
157
+ }
158
+ // ─── instance shaping ──────────────────────────────────────────────────────────────────────────
159
+ function freshInstance(ctx, request, existing, lock, nowMs) {
160
+ const iso = isoFrom(nowMs);
161
+ return {
162
+ schemaVersion: TASK_WORKSPACE_SCHEMA_VERSION,
163
+ workspaceId: ctx.workspaceId,
164
+ taskId: request.taskId,
165
+ repositoryId: ctx.repositoryId,
166
+ repositoryRoot: ctx.repositoryRoot,
167
+ baseBranch: request.baseBranch,
168
+ taskBranch: ctx.taskBranch,
169
+ managedWorktreePath: ctx.worktreePath,
170
+ gitdirIdentity: existing?.gitdirIdentity ?? ctx.workspaceId,
171
+ lifecycleState: "provisioning",
172
+ health: "unknown",
173
+ lock,
174
+ createdAt: existing?.createdAt ?? iso,
175
+ updatedAt: iso,
176
+ driftMarkers: [],
177
+ recoveryHints: [],
178
+ auditCorrelationId: existing?.auditCorrelationId ?? ctx.workspaceId,
179
+ };
180
+ }
181
+ function finalizeActive(ctx, provisioning, identity, nowMs) {
182
+ const transition = validateTaskWorkspaceTransition({
183
+ from: "provisioning",
184
+ to: "active",
185
+ context: {
186
+ lockHeldByActor: true,
187
+ pathContained: true,
188
+ worktreeClean: true,
189
+ branchReady: true,
190
+ providerReady: true,
191
+ operatorApproved: false,
192
+ },
193
+ });
194
+ if (!transition.ok) {
195
+ throw new TaskWorkspaceError("PROVISIONING_FAILED", "provisioning to active transition rejected", transition.reasons);
196
+ }
197
+ const iso = isoFrom(nowMs);
198
+ return ctx.deps.store.upsert({
199
+ ...provisioning,
200
+ lifecycleState: "active",
201
+ health: "healthy",
202
+ lock: null,
203
+ gitdirIdentity: identity,
204
+ lastVerifiedAt: iso,
205
+ updatedAt: iso,
206
+ driftMarkers: [],
207
+ recoveryHints: [],
208
+ });
209
+ }
210
+ // ─── pre-write gates ────────────────────────────────────────────────────────────────────────────
211
+ function assertNotLocked(ctx, existing, request, nowMs) {
212
+ if (existing !== undefined &&
213
+ provisioningLockLive(ctx, existing.lock, nowMs) &&
214
+ existing.lock?.owner !== request.requestedBy) {
215
+ throw new TaskWorkspaceError("LOCK_CONTENTION", "workspace is locked by another actor");
216
+ }
217
+ }
218
+ async function assertNoTargetOrBranchConflict(ctx, existing) {
219
+ const ours = existing?.managedWorktreePath === ctx.worktreePath;
220
+ if (managedTargetExists(ctx.worktreePath)) {
221
+ if (!ours) {
222
+ throw new TaskWorkspaceError("EXISTING_UNMANAGED_PATH", "target worktree path exists and is not Keiko-managed");
223
+ }
224
+ return;
225
+ }
226
+ if (!ours && (await ctx.adapter.localBranchExists(ctx.taskBranch))) {
227
+ throw new TaskWorkspaceError("BRANCH_CONFLICT", "task branch already exists");
228
+ }
229
+ }
230
+ // Detects the pre-write rejections. Throws WITHOUT persisting an instance — nothing was created, so
231
+ // there is no partial state to classify.
232
+ async function assertProvisionable(ctx, repo, request, existing, nowMs) {
233
+ assertNotLocked(ctx, existing, request, nowMs);
234
+ if (!(await repo.adapter.refResolves(request.baseBranch))) {
235
+ throw new TaskWorkspaceError("INVALID_BASE_BRANCH", "base branch does not resolve");
236
+ }
237
+ await assertNoTargetOrBranchConflict(repo, existing);
238
+ }
239
+ // ─── worktree materialization ────────────────────────────────────────────────────────────────────
240
+ async function materializeWorktree(repo, request) {
241
+ if (managedTargetExists(repo.worktreePath)) {
242
+ return false; // our managed worktree already present — resume-complete without re-adding
243
+ }
244
+ ensureManagedWorktreeParent(repo.worktreePath);
245
+ const branchExists = await repo.adapter.localBranchExists(repo.taskBranch);
246
+ const result = branchExists
247
+ ? await repo.adapter.addWorktreeForExistingBranch({
248
+ worktreePath: repo.worktreePath,
249
+ branch: repo.taskBranch,
250
+ })
251
+ : await repo.adapter.addWorktree({
252
+ worktreePath: repo.worktreePath,
253
+ taskBranch: repo.taskBranch,
254
+ baseRef: request.baseBranch,
255
+ });
256
+ if (!result.ok) {
257
+ throw new TaskWorkspaceError("PROVISIONING_FAILED", "git worktree add failed");
258
+ }
259
+ return true;
260
+ }
261
+ // Persists the partial-failure state visibly (SC4): the instance moves to `failed`/`recovery-required`
262
+ // with the lock released so a retry is unblocked, and a best-effort rollback removes the half-created
263
+ // worktree.
264
+ async function failProvisioning(repo, ctx, provisioning, error, nowMs) {
265
+ const target = error.outcome === "retry-required" ? "recovery-required" : "failed";
266
+ try {
267
+ await repo.adapter.removeWorktree({ worktreePath: repo.worktreePath, force: true });
268
+ await repo.adapter.pruneWorktrees();
269
+ }
270
+ catch {
271
+ // Rollback is best-effort; the visible state plus drift markers drive #447 repair.
272
+ }
273
+ ctx.deps.store.upsert({
274
+ ...provisioning,
275
+ lifecycleState: target,
276
+ health: error.outcome === "retry-required" ? "drifted" : "degraded",
277
+ lock: null,
278
+ updatedAt: isoFrom(nowMs),
279
+ driftMarkers: error.code === "POINTER_DRIFT" ? ["pointer-stale"] : [],
280
+ });
281
+ emit(ctx, {
282
+ operation: "provision",
283
+ outcome: error.outcome,
284
+ type: error.code === "POINTER_DRIFT" ? "drift-detected" : "transition-rejected",
285
+ workspaceId: provisioning.workspaceId,
286
+ taskId: provisioning.taskId,
287
+ nowMs,
288
+ fromState: "provisioning",
289
+ toState: target,
290
+ });
291
+ throw error;
292
+ }
293
+ // ─── resume / drift ──────────────────────────────────────────────────────────────────────────────
294
+ function resumeExisting(ctx, repo, existing, nowMs) {
295
+ assertPersistedManagedPath(ctx, existing);
296
+ const identity = gitdirIdentity(repo.worktreePath);
297
+ const refreshed = ctx.deps.store.upsert({
298
+ ...existing,
299
+ health: "healthy",
300
+ gitdirIdentity: identity,
301
+ lastVerifiedAt: isoFrom(nowMs),
302
+ updatedAt: isoFrom(nowMs),
303
+ });
304
+ emit(ctx, {
305
+ operation: "provision",
306
+ outcome: "resumed",
307
+ type: "activated",
308
+ workspaceId: refreshed.workspaceId,
309
+ taskId: refreshed.taskId,
310
+ nowMs,
311
+ toState: refreshed.lifecycleState,
312
+ });
313
+ return { instance: refreshed, binding: buildBinding(refreshed), created: false };
314
+ }
315
+ // An active/paused workspace whose managed worktree has vanished is drift, not a re-provision: the
316
+ // contract forbids re-entering `provisioning`, so it transitions to `recovery-required` (a legal move)
317
+ // and #447 owns the repair.
318
+ function flagResumableDrift(ctx, existing, nowMs) {
319
+ const drifted = ctx.deps.store.upsert({
320
+ ...existing,
321
+ lifecycleState: "recovery-required",
322
+ health: "missing",
323
+ lock: null,
324
+ updatedAt: isoFrom(nowMs),
325
+ driftMarkers: ["worktree-missing"],
326
+ });
327
+ emit(ctx, {
328
+ operation: "provision",
329
+ outcome: "retry-required",
330
+ type: "drift-detected",
331
+ workspaceId: drifted.workspaceId,
332
+ taskId: drifted.taskId,
333
+ nowMs,
334
+ fromState: existing.lifecycleState,
335
+ toState: "recovery-required",
336
+ });
337
+ throw new TaskWorkspaceError("POINTER_DRIFT", "managed worktree is missing");
338
+ }
339
+ function reuseExistingOrUndefined(ctx, repo, existing, nowMs) {
340
+ if (existing === undefined)
341
+ return undefined;
342
+ if (RESUMABLE_STATES.includes(existing.lifecycleState)) {
343
+ return managedTargetExists(repo.worktreePath)
344
+ ? resumeExisting(ctx, repo, existing, nowMs)
345
+ : flagResumableDrift(ctx, existing, nowMs);
346
+ }
347
+ if (!COMPLETABLE_STATES.includes(existing.lifecycleState)) {
348
+ // Terminal state (archived/merged/abandoned/cleanup-pending): idempotent no-op, return as-is.
349
+ assertPersistedManagedPath(ctx, existing);
350
+ return { instance: existing, binding: buildBinding(existing), created: false };
351
+ }
352
+ return undefined; // COMPLETABLE: fall through to (re)provision/complete.
353
+ }
354
+ // ─── provision orchestration ─────────────────────────────────────────────────────────────────────
355
+ async function runWorktreeMutation(ctx, repo, request, provisioning) {
356
+ let created;
357
+ let identity;
358
+ try {
359
+ created = await materializeWorktree(repo, request);
360
+ identity = gitdirIdentity(repo.worktreePath);
361
+ }
362
+ catch (error) {
363
+ const failure = error instanceof TaskWorkspaceError
364
+ ? error
365
+ : new TaskWorkspaceError("PROVISIONING_FAILED", "unexpected provisioning failure");
366
+ return failProvisioning(repo, ctx, provisioning, failure, ctx.deps.now());
367
+ }
368
+ const active = finalizeActive(ctx, provisioning, identity, ctx.deps.now());
369
+ emit(ctx, {
370
+ operation: "provision",
371
+ outcome: "provisioned",
372
+ type: "provisioned",
373
+ workspaceId: active.workspaceId,
374
+ taskId: active.taskId,
375
+ nowMs: ctx.deps.now(),
376
+ fromState: "provisioning",
377
+ toState: "active",
378
+ });
379
+ return { instance: active, binding: buildBinding(active), created };
380
+ }
381
+ // The gated provisioning critical section. Runs under the `prov:<repositoryId>:<taskId>` mutex key
382
+ // (#449, ADR-0093 D1) so two concurrent provisions of the SAME (repo, task) serialize instead of both
383
+ // passing the check-then-write gates and racing `git worktree add`. The advisory cross-actor
384
+ // LOCK_CONTENTION check (assertProvisionable → assertNotLocked) stays INSIDE this section, preserving the
385
+ // across-actor rejection while the mutex only serializes same-process callers.
386
+ async function provisionLocked(ctx, request, repo) {
387
+ assertManagedRootOwned(ctx.deps.managedRoot);
388
+ assertManagedTargetContained(ctx.deps.managedRoot, repo.worktreePath);
389
+ const nowMs = ctx.deps.now();
390
+ const existing = ctx.deps.store.findByRepositoryAndTask(repo.repositoryId, request.taskId);
391
+ const reused = reuseExistingOrUndefined(ctx, repo, existing, nowMs);
392
+ if (reused !== undefined)
393
+ return reused;
394
+ try {
395
+ await assertProvisionable(ctx, repo, request, existing, nowMs);
396
+ }
397
+ catch (error) {
398
+ if (error instanceof TaskWorkspaceError) {
399
+ emitOutcomeForCode(ctx, "provision", error.outcome, repo.workspaceId, request.taskId, nowMs);
400
+ }
401
+ throw error;
402
+ }
403
+ const lock = makeLock(ctx, request.requestedBy, "provisioning", nowMs);
404
+ const provisioning = ctx.deps.store.upsert(freshInstance(repo, request, existing, lock, nowMs));
405
+ return runWorktreeMutation(ctx, repo, request, provisioning);
406
+ }
407
+ async function provisionImpl(ctx, request) {
408
+ validateProvisionRequest(request);
409
+ // Resolve the repository identity (read-only git-root resolution) BEFORE acquiring the key — the key
410
+ // is derived from (repositoryId, taskId) and serialization must cover only the mutating gated section.
411
+ const repo = await resolveRepositoryContext(ctx, request);
412
+ return ctx.deps.mutex.runExclusive([provisionKey(repo.repositoryId, request.taskId)], () => provisionLocked(ctx, request, repo));
413
+ }
414
+ // ─── activate orchestration ──────────────────────────────────────────────────────────────────────
415
+ function activateActiveOrResume(instance) {
416
+ if (instance.lifecycleState === "active") {
417
+ return { next: instance, type: "activated" };
418
+ }
419
+ const transition = validateTaskWorkspaceTransition({
420
+ from: instance.lifecycleState,
421
+ to: "active",
422
+ context: {
423
+ lockHeldByActor: true,
424
+ pathContained: true,
425
+ worktreeClean: true,
426
+ branchReady: true,
427
+ providerReady: true,
428
+ operatorApproved: false,
429
+ },
430
+ });
431
+ if (!transition.ok) {
432
+ throw new TaskWorkspaceError("ILLEGAL_TRANSITION", "cannot resume workspace", transition.reasons);
433
+ }
434
+ return { next: { ...instance, lifecycleState: "active" }, type: "resumed" };
435
+ }
436
+ function assertActivatable(ctx, instance, request, nowMs) {
437
+ if (isBoundedNonEmpty(request.taskId) && instance.taskId !== request.taskId) {
438
+ throw new TaskWorkspaceError("INVALID_REQUEST", "taskId does not match workspace");
439
+ }
440
+ if (request.expectedLifecycleState !== undefined &&
441
+ request.expectedLifecycleState !== instance.lifecycleState) {
442
+ throw new TaskWorkspaceError("LOCK_CONTENTION", "workspace state changed; retry");
443
+ }
444
+ if (provisioningLockLive(ctx, instance.lock, nowMs) &&
445
+ instance.lock?.owner !== request.requestedBy) {
446
+ throw new TaskWorkspaceError("LOCK_CONTENTION", "workspace is locked by another actor");
447
+ }
448
+ if (!RESUMABLE_STATES.includes(instance.lifecycleState)) {
449
+ throw new TaskWorkspaceError("ILLEGAL_TRANSITION", `cannot activate from ${instance.lifecycleState}`);
450
+ }
451
+ }
452
+ function flagActivateDrift(ctx, instance, nowMs) {
453
+ const drifted = ctx.deps.store.upsert({
454
+ ...instance,
455
+ lifecycleState: "recovery-required",
456
+ health: "missing",
457
+ lock: null,
458
+ updatedAt: isoFrom(nowMs),
459
+ driftMarkers: ["worktree-missing"],
460
+ });
461
+ emit(ctx, {
462
+ operation: "activate",
463
+ outcome: "retry-required",
464
+ type: "drift-detected",
465
+ workspaceId: drifted.workspaceId,
466
+ taskId: drifted.taskId,
467
+ nowMs,
468
+ fromState: instance.lifecycleState,
469
+ toState: "recovery-required",
470
+ });
471
+ throw new TaskWorkspaceError("POINTER_DRIFT", "managed worktree is missing");
472
+ }
473
+ // The gated activation critical section, run under the `ws:<workspaceId>` mutex key (#449, ADR-0093 D1)
474
+ // so a concurrent activate/pause/repair/cleanup of the same workspace serializes. The advisory
475
+ // cross-actor LOCK_CONTENTION check (assertActivatable) stays INSIDE.
476
+ function activateLocked(ctx, request) {
477
+ const instance = ctx.deps.store.getById(request.workspaceId);
478
+ if (instance === undefined) {
479
+ throw new TaskWorkspaceError("WORKSPACE_NOT_FOUND", "workspace not found");
480
+ }
481
+ const nowMs = ctx.deps.now();
482
+ assertActivatable(ctx, instance, request, nowMs);
483
+ assertPersistedManagedPath(ctx, instance);
484
+ if (!managedTargetExists(instance.managedWorktreePath)) {
485
+ flagActivateDrift(ctx, instance, nowMs);
486
+ }
487
+ const { next, type } = activateActiveOrResume(instance);
488
+ const lock = request.acquireLock ? makeLock(ctx, request.requestedBy, "activation", nowMs) : null;
489
+ const persisted = ctx.deps.store.upsert({
490
+ ...next,
491
+ health: "healthy",
492
+ lock,
493
+ lastVerifiedAt: isoFrom(nowMs),
494
+ updatedAt: isoFrom(nowMs),
495
+ });
496
+ emit(ctx, {
497
+ operation: "activate",
498
+ outcome: type === "resumed" ? "resumed" : "activated",
499
+ type,
500
+ workspaceId: persisted.workspaceId,
501
+ taskId: persisted.taskId,
502
+ nowMs,
503
+ ...(type === "resumed" ? { fromState: instance.lifecycleState } : {}),
504
+ toState: "active",
505
+ ...(lock !== null ? { lockId: lock.lockId } : {}),
506
+ });
507
+ return { instance: persisted, binding: buildBinding(persisted) };
508
+ }
509
+ function activateImpl(ctx, request) {
510
+ if (!isBoundedNonEmpty(request.workspaceId) || !isBoundedNonEmpty(request.requestedBy)) {
511
+ throw new TaskWorkspaceError("INVALID_REQUEST", "invalid activation request");
512
+ }
513
+ // requestedBy becomes the activation advisory-lock owner; a provided taskId is the cross-check key.
514
+ // Both flow into operator-visible state, so reject control/zero-width/bidi code points here too.
515
+ assertSafeFieldValue(request.requestedBy, "requestedBy");
516
+ if (isBoundedNonEmpty(request.taskId))
517
+ assertSafeFieldValue(request.taskId, "taskId");
518
+ return ctx.deps.mutex.runExclusive([workspaceKey(request.workspaceId)], () => activateLocked(ctx, request));
519
+ }
520
+ // ─── factory ─────────────────────────────────────────────────────────────────────────────────────
521
+ export function createWorkspaceProvisioningService(deps) {
522
+ const ctx = { deps, lockTtlMs: resolveLockTtl(deps.lockTtlMs) };
523
+ return {
524
+ provision: (request) => provisionImpl(ctx, request),
525
+ activate: (request) => activateImpl(ctx, request),
526
+ getInstance: (workspaceId) => deps.store.getById(workspaceId),
527
+ };
528
+ }
@@ -0,0 +1,15 @@
1
+ import type { GitWorktreeAdapter, WorktreeListEntry } from "@oscharko-dev/keiko-tools/internal/git-mutation";
2
+ import { type WorkspaceInstance, type WorkspaceReconciliationFacts, type WorkspaceReconciliationOutcome } from "@oscharko-dev/keiko-contracts";
3
+ import type { WorkspaceReconciliationService, WorkspaceReconciliationServiceDeps } from "./types.js";
4
+ export interface ReconcileInstanceResult {
5
+ readonly instance: WorkspaceInstance;
6
+ readonly outcome: WorkspaceReconciliationOutcome;
7
+ }
8
+ export interface FactsAndHead {
9
+ readonly facts: WorkspaceReconciliationFacts;
10
+ readonly observedHead: string | undefined;
11
+ }
12
+ export declare function gatherInstanceReconciliationFacts(deps: WorkspaceReconciliationServiceDeps, adapter: GitWorktreeAdapter, worktrees: readonly WorktreeListEntry[], instance: WorkspaceInstance, nowMs: number, actor?: string): Promise<FactsAndHead>;
13
+ export declare function reconcileSingleInstance(deps: WorkspaceReconciliationServiceDeps, instance: WorkspaceInstance, nowMs: number, actor?: string): Promise<ReconcileInstanceResult>;
14
+ export declare function createWorkspaceReconciliationService(deps: WorkspaceReconciliationServiceDeps): WorkspaceReconciliationService;
15
+ //# sourceMappingURL=reconciliation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconciliation.d.ts","sourceRoot":"","sources":["../../src/task-workspace/reconciliation.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAUL,KAAK,iBAAiB,EAEtB,KAAK,4BAA4B,EACjC,KAAK,8BAA8B,EAEpC,MAAM,+BAA+B,CAAC;AAQvC,OAAO,KAAK,EACV,8BAA8B,EAC9B,kCAAkC,EACnC,MAAM,YAAY,CAAC;AAQpB,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,8BAA8B,CAAC;CAClD;AAwDD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,4BAA4B,CAAC;IAC7C,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3C;AAmKD,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,kCAAkC,EACxC,OAAO,EAAE,kBAAkB,EAC3B,SAAS,EAAE,SAAS,iBAAiB,EAAE,EACvC,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,MAAM,EACb,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,YAAY,CAAC,CAGvB;AAID,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,kCAAkC,EACxC,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,MAAM,EACb,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,uBAAuB,CAAC,CAalC;AA8ED,wBAAgB,oCAAoC,CAClD,IAAI,EAAE,kCAAkC,GACvC,8BAA8B,CAQhC"}