@software-dlc/cli 0.157.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 (701) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +105 -0
  3. package/README.md +31 -0
  4. package/dist/checkout.d.ts +43 -0
  5. package/dist/checkout.d.ts.map +1 -0
  6. package/dist/checkout.js +92 -0
  7. package/dist/checkout.js.map +1 -0
  8. package/dist/cli-arguments.d.ts +13 -0
  9. package/dist/cli-arguments.d.ts.map +1 -0
  10. package/dist/cli-arguments.js +67 -0
  11. package/dist/cli-arguments.js.map +1 -0
  12. package/dist/cli-branch-history.d.ts +48 -0
  13. package/dist/cli-branch-history.d.ts.map +1 -0
  14. package/dist/cli-branch-history.js +319 -0
  15. package/dist/cli-branch-history.js.map +1 -0
  16. package/dist/cli-branches.d.ts +22 -0
  17. package/dist/cli-branches.d.ts.map +1 -0
  18. package/dist/cli-branches.js +59 -0
  19. package/dist/cli-branches.js.map +1 -0
  20. package/dist/cli-deploy.d.ts +14 -0
  21. package/dist/cli-deploy.d.ts.map +1 -0
  22. package/dist/cli-deploy.js +164 -0
  23. package/dist/cli-deploy.js.map +1 -0
  24. package/dist/cli-health.d.ts +3 -0
  25. package/dist/cli-health.d.ts.map +1 -0
  26. package/dist/cli-health.js +73 -0
  27. package/dist/cli-health.js.map +1 -0
  28. package/dist/cli-help.d.ts +21 -0
  29. package/dist/cli-help.d.ts.map +1 -0
  30. package/dist/cli-help.js +176 -0
  31. package/dist/cli-help.js.map +1 -0
  32. package/dist/cli-login.d.ts +76 -0
  33. package/dist/cli-login.d.ts.map +1 -0
  34. package/dist/cli-login.js +634 -0
  35. package/dist/cli-login.js.map +1 -0
  36. package/dist/cli-operation.d.ts +2 -0
  37. package/dist/cli-operation.d.ts.map +1 -0
  38. package/dist/cli-operation.js +66 -0
  39. package/dist/cli-operation.js.map +1 -0
  40. package/dist/cli-org.d.ts +12 -0
  41. package/dist/cli-org.d.ts.map +1 -0
  42. package/dist/cli-org.js +218 -0
  43. package/dist/cli-org.js.map +1 -0
  44. package/dist/cli-rebuild.d.ts +20 -0
  45. package/dist/cli-rebuild.d.ts.map +1 -0
  46. package/dist/cli-rebuild.js +29 -0
  47. package/dist/cli-rebuild.js.map +1 -0
  48. package/dist/cli-reset.d.ts +27 -0
  49. package/dist/cli-reset.d.ts.map +1 -0
  50. package/dist/cli-reset.js +44 -0
  51. package/dist/cli-reset.js.map +1 -0
  52. package/dist/cli-snapshot.d.ts +398 -0
  53. package/dist/cli-snapshot.d.ts.map +1 -0
  54. package/dist/cli-snapshot.js +45 -0
  55. package/dist/cli-snapshot.js.map +1 -0
  56. package/dist/cli-task-context.d.ts +3 -0
  57. package/dist/cli-task-context.d.ts.map +1 -0
  58. package/dist/cli-task-context.js +112 -0
  59. package/dist/cli-task-context.js.map +1 -0
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.d.ts.map +1 -0
  62. package/dist/cli.js +1004 -0
  63. package/dist/cli.js.map +1 -0
  64. package/dist/graph-packages-lock.d.ts +169 -0
  65. package/dist/graph-packages-lock.d.ts.map +1 -0
  66. package/dist/graph-packages-lock.js +503 -0
  67. package/dist/graph-packages-lock.js.map +1 -0
  68. package/dist/index.d.ts +2 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +2 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/local/aggregate-preflight.d.ts +12 -0
  73. package/dist/local/aggregate-preflight.d.ts.map +1 -0
  74. package/dist/local/aggregate-preflight.js +22 -0
  75. package/dist/local/aggregate-preflight.js.map +1 -0
  76. package/dist/local/allow-mcp.d.ts +77 -0
  77. package/dist/local/allow-mcp.d.ts.map +1 -0
  78. package/dist/local/allow-mcp.js +394 -0
  79. package/dist/local/allow-mcp.js.map +1 -0
  80. package/dist/local/api-client.d.ts +15 -0
  81. package/dist/local/api-client.d.ts.map +1 -0
  82. package/dist/local/api-client.js +87 -0
  83. package/dist/local/api-client.js.map +1 -0
  84. package/dist/local/app-contract-readers.d.ts +229 -0
  85. package/dist/local/app-contract-readers.d.ts.map +1 -0
  86. package/dist/local/app-contract-readers.js +450 -0
  87. package/dist/local/app-contract-readers.js.map +1 -0
  88. package/dist/local/app-org-reader.d.ts +21 -0
  89. package/dist/local/app-org-reader.d.ts.map +1 -0
  90. package/dist/local/app-org-reader.js +66 -0
  91. package/dist/local/app-org-reader.js.map +1 -0
  92. package/dist/local/app-shell-readers.d.ts +96 -0
  93. package/dist/local/app-shell-readers.d.ts.map +1 -0
  94. package/dist/local/app-shell-readers.js +381 -0
  95. package/dist/local/app-shell-readers.js.map +1 -0
  96. package/dist/local/app-tasks-read.d.ts +202 -0
  97. package/dist/local/app-tasks-read.d.ts.map +1 -0
  98. package/dist/local/app-tasks-read.js +290 -0
  99. package/dist/local/app-tasks-read.js.map +1 -0
  100. package/dist/local/app-worktrees.d.ts +69 -0
  101. package/dist/local/app-worktrees.d.ts.map +1 -0
  102. package/dist/local/app-worktrees.js +554 -0
  103. package/dist/local/app-worktrees.js.map +1 -0
  104. package/dist/local/base-sync-manifest.d.ts +12 -0
  105. package/dist/local/base-sync-manifest.d.ts.map +1 -0
  106. package/dist/local/base-sync-manifest.js +57 -0
  107. package/dist/local/base-sync-manifest.js.map +1 -0
  108. package/dist/local/blast-radius.d.ts +191 -0
  109. package/dist/local/blast-radius.d.ts.map +1 -0
  110. package/dist/local/blast-radius.js +585 -0
  111. package/dist/local/blast-radius.js.map +1 -0
  112. package/dist/local/canonical.d.ts +2 -0
  113. package/dist/local/canonical.d.ts.map +1 -0
  114. package/dist/local/canonical.js +2 -0
  115. package/dist/local/canonical.js.map +1 -0
  116. package/dist/local/config.d.ts +22 -0
  117. package/dist/local/config.d.ts.map +1 -0
  118. package/dist/local/config.js +57 -0
  119. package/dist/local/config.js.map +1 -0
  120. package/dist/local/document-commands.d.ts +163 -0
  121. package/dist/local/document-commands.d.ts.map +1 -0
  122. package/dist/local/document-commands.js +620 -0
  123. package/dist/local/document-commands.js.map +1 -0
  124. package/dist/local/document-embeds.d.ts +21 -0
  125. package/dist/local/document-embeds.d.ts.map +1 -0
  126. package/dist/local/document-embeds.js +38 -0
  127. package/dist/local/document-embeds.js.map +1 -0
  128. package/dist/local/environment-context.d.ts +39 -0
  129. package/dist/local/environment-context.d.ts.map +1 -0
  130. package/dist/local/environment-context.js +193 -0
  131. package/dist/local/environment-context.js.map +1 -0
  132. package/dist/local/environment-exclude.d.ts +7 -0
  133. package/dist/local/environment-exclude.d.ts.map +1 -0
  134. package/dist/local/environment-exclude.js +82 -0
  135. package/dist/local/environment-exclude.js.map +1 -0
  136. package/dist/local/environment-sidecar.d.ts +126 -0
  137. package/dist/local/environment-sidecar.d.ts.map +1 -0
  138. package/dist/local/environment-sidecar.js +442 -0
  139. package/dist/local/environment-sidecar.js.map +1 -0
  140. package/dist/local/environment-switch.d.ts +22 -0
  141. package/dist/local/environment-switch.d.ts.map +1 -0
  142. package/dist/local/environment-switch.js +385 -0
  143. package/dist/local/environment-switch.js.map +1 -0
  144. package/dist/local/exact-remote-projection.d.ts +17 -0
  145. package/dist/local/exact-remote-projection.d.ts.map +1 -0
  146. package/dist/local/exact-remote-projection.js +53 -0
  147. package/dist/local/exact-remote-projection.js.map +1 -0
  148. package/dist/local/experimental/path-object-store.d.ts +66 -0
  149. package/dist/local/experimental/path-object-store.d.ts.map +1 -0
  150. package/dist/local/experimental/path-object-store.js +135 -0
  151. package/dist/local/experimental/path-object-store.js.map +1 -0
  152. package/dist/local/external-worktree-binding.d.ts +21 -0
  153. package/dist/local/external-worktree-binding.d.ts.map +1 -0
  154. package/dist/local/external-worktree-binding.js +56 -0
  155. package/dist/local/external-worktree-binding.js.map +1 -0
  156. package/dist/local/folder-members.d.ts +34 -0
  157. package/dist/local/folder-members.d.ts.map +1 -0
  158. package/dist/local/folder-members.js +168 -0
  159. package/dist/local/folder-members.js.map +1 -0
  160. package/dist/local/git-ref-authority.d.ts +11 -0
  161. package/dist/local/git-ref-authority.d.ts.map +1 -0
  162. package/dist/local/git-ref-authority.js +76 -0
  163. package/dist/local/git-ref-authority.js.map +1 -0
  164. package/dist/local/git.d.ts +88 -0
  165. package/dist/local/git.d.ts.map +1 -0
  166. package/dist/local/git.js +413 -0
  167. package/dist/local/git.js.map +1 -0
  168. package/dist/local/graph-read.d.ts +168 -0
  169. package/dist/local/graph-read.d.ts.map +1 -0
  170. package/dist/local/graph-read.js +360 -0
  171. package/dist/local/graph-read.js.map +1 -0
  172. package/dist/local/graph-sync.d.ts +39 -0
  173. package/dist/local/graph-sync.d.ts.map +1 -0
  174. package/dist/local/graph-sync.js +186 -0
  175. package/dist/local/graph-sync.js.map +1 -0
  176. package/dist/local/guide-commands.d.ts +79 -0
  177. package/dist/local/guide-commands.d.ts.map +1 -0
  178. package/dist/local/guide-commands.js +247 -0
  179. package/dist/local/guide-commands.js.map +1 -0
  180. package/dist/local/health-files.d.ts +24 -0
  181. package/dist/local/health-files.d.ts.map +1 -0
  182. package/dist/local/health-files.js +133 -0
  183. package/dist/local/health-files.js.map +1 -0
  184. package/dist/local/health-inspection.d.ts +12 -0
  185. package/dist/local/health-inspection.d.ts.map +1 -0
  186. package/dist/local/health-inspection.js +407 -0
  187. package/dist/local/health-inspection.js.map +1 -0
  188. package/dist/local/health-policy.d.ts +11 -0
  189. package/dist/local/health-policy.d.ts.map +1 -0
  190. package/dist/local/health-policy.js +257 -0
  191. package/dist/local/health-policy.js.map +1 -0
  192. package/dist/local/health-projection.d.ts +14 -0
  193. package/dist/local/health-projection.d.ts.map +1 -0
  194. package/dist/local/health-projection.js +102 -0
  195. package/dist/local/health-projection.js.map +1 -0
  196. package/dist/local/health-schema.d.ts +338 -0
  197. package/dist/local/health-schema.d.ts.map +1 -0
  198. package/dist/local/health-schema.js +213 -0
  199. package/dist/local/health-schema.js.map +1 -0
  200. package/dist/local/health-store.d.ts +61 -0
  201. package/dist/local/health-store.d.ts.map +1 -0
  202. package/dist/local/health-store.js +218 -0
  203. package/dist/local/health-store.js.map +1 -0
  204. package/dist/local/index.d.ts +66 -0
  205. package/dist/local/index.d.ts.map +1 -0
  206. package/dist/local/index.js +66 -0
  207. package/dist/local/index.js.map +1 -0
  208. package/dist/local/journey-commands.d.ts +65 -0
  209. package/dist/local/journey-commands.d.ts.map +1 -0
  210. package/dist/local/journey-commands.js +138 -0
  211. package/dist/local/journey-commands.js.map +1 -0
  212. package/dist/local/local-org-store.d.ts +46 -0
  213. package/dist/local/local-org-store.d.ts.map +1 -0
  214. package/dist/local/local-org-store.js +132 -0
  215. package/dist/local/local-org-store.js.map +1 -0
  216. package/dist/local/local-org.d.ts +389 -0
  217. package/dist/local/local-org.d.ts.map +1 -0
  218. package/dist/local/local-org.js +855 -0
  219. package/dist/local/local-org.js.map +1 -0
  220. package/dist/local/media-upload.d.ts +5 -0
  221. package/dist/local/media-upload.d.ts.map +1 -0
  222. package/dist/local/media-upload.js +41 -0
  223. package/dist/local/media-upload.js.map +1 -0
  224. package/dist/local/operation.d.ts +155 -0
  225. package/dist/local/operation.d.ts.map +1 -0
  226. package/dist/local/operation.js +340 -0
  227. package/dist/local/operation.js.map +1 -0
  228. package/dist/local/ownership-reader-schema.d.ts +117 -0
  229. package/dist/local/ownership-reader-schema.d.ts.map +1 -0
  230. package/dist/local/ownership-reader-schema.js +105 -0
  231. package/dist/local/ownership-reader-schema.js.map +1 -0
  232. package/dist/local/ownership-reader-worker.d.ts +2 -0
  233. package/dist/local/ownership-reader-worker.d.ts.map +1 -0
  234. package/dist/local/ownership-reader-worker.js +341 -0
  235. package/dist/local/ownership-reader-worker.js.map +1 -0
  236. package/dist/local/ownership-reader.d.ts +10 -0
  237. package/dist/local/ownership-reader.d.ts.map +1 -0
  238. package/dist/local/ownership-reader.js +93 -0
  239. package/dist/local/ownership-reader.js.map +1 -0
  240. package/dist/local/package-uses-check.d.ts +33 -0
  241. package/dist/local/package-uses-check.d.ts.map +1 -0
  242. package/dist/local/package-uses-check.js +62 -0
  243. package/dist/local/package-uses-check.js.map +1 -0
  244. package/dist/local/pending-recovery.d.ts +22 -0
  245. package/dist/local/pending-recovery.d.ts.map +1 -0
  246. package/dist/local/pending-recovery.js +171 -0
  247. package/dist/local/pending-recovery.js.map +1 -0
  248. package/dist/local/portable-commit-store.d.ts +28 -0
  249. package/dist/local/portable-commit-store.d.ts.map +1 -0
  250. package/dist/local/portable-commit-store.js +187 -0
  251. package/dist/local/portable-commit-store.js.map +1 -0
  252. package/dist/local/portable-commit.d.ts +109 -0
  253. package/dist/local/portable-commit.d.ts.map +1 -0
  254. package/dist/local/portable-commit.js +209 -0
  255. package/dist/local/portable-commit.js.map +1 -0
  256. package/dist/local/portable-remote.d.ts +432 -0
  257. package/dist/local/portable-remote.d.ts.map +1 -0
  258. package/dist/local/portable-remote.js +280 -0
  259. package/dist/local/portable-remote.js.map +1 -0
  260. package/dist/local/project-deleted.d.ts +4 -0
  261. package/dist/local/project-deleted.d.ts.map +1 -0
  262. package/dist/local/project-deleted.js +13 -0
  263. package/dist/local/project-deleted.js.map +1 -0
  264. package/dist/local/projection.d.ts +166 -0
  265. package/dist/local/projection.d.ts.map +1 -0
  266. package/dist/local/projection.js +709 -0
  267. package/dist/local/projection.js.map +1 -0
  268. package/dist/local/read-operation.d.ts +3 -0
  269. package/dist/local/read-operation.d.ts.map +1 -0
  270. package/dist/local/read-operation.js +19 -0
  271. package/dist/local/read-operation.js.map +1 -0
  272. package/dist/local/rebuild.d.ts +11 -0
  273. package/dist/local/rebuild.d.ts.map +1 -0
  274. package/dist/local/rebuild.js +42 -0
  275. package/dist/local/rebuild.js.map +1 -0
  276. package/dist/local/remote-base.d.ts +25 -0
  277. package/dist/local/remote-base.d.ts.map +1 -0
  278. package/dist/local/remote-base.js +68 -0
  279. package/dist/local/remote-base.js.map +1 -0
  280. package/dist/local/save-layout-worker.d.ts +19 -0
  281. package/dist/local/save-layout-worker.d.ts.map +1 -0
  282. package/dist/local/save-layout-worker.js +28 -0
  283. package/dist/local/save-layout-worker.js.map +1 -0
  284. package/dist/local/save-layout.d.ts +84 -0
  285. package/dist/local/save-layout.d.ts.map +1 -0
  286. package/dist/local/save-layout.js +221 -0
  287. package/dist/local/save-layout.js.map +1 -0
  288. package/dist/local/scope-impact.d.ts +31 -0
  289. package/dist/local/scope-impact.d.ts.map +1 -0
  290. package/dist/local/scope-impact.js +32 -0
  291. package/dist/local/scope-impact.js.map +1 -0
  292. package/dist/local/source-evidence/filesystem-worker.d.ts +2 -0
  293. package/dist/local/source-evidence/filesystem-worker.d.ts.map +1 -0
  294. package/dist/local/source-evidence/filesystem-worker.js +139 -0
  295. package/dist/local/source-evidence/filesystem-worker.js.map +1 -0
  296. package/dist/local/source-evidence/processes.d.ts +20 -0
  297. package/dist/local/source-evidence/processes.d.ts.map +1 -0
  298. package/dist/local/source-evidence/processes.js +133 -0
  299. package/dist/local/source-evidence/processes.js.map +1 -0
  300. package/dist/local/source-evidence/protocol.d.ts +52 -0
  301. package/dist/local/source-evidence/protocol.d.ts.map +1 -0
  302. package/dist/local/source-evidence/protocol.js +72 -0
  303. package/dist/local/source-evidence/protocol.js.map +1 -0
  304. package/dist/local/source-evidence/reader.d.ts +14 -0
  305. package/dist/local/source-evidence/reader.d.ts.map +1 -0
  306. package/dist/local/source-evidence/reader.js +308 -0
  307. package/dist/local/source-evidence/reader.js.map +1 -0
  308. package/dist/local/source-evidence/schema.d.ts +2504 -0
  309. package/dist/local/source-evidence/schema.d.ts.map +1 -0
  310. package/dist/local/source-evidence/schema.js +99 -0
  311. package/dist/local/source-evidence/schema.js.map +1 -0
  312. package/dist/local/source-policy/loader.d.ts +29 -0
  313. package/dist/local/source-policy/loader.d.ts.map +1 -0
  314. package/dist/local/source-policy/loader.js +209 -0
  315. package/dist/local/source-policy/loader.js.map +1 -0
  316. package/dist/local/source-policy/process.d.ts +7 -0
  317. package/dist/local/source-policy/process.d.ts.map +1 -0
  318. package/dist/local/source-policy/process.js +53 -0
  319. package/dist/local/source-policy/process.js.map +1 -0
  320. package/dist/local/source-policy/protocol.d.ts +27 -0
  321. package/dist/local/source-policy/protocol.d.ts.map +1 -0
  322. package/dist/local/source-policy/protocol.js +48 -0
  323. package/dist/local/source-policy/protocol.js.map +1 -0
  324. package/dist/local/source-policy/worker.d.ts +2 -0
  325. package/dist/local/source-policy/worker.d.ts.map +1 -0
  326. package/dist/local/source-policy/worker.js +100 -0
  327. package/dist/local/source-policy/worker.js.map +1 -0
  328. package/dist/local/state-directory.d.ts +17 -0
  329. package/dist/local/state-directory.d.ts.map +1 -0
  330. package/dist/local/state-directory.js +68 -0
  331. package/dist/local/state-directory.js.map +1 -0
  332. package/dist/local/store-database.d.ts +13 -0
  333. package/dist/local/store-database.d.ts.map +1 -0
  334. package/dist/local/store-database.js +187 -0
  335. package/dist/local/store-database.js.map +1 -0
  336. package/dist/local/store.d.ts +181 -0
  337. package/dist/local/store.d.ts.map +1 -0
  338. package/dist/local/store.js +585 -0
  339. package/dist/local/store.js.map +1 -0
  340. package/dist/local/sync.d.ts +47 -0
  341. package/dist/local/sync.d.ts.map +1 -0
  342. package/dist/local/sync.js +518 -0
  343. package/dist/local/sync.js.map +1 -0
  344. package/dist/local/task-context-inspection.d.ts +23 -0
  345. package/dist/local/task-context-inspection.d.ts.map +1 -0
  346. package/dist/local/task-context-inspection.js +165 -0
  347. package/dist/local/task-context-inspection.js.map +1 -0
  348. package/dist/local/task-context-observations.d.ts +9553 -0
  349. package/dist/local/task-context-observations.d.ts.map +1 -0
  350. package/dist/local/task-context-observations.js +232 -0
  351. package/dist/local/task-context-observations.js.map +1 -0
  352. package/dist/local/task-context-provider.d.ts +18 -0
  353. package/dist/local/task-context-provider.d.ts.map +1 -0
  354. package/dist/local/task-context-provider.js +231 -0
  355. package/dist/local/task-context-provider.js.map +1 -0
  356. package/dist/local/task-context-schema.d.ts +847 -0
  357. package/dist/local/task-context-schema.d.ts.map +1 -0
  358. package/dist/local/task-context-schema.js +305 -0
  359. package/dist/local/task-context-schema.js.map +1 -0
  360. package/dist/local/task-context-source.d.ts +16 -0
  361. package/dist/local/task-context-source.d.ts.map +1 -0
  362. package/dist/local/task-context-source.js +45 -0
  363. package/dist/local/task-context-source.js.map +1 -0
  364. package/dist/local/task-context.d.ts +14 -0
  365. package/dist/local/task-context.d.ts.map +1 -0
  366. package/dist/local/task-context.js +570 -0
  367. package/dist/local/task-context.js.map +1 -0
  368. package/dist/local/test-commands.d.ts +71 -0
  369. package/dist/local/test-commands.d.ts.map +1 -0
  370. package/dist/local/test-commands.js +292 -0
  371. package/dist/local/test-commands.js.map +1 -0
  372. package/dist/local/tree-save.d.ts +117 -0
  373. package/dist/local/tree-save.d.ts.map +1 -0
  374. package/dist/local/tree-save.js +615 -0
  375. package/dist/local/tree-save.js.map +1 -0
  376. package/dist/local/variants.d.ts +125 -0
  377. package/dist/local/variants.d.ts.map +1 -0
  378. package/dist/local/variants.js +267 -0
  379. package/dist/local/variants.js.map +1 -0
  380. package/dist/local/workspace-admission.d.ts +61 -0
  381. package/dist/local/workspace-admission.d.ts.map +1 -0
  382. package/dist/local/workspace-admission.js +112 -0
  383. package/dist/local/workspace-admission.js.map +1 -0
  384. package/dist/local/workspace-authority-guard.d.ts +18 -0
  385. package/dist/local/workspace-authority-guard.d.ts.map +1 -0
  386. package/dist/local/workspace-authority-guard.js +69 -0
  387. package/dist/local/workspace-authority-guard.js.map +1 -0
  388. package/dist/local/workspace-authority.d.ts +33 -0
  389. package/dist/local/workspace-authority.d.ts.map +1 -0
  390. package/dist/local/workspace-authority.js +33 -0
  391. package/dist/local/workspace-authority.js.map +1 -0
  392. package/dist/local/workspace-branch-runtime.d.ts +61 -0
  393. package/dist/local/workspace-branch-runtime.d.ts.map +1 -0
  394. package/dist/local/workspace-branch-runtime.js +216 -0
  395. package/dist/local/workspace-branch-runtime.js.map +1 -0
  396. package/dist/local/workspace-built-content.d.ts +137 -0
  397. package/dist/local/workspace-built-content.d.ts.map +1 -0
  398. package/dist/local/workspace-built-content.js +393 -0
  399. package/dist/local/workspace-built-content.js.map +1 -0
  400. package/dist/local/workspace-claim-status-read.d.ts +53 -0
  401. package/dist/local/workspace-claim-status-read.d.ts.map +1 -0
  402. package/dist/local/workspace-claim-status-read.js +118 -0
  403. package/dist/local/workspace-claim-status-read.js.map +1 -0
  404. package/dist/local/workspace-claim-status.d.ts +172 -0
  405. package/dist/local/workspace-claim-status.d.ts.map +1 -0
  406. package/dist/local/workspace-claim-status.js +721 -0
  407. package/dist/local/workspace-claim-status.js.map +1 -0
  408. package/dist/local/workspace-claim-views.d.ts +45 -0
  409. package/dist/local/workspace-claim-views.d.ts.map +1 -0
  410. package/dist/local/workspace-claim-views.js +45 -0
  411. package/dist/local/workspace-claim-views.js.map +1 -0
  412. package/dist/local/workspace-cli.d.ts +46 -0
  413. package/dist/local/workspace-cli.d.ts.map +1 -0
  414. package/dist/local/workspace-cli.js +1524 -0
  415. package/dist/local/workspace-cli.js.map +1 -0
  416. package/dist/local/workspace-commit-resolver.d.ts +19 -0
  417. package/dist/local/workspace-commit-resolver.d.ts.map +1 -0
  418. package/dist/local/workspace-commit-resolver.js +47 -0
  419. package/dist/local/workspace-commit-resolver.js.map +1 -0
  420. package/dist/local/workspace-decision-store.d.ts +184 -0
  421. package/dist/local/workspace-decision-store.d.ts.map +1 -0
  422. package/dist/local/workspace-decision-store.js +510 -0
  423. package/dist/local/workspace-decision-store.js.map +1 -0
  424. package/dist/local/workspace-environment-runtime.d.ts +107 -0
  425. package/dist/local/workspace-environment-runtime.d.ts.map +1 -0
  426. package/dist/local/workspace-environment-runtime.js +135 -0
  427. package/dist/local/workspace-environment-runtime.js.map +1 -0
  428. package/dist/local/workspace-evidence-drop.d.ts +21 -0
  429. package/dist/local/workspace-evidence-drop.d.ts.map +1 -0
  430. package/dist/local/workspace-evidence-drop.js +58 -0
  431. package/dist/local/workspace-evidence-drop.js.map +1 -0
  432. package/dist/local/workspace-evidence-sign-tool.d.ts +20 -0
  433. package/dist/local/workspace-evidence-sign-tool.d.ts.map +1 -0
  434. package/dist/local/workspace-evidence-sign-tool.js +85 -0
  435. package/dist/local/workspace-evidence-sign-tool.js.map +1 -0
  436. package/dist/local/workspace-evidence-status.d.ts +89 -0
  437. package/dist/local/workspace-evidence-status.d.ts.map +1 -0
  438. package/dist/local/workspace-evidence-status.js +141 -0
  439. package/dist/local/workspace-evidence-status.js.map +1 -0
  440. package/dist/local/workspace-evidence.d.ts +145 -0
  441. package/dist/local/workspace-evidence.d.ts.map +1 -0
  442. package/dist/local/workspace-evidence.js +347 -0
  443. package/dist/local/workspace-evidence.js.map +1 -0
  444. package/dist/local/workspace-graph-store.d.ts +32 -0
  445. package/dist/local/workspace-graph-store.d.ts.map +1 -0
  446. package/dist/local/workspace-graph-store.js +249 -0
  447. package/dist/local/workspace-graph-store.js.map +1 -0
  448. package/dist/local/workspace-handoff.d.ts +32 -0
  449. package/dist/local/workspace-handoff.d.ts.map +1 -0
  450. package/dist/local/workspace-handoff.js +144 -0
  451. package/dist/local/workspace-handoff.js.map +1 -0
  452. package/dist/local/workspace-history-journal.d.ts +70 -0
  453. package/dist/local/workspace-history-journal.d.ts.map +1 -0
  454. package/dist/local/workspace-history-journal.js +131 -0
  455. package/dist/local/workspace-history-journal.js.map +1 -0
  456. package/dist/local/workspace-history-store.d.ts +98 -0
  457. package/dist/local/workspace-history-store.d.ts.map +1 -0
  458. package/dist/local/workspace-history-store.js +632 -0
  459. package/dist/local/workspace-history-store.js.map +1 -0
  460. package/dist/local/workspace-history-types.d.ts +307 -0
  461. package/dist/local/workspace-history-types.d.ts.map +1 -0
  462. package/dist/local/workspace-history-types.js +2 -0
  463. package/dist/local/workspace-history-types.js.map +1 -0
  464. package/dist/local/workspace-integration-runtime.d.ts +48 -0
  465. package/dist/local/workspace-integration-runtime.d.ts.map +1 -0
  466. package/dist/local/workspace-integration-runtime.js +419 -0
  467. package/dist/local/workspace-integration-runtime.js.map +1 -0
  468. package/dist/local/workspace-lock.d.ts +7 -0
  469. package/dist/local/workspace-lock.d.ts.map +1 -0
  470. package/dist/local/workspace-lock.js +211 -0
  471. package/dist/local/workspace-lock.js.map +1 -0
  472. package/dist/local/workspace-merge.d.ts +77 -0
  473. package/dist/local/workspace-merge.d.ts.map +1 -0
  474. package/dist/local/workspace-merge.js +703 -0
  475. package/dist/local/workspace-merge.js.map +1 -0
  476. package/dist/local/workspace-mixed-history.d.ts +33 -0
  477. package/dist/local/workspace-mixed-history.d.ts.map +1 -0
  478. package/dist/local/workspace-mixed-history.js +107 -0
  479. package/dist/local/workspace-mixed-history.js.map +1 -0
  480. package/dist/local/workspace-overlap-allocation.d.ts +33 -0
  481. package/dist/local/workspace-overlap-allocation.d.ts.map +1 -0
  482. package/dist/local/workspace-overlap-allocation.js +69 -0
  483. package/dist/local/workspace-overlap-allocation.js.map +1 -0
  484. package/dist/local/workspace-overlap-impact.d.ts +46 -0
  485. package/dist/local/workspace-overlap-impact.d.ts.map +1 -0
  486. package/dist/local/workspace-overlap-impact.js +162 -0
  487. package/dist/local/workspace-overlap-impact.js.map +1 -0
  488. package/dist/local/workspace-overlap.d.ts +109 -0
  489. package/dist/local/workspace-overlap.d.ts.map +1 -0
  490. package/dist/local/workspace-overlap.js +327 -0
  491. package/dist/local/workspace-overlap.js.map +1 -0
  492. package/dist/local/workspace-paths.d.ts +4 -0
  493. package/dist/local/workspace-paths.d.ts.map +1 -0
  494. package/dist/local/workspace-paths.js +8 -0
  495. package/dist/local/workspace-paths.js.map +1 -0
  496. package/dist/local/workspace-pick-runtime.d.ts +70 -0
  497. package/dist/local/workspace-pick-runtime.d.ts.map +1 -0
  498. package/dist/local/workspace-pick-runtime.js +177 -0
  499. package/dist/local/workspace-pick-runtime.js.map +1 -0
  500. package/dist/local/workspace-pick-store.d.ts +33 -0
  501. package/dist/local/workspace-pick-store.d.ts.map +1 -0
  502. package/dist/local/workspace-pick-store.js +43 -0
  503. package/dist/local/workspace-pick-store.js.map +1 -0
  504. package/dist/local/workspace-pick.d.ts +73 -0
  505. package/dist/local/workspace-pick.d.ts.map +1 -0
  506. package/dist/local/workspace-pick.js +253 -0
  507. package/dist/local/workspace-pick.js.map +1 -0
  508. package/dist/local/workspace-portable-authoring.d.ts +43 -0
  509. package/dist/local/workspace-portable-authoring.d.ts.map +1 -0
  510. package/dist/local/workspace-portable-authoring.js +128 -0
  511. package/dist/local/workspace-portable-authoring.js.map +1 -0
  512. package/dist/local/workspace-prune.d.ts +63 -0
  513. package/dist/local/workspace-prune.d.ts.map +1 -0
  514. package/dist/local/workspace-prune.js +267 -0
  515. package/dist/local/workspace-prune.js.map +1 -0
  516. package/dist/local/workspace-remote-fetch.d.ts +82 -0
  517. package/dist/local/workspace-remote-fetch.d.ts.map +1 -0
  518. package/dist/local/workspace-remote-fetch.js +409 -0
  519. package/dist/local/workspace-remote-fetch.js.map +1 -0
  520. package/dist/local/workspace-remote-provider.d.ts +97 -0
  521. package/dist/local/workspace-remote-provider.d.ts.map +1 -0
  522. package/dist/local/workspace-remote-provider.js +400 -0
  523. package/dist/local/workspace-remote-provider.js.map +1 -0
  524. package/dist/local/workspace-remote-pull-request-journal.d.ts +49 -0
  525. package/dist/local/workspace-remote-pull-request-journal.d.ts.map +1 -0
  526. package/dist/local/workspace-remote-pull-request-journal.js +63 -0
  527. package/dist/local/workspace-remote-pull-request-journal.js.map +1 -0
  528. package/dist/local/workspace-remote-pull-request.d.ts +36 -0
  529. package/dist/local/workspace-remote-pull-request.d.ts.map +1 -0
  530. package/dist/local/workspace-remote-pull-request.js +158 -0
  531. package/dist/local/workspace-remote-pull-request.js.map +1 -0
  532. package/dist/local/workspace-remote-push.d.ts +88 -0
  533. package/dist/local/workspace-remote-push.d.ts.map +1 -0
  534. package/dist/local/workspace-remote-push.js +297 -0
  535. package/dist/local/workspace-remote-push.js.map +1 -0
  536. package/dist/local/workspace-remote-topic-journal.d.ts +56 -0
  537. package/dist/local/workspace-remote-topic-journal.d.ts.map +1 -0
  538. package/dist/local/workspace-remote-topic-journal.js +121 -0
  539. package/dist/local/workspace-remote-topic-journal.js.map +1 -0
  540. package/dist/local/workspace-remote-topic.d.ts +45 -0
  541. package/dist/local/workspace-remote-topic.d.ts.map +1 -0
  542. package/dist/local/workspace-remote-topic.js +204 -0
  543. package/dist/local/workspace-remote-topic.js.map +1 -0
  544. package/dist/local/workspace-remote-transfer.d.ts +76 -0
  545. package/dist/local/workspace-remote-transfer.d.ts.map +1 -0
  546. package/dist/local/workspace-remote-transfer.js +270 -0
  547. package/dist/local/workspace-remote-transfer.js.map +1 -0
  548. package/dist/local/workspace-resolution.d.ts +145 -0
  549. package/dist/local/workspace-resolution.d.ts.map +1 -0
  550. package/dist/local/workspace-resolution.js +479 -0
  551. package/dist/local/workspace-resolution.js.map +1 -0
  552. package/dist/local/workspace-resolver-provider.d.ts +72 -0
  553. package/dist/local/workspace-resolver-provider.d.ts.map +1 -0
  554. package/dist/local/workspace-resolver-provider.js +312 -0
  555. package/dist/local/workspace-resolver-provider.js.map +1 -0
  556. package/dist/local/workspace-runtime-handoff.d.ts +52 -0
  557. package/dist/local/workspace-runtime-handoff.d.ts.map +1 -0
  558. package/dist/local/workspace-runtime-handoff.js +237 -0
  559. package/dist/local/workspace-runtime-handoff.js.map +1 -0
  560. package/dist/local/workspace-runtime-lease.d.ts +26 -0
  561. package/dist/local/workspace-runtime-lease.d.ts.map +1 -0
  562. package/dist/local/workspace-runtime-lease.js +123 -0
  563. package/dist/local/workspace-runtime-lease.js.map +1 -0
  564. package/dist/local/workspace-runtime.d.ts +690 -0
  565. package/dist/local/workspace-runtime.d.ts.map +1 -0
  566. package/dist/local/workspace-runtime.js +2124 -0
  567. package/dist/local/workspace-runtime.js.map +1 -0
  568. package/dist/local/workspace-scope.d.ts +70 -0
  569. package/dist/local/workspace-scope.d.ts.map +1 -0
  570. package/dist/local/workspace-scope.js +206 -0
  571. package/dist/local/workspace-scope.js.map +1 -0
  572. package/dist/local/workspace-sources.d.ts +159 -0
  573. package/dist/local/workspace-sources.d.ts.map +1 -0
  574. package/dist/local/workspace-sources.js +570 -0
  575. package/dist/local/workspace-sources.js.map +1 -0
  576. package/dist/local/workspace-store-database.d.ts +5 -0
  577. package/dist/local/workspace-store-database.d.ts.map +1 -0
  578. package/dist/local/workspace-store-database.js +726 -0
  579. package/dist/local/workspace-store-database.js.map +1 -0
  580. package/dist/local/workspace-store.d.ts +261 -0
  581. package/dist/local/workspace-store.d.ts.map +1 -0
  582. package/dist/local/workspace-store.js +831 -0
  583. package/dist/local/workspace-store.js.map +1 -0
  584. package/dist/local/workspace-topology-runtime.d.ts +27 -0
  585. package/dist/local/workspace-topology-runtime.d.ts.map +1 -0
  586. package/dist/local/workspace-topology-runtime.js +396 -0
  587. package/dist/local/workspace-topology-runtime.js.map +1 -0
  588. package/dist/local/workspace-topology.d.ts +140 -0
  589. package/dist/local/workspace-topology.d.ts.map +1 -0
  590. package/dist/local/workspace-topology.js +359 -0
  591. package/dist/local/workspace-topology.js.map +1 -0
  592. package/dist/local/workspace-upstream-inspection.d.ts +60 -0
  593. package/dist/local/workspace-upstream-inspection.d.ts.map +1 -0
  594. package/dist/local/workspace-upstream-inspection.js +189 -0
  595. package/dist/local/workspace-upstream-inspection.js.map +1 -0
  596. package/dist/local/workspace-upstream-runtime.d.ts +91 -0
  597. package/dist/local/workspace-upstream-runtime.d.ts.map +1 -0
  598. package/dist/local/workspace-upstream-runtime.js +270 -0
  599. package/dist/local/workspace-upstream-runtime.js.map +1 -0
  600. package/dist/local/workspace-upstream-store.d.ts +51 -0
  601. package/dist/local/workspace-upstream-store.d.ts.map +1 -0
  602. package/dist/local/workspace-upstream-store.js +475 -0
  603. package/dist/local/workspace-upstream-store.js.map +1 -0
  604. package/dist/local/workspace-viewer-server.d.ts +136 -0
  605. package/dist/local/workspace-viewer-server.d.ts.map +1 -0
  606. package/dist/local/workspace-viewer-server.js +1097 -0
  607. package/dist/local/workspace-viewer-server.js.map +1 -0
  608. package/dist/local/workspace-viewer-snapshot.d.ts +49 -0
  609. package/dist/local/workspace-viewer-snapshot.d.ts.map +1 -0
  610. package/dist/local/workspace-viewer-snapshot.js +91 -0
  611. package/dist/local/workspace-viewer-snapshot.js.map +1 -0
  612. package/dist/local/worktree-projection.d.ts +76 -0
  613. package/dist/local/worktree-projection.d.ts.map +1 -0
  614. package/dist/local/worktree-projection.js +470 -0
  615. package/dist/local/worktree-projection.js.map +1 -0
  616. package/dist/mcp/client-identity.d.ts +8 -0
  617. package/dist/mcp/client-identity.d.ts.map +1 -0
  618. package/dist/mcp/client-identity.js +33 -0
  619. package/dist/mcp/client-identity.js.map +1 -0
  620. package/dist/mcp/graph-to-code.d.ts +53 -0
  621. package/dist/mcp/graph-to-code.d.ts.map +1 -0
  622. package/dist/mcp/graph-to-code.js +1624 -0
  623. package/dist/mcp/graph-to-code.js.map +1 -0
  624. package/dist/mcp/index.d.ts +3 -0
  625. package/dist/mcp/index.d.ts.map +1 -0
  626. package/dist/mcp/index.js +3 -0
  627. package/dist/mcp/index.js.map +1 -0
  628. package/dist/mcp/stdio/graph-to-code.d.ts +3 -0
  629. package/dist/mcp/stdio/graph-to-code.d.ts.map +1 -0
  630. package/dist/mcp/stdio/graph-to-code.js +55 -0
  631. package/dist/mcp/stdio/graph-to-code.js.map +1 -0
  632. package/dist/mcp/stdio/worktree.d.ts +3 -0
  633. package/dist/mcp/stdio/worktree.d.ts.map +1 -0
  634. package/dist/mcp/stdio/worktree.js +67 -0
  635. package/dist/mcp/stdio/worktree.js.map +1 -0
  636. package/dist/mcp/worktree-claim-status.d.ts +162 -0
  637. package/dist/mcp/worktree-claim-status.d.ts.map +1 -0
  638. package/dist/mcp/worktree-claim-status.js +113 -0
  639. package/dist/mcp/worktree-claim-status.js.map +1 -0
  640. package/dist/mcp/worktree-dispatch.d.ts +13 -0
  641. package/dist/mcp/worktree-dispatch.d.ts.map +1 -0
  642. package/dist/mcp/worktree-dispatch.js +52 -0
  643. package/dist/mcp/worktree-dispatch.js.map +1 -0
  644. package/dist/mcp/worktree-errors.d.ts +15 -0
  645. package/dist/mcp/worktree-errors.d.ts.map +1 -0
  646. package/dist/mcp/worktree-errors.js +17 -0
  647. package/dist/mcp/worktree-errors.js.map +1 -0
  648. package/dist/mcp/worktree-health.d.ts +12 -0
  649. package/dist/mcp/worktree-health.d.ts.map +1 -0
  650. package/dist/mcp/worktree-health.js +32 -0
  651. package/dist/mcp/worktree-health.js.map +1 -0
  652. package/dist/mcp/worktree-results.d.ts +15 -0
  653. package/dist/mcp/worktree-results.d.ts.map +1 -0
  654. package/dist/mcp/worktree-results.js +12 -0
  655. package/dist/mcp/worktree-results.js.map +1 -0
  656. package/dist/mcp/worktree-task-context.d.ts +57 -0
  657. package/dist/mcp/worktree-task-context.d.ts.map +1 -0
  658. package/dist/mcp/worktree-task-context.js +45 -0
  659. package/dist/mcp/worktree-task-context.js.map +1 -0
  660. package/dist/mcp/worktree-tools.d.ts +2648 -0
  661. package/dist/mcp/worktree-tools.d.ts.map +1 -0
  662. package/dist/mcp/worktree-tools.js +192 -0
  663. package/dist/mcp/worktree-tools.js.map +1 -0
  664. package/dist/mcp/worktree.d.ts +33 -0
  665. package/dist/mcp/worktree.d.ts.map +1 -0
  666. package/dist/mcp/worktree.js +551 -0
  667. package/dist/mcp/worktree.js.map +1 -0
  668. package/dist/own-package.d.ts +13 -0
  669. package/dist/own-package.d.ts.map +1 -0
  670. package/dist/own-package.js +32 -0
  671. package/dist/own-package.js.map +1 -0
  672. package/dist/packages-browse.d.ts +107 -0
  673. package/dist/packages-browse.d.ts.map +1 -0
  674. package/dist/packages-browse.js +646 -0
  675. package/dist/packages-browse.js.map +1 -0
  676. package/dist/packages-cli.d.ts +8 -0
  677. package/dist/packages-cli.d.ts.map +1 -0
  678. package/dist/packages-cli.js +184 -0
  679. package/dist/packages-cli.js.map +1 -0
  680. package/dist/packages-uses.d.ts +46 -0
  681. package/dist/packages-uses.d.ts.map +1 -0
  682. package/dist/packages-uses.js +737 -0
  683. package/dist/packages-uses.js.map +1 -0
  684. package/dist/portable.d.ts +7 -0
  685. package/dist/portable.d.ts.map +1 -0
  686. package/dist/portable.js +14 -0
  687. package/dist/portable.js.map +1 -0
  688. package/dist/quiet-warnings.d.ts +3 -0
  689. package/dist/quiet-warnings.d.ts.map +1 -0
  690. package/dist/quiet-warnings.js +21 -0
  691. package/dist/quiet-warnings.js.map +1 -0
  692. package/dist/view-seam.d.ts +62 -0
  693. package/dist/view-seam.d.ts.map +1 -0
  694. package/dist/view-seam.js +70 -0
  695. package/dist/view-seam.js.map +1 -0
  696. package/generated/graph-packages/index.json +5 -0
  697. package/graph-packages.lock.json +4 -0
  698. package/package.json +81 -0
  699. package/viewer/assets/index-BZKGDARF.css +1 -0
  700. package/viewer/assets/index-DWu0CbZZ.js +272 -0
  701. package/viewer/index.html +13 -0
@@ -0,0 +1,1097 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { createServer } from 'node:http';
4
+ import { extname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { inspectLocalWorkspaceViewerSnapshot } from './workspace-viewer-snapshot.js';
7
+ const LOOPBACK_HOST = '127.0.0.1';
8
+ const THREADS_TTL_MS = 5_000;
9
+ const PIN_BODY_LIMIT = 4_096;
10
+ // A Work queues answer this young is served as it is; an older one is served while a newer is read.
11
+ const QUEUES_FRESH_MS = 15_000;
12
+ const PIN_KEY_LIMIT = 200;
13
+ // The Releases panel's reads (rl-ui-viewer): a summary is kept for a little under the page's
14
+ // five-minute refresh, timed from when its read starts, so each refresh reads it anew; a subject is
15
+ // kept one minute; a failed read is never kept.
16
+ const RELEASE_SUMMARY_TTL_MS = 5 * 60_000 - 30_000;
17
+ const RELEASE_SUBJECT_TTL_MS = 60_000;
18
+ // The subjects api/release-links reads, each value checked before release_links.py sees it (none
19
+ // starts with '-', so none reads as an option). The page checks the address with the same patterns.
20
+ // A version, the candidate, or a release batch: a Release that did not change the app keeps the
21
+ // app's version and is named by its batch (release-versioning-and-notes.md, Label).
22
+ const RELEASE_REF = /^(candidate|v?\d{1,4}\.\d{1,4}\.\d{1,6}(?:-[0-9A-Za-z.]{1,40})?|[a-z][a-z0-9-]{0,63})$/;
23
+ const PLANNED_REF = /^([a-z0-9][a-z0-9-]{0,63})?$/i;
24
+ const ISSUE_REF = /^iss-\d{1,9}$/;
25
+ // A task id as task-page-read.js accepts it (tg-task-page-viewer).
26
+ const TASK_REF = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
27
+ // A hold id as the coordination records write it.
28
+ const HOLD_REF = /^hold-[A-Za-z0-9]{1,64}$/;
29
+ /** The subject a query names, or why it names none: at most one of release, planned, issue, task, releases, threads. */
30
+ export function releaseSubjectOf(query) {
31
+ const keys = [...query.keys()];
32
+ if (!keys.length)
33
+ return { kind: 'summary' };
34
+ const refused = {
35
+ refused: 'Ask for one of release=<version|candidate>, planned[=<id>], issue=<iss-N>, task=<id>, releases or threads.'
36
+ };
37
+ if (keys.length > 1)
38
+ return refused;
39
+ const [key] = keys;
40
+ const value = query.get(key) ?? '';
41
+ if (key === 'releases')
42
+ return value ? refused : { kind: 'releases' };
43
+ if (key === 'threads')
44
+ return value ? refused : { kind: 'threads' };
45
+ if (key === 'release' && RELEASE_REF.test(value))
46
+ return { kind: 'release', ref: value };
47
+ if (key === 'planned' && PLANNED_REF.test(value))
48
+ return { kind: 'planned', ref: value };
49
+ if (key === 'issue' && ISSUE_REF.test(value))
50
+ return { kind: 'issue', ref: value };
51
+ if (key === 'task' && TASK_REF.test(value))
52
+ return { kind: 'task', ref: value };
53
+ return refused;
54
+ }
55
+ function viewerHtml(input) {
56
+ return `<!doctype html>
57
+ <html lang="en">
58
+ <head>
59
+ <meta charset="utf-8">
60
+ <meta name="viewport" content="width=device-width,initial-scale=1">
61
+ <title>SDLC Workspace Viewer</title>
62
+ <style nonce="${input.nonce}">
63
+ :root{color-scheme:dark;--bg:#0a0c10;--panel:#12161d;--line:#2a303a;--ink:#f4f1e8;--muted:#9aa3af;--lime:#b7f46b;--amber:#f2c45c;--red:#f17676;--blue:#72b7ff;--card:#0d1117;font:14px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace}
64
+ *{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 15% -10%,#1a2730 0,transparent 35%),var(--bg);color:var(--ink)}
65
+ main{max-width:1280px;margin:auto;padding:42px 28px 64px}header{display:flex;justify-content:space-between;gap:24px;align-items:flex-end;margin-bottom:28px}
66
+ h1{font:600 clamp(28px,5vw,54px)/.95 ui-sans-serif,system-ui;margin:0;letter-spacing:-.05em}h2{font:600 15px/1.2 ui-sans-serif,system-ui;margin:0 0 14px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted)}
67
+ .eyebrow{color:var(--lime);text-transform:uppercase;letter-spacing:.18em;margin-bottom:10px}.muted{color:var(--muted)}.mono{overflow-wrap:anywhere}
68
+ .pulse{display:inline-block;width:9px;height:9px;border-radius:50%;background:var(--lime);box-shadow:0 0 16px var(--lime);margin-right:8px}
69
+ .metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:24px}.metric,.panel{background:color-mix(in srgb,var(--panel) 92%,transparent);border:1px solid var(--line);border-radius:14px}
70
+ .metric{padding:18px}.metric strong{display:block;font:600 28px/1 ui-sans-serif,system-ui;margin-top:6px}.metric span{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.09em}
71
+ .layout{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(310px,.6fr);gap:16px}.panel{padding:20px;min-width:0}.span{grid-column:1/-1}
72
+ .workspace{position:relative;display:grid;grid-template-columns:minmax(150px,1fr) auto;gap:12px;padding:15px 16px;margin:9px 0;border:1px solid var(--line);border-left:3px solid var(--blue);border-radius:10px;background:var(--card)}
73
+ .workspace.current{border-left-color:var(--lime)}.workspace.closed{opacity:.58}.workspace small{display:block;color:var(--muted);margin-top:4px}.badges{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px;align-content:start}
74
+ .badge{border:1px solid var(--line);border-radius:999px;padding:3px 8px;font-size:11px;color:var(--muted)}.good{color:var(--lime);border-color:#426026}.warn{color:var(--amber);border-color:#6b5421}.bad{color:var(--red);border-color:#693232}
75
+ .info{color:var(--blue);border-color:#264a6b}.purple{color:#c69cff;border-color:#4b3a6b}.thread-line{margin:4px 0 0;overflow-wrap:anywhere}.thread-line.good{color:var(--lime)}.thread-line.warn{color:var(--amber)}.thread-line.bad{color:var(--red)}.thread-line.info{color:var(--blue)}.thread-line.purple{color:#c69cff}
76
+ .event{padding:12px 0;border-top:1px solid var(--line)}.event:first-of-type{border-top:0}.event-head{display:flex;justify-content:space-between;gap:10px}.empty{color:var(--muted);padding:18px 0}
77
+ dl{display:grid;grid-template-columns:max-content 1fr;gap:8px 14px;margin:0}dt{color:var(--muted)}dd{margin:0;overflow-wrap:anywhere}.error{color:var(--red)}
78
+ .group{font:500 13px/1.3 ui-sans-serif,system-ui;color:var(--muted);margin:16px 0 2px}.group .count{margin-left:6px;font-size:12px}.tools{display:flex;flex-wrap:wrap;gap:6px;align-items:center;justify-content:flex-end}
79
+ .pin{font-family:inherit;font-size:11px;font-weight:500;line-height:1;min-height:22px;padding:4px 9px;border:1px solid var(--line);border-radius:6px;background:transparent;color:var(--muted);cursor:pointer}.pin:hover{background:var(--line);color:var(--ink)}.pin.on{background:var(--line);color:var(--ink)}.pin.on:hover{border-color:var(--muted)}.pin:focus-visible{outline:2px solid var(--blue);outline-offset:2px}.pin[aria-busy=true]{cursor:progress}
80
+ .sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}#threads-error:empty{display:none}#shell-link{margin-left:14px}
81
+ a{color:var(--blue);font:inherit;text-decoration:underline;text-underline-offset:2px}a:focus-visible,.rl input:focus-visible,.rl button:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
82
+ .rl h2:focus,.rl h3:focus,.rl h4:focus{outline:none}.rl h3{font:600 15px/1.3 ui-sans-serif,system-ui;margin:10px 0 6px;overflow-wrap:anywhere}.rl h3 code{color:var(--muted);margin-right:8px}.rl h3 .small{font-size:12px;font-weight:400;margin-left:4px}.rl h3>span:not(.small){font-size:14px;font-weight:400;margin-left:4px}.rl h3>span.strong{font-weight:600}.rl h3 .clip{display:inline-block;max-width:60%;vertical-align:bottom;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
83
+ .rl h4{font:500 13px/1.3 ui-sans-serif,system-ui;color:var(--muted);margin:14px 0 4px}.rl h4 .count{margin-left:4px;font-size:12px}.rl code{font:inherit}
84
+ .rl-terms{grid-template-columns:100px minmax(0,1fr);margin-bottom:12px}.rl-row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:8px;row-gap:2px;margin:3px 0;min-width:0}
85
+ .rl-row .tight{margin-left:-8px}.rl-row .small{font-size:12px}.rl-row .strong{font-weight:600}.rl-row .clip{flex:1 1 0;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.rl-row a.clip{flex:0 1 auto;max-width:24ch}.rl-row .below{flex:0 0 100%;min-width:0;padding-left:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.in1{padding-left:12px}.in2{padding-left:24px}
86
+ .rl-actions,.rl-actions form{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:0}.rl label{color:var(--muted);font-size:12px;font-weight:500}
87
+ .rl input{font:inherit;height:28px;width:140px;padding:0 8px;border:1px solid var(--muted);border-radius:6px;background:var(--bg);color:var(--ink)}.rl input[aria-invalid=true]{border-color:var(--red)}.rl .pin{min-height:28px}
88
+ .rl-back{display:inline-block;margin-bottom:2px}#rl-lookup-error{margin-top:6px}#rl-lookup-error:empty{display:none}.rl input::placeholder{color:var(--muted)}
89
+ .rl-terms dd .rl-row{margin:0}.rl-head>:last-child{flex:1 1 16ch;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.rl-said:focus{outline:none}
90
+ #releases-status:empty{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
91
+ .wq-head{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:12px;margin-bottom:14px}.wq-head h2{margin:0}.wq-head span{font-size:12px}#queues-status:empty{display:none}
92
+ .wq-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(320px,100%),1fr));gap:12px;align-items:start}
93
+ .wq-card{display:flex;flex-direction:column;gap:6px;padding:14px;border:1px solid var(--line);border-radius:10px;background:var(--card);min-width:0}
94
+ .wq-card h3{font:600 14px/1.3 ui-sans-serif,system-ui;margin:0}.wq-card p{margin:0;font-size:12px;overflow-wrap:anywhere}
95
+ .wq-alert{border:1px solid;border-radius:8px;padding:8px 10px;background:color-mix(in srgb,currentColor 10%,transparent);font-size:12px;overflow-wrap:anywhere}.wq-alert strong{display:block;font-size:13px;font-weight:600}
96
+ .wq-rows{list-style:none;margin:4px 0 0;padding:0;display:flex;flex-direction:column;gap:8px}.wq-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:10px;align-items:start}
97
+ .wq-row .pos{color:var(--muted);font-size:12px;min-width:1.2em;text-align:right}.wq-row .title{overflow-wrap:anywhere}.wq-row .detail{color:var(--muted);font-size:12px;overflow-wrap:anywhere}.wq-row .badge{min-width:4.5em;text-align:center}
98
+ .tg h3.group{margin:12px 0 2px}.fold{display:inline-flex;align-items:baseline;gap:6px;font:inherit;color:inherit;background:none;border:0;padding:2px 4px;margin-left:-4px;border-radius:4px;cursor:pointer}.fold:hover{color:var(--ink)}.fold:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
99
+ .tg-row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:8px;row-gap:2px;padding-left:16px;margin:3px 0;min-width:0}.tg-row .title,.tg-row .id,.tg-row a{overflow-wrap:anywhere;min-width:0}.tg .more{padding:4px 0 4px 16px}#issues-error:empty{display:none}
100
+ .rl-back[hidden]{display:none}.popout{display:inline-flex;align-items:center;justify-content:center;min-width:24px;min-height:24px;text-decoration:none;align-self:center}#whole-view{margin-bottom:14px}
101
+ .issue-toolbar{display:flex;flex-direction:column;gap:8px;margin:6px 0 4px}.tb-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.issue-toolbar[hidden],.tb-row[hidden]{display:none}.tb-row.chips{column-gap:14px}
102
+ .issue-toolbar input,.issue-toolbar select{font:inherit;height:28px;padding:0 8px;border:1px solid var(--muted);border-radius:6px;background:var(--bg);color:var(--ink)}.issue-toolbar input{flex:1 1 160px;min-width:0}.issue-toolbar select{min-width:140px;max-width:100%}
103
+ .issue-toolbar input:focus-visible,.issue-toolbar select:focus-visible,.issue-toolbar button:focus-visible{outline:2px solid var(--blue);outline-offset:2px}.issue-toolbar input::placeholder{color:var(--muted)}
104
+ .chip:not(.on):hover{background:transparent;color:var(--muted);border-color:var(--muted)}.chip[aria-disabled=true]{opacity:.5;cursor:default}.chip[aria-disabled=true]:hover{border-color:var(--line)}
105
+ .one-section .metrics,.one-section .popout,.one-section .layout>section:not(.keep){display:none}
106
+ @media(max-width:800px){.rl-row .clip,.rl-row .below,.rl h3 .clip,.rl-head>:last-child{white-space:normal;max-width:none}header{display:block}.metrics{grid-template-columns:repeat(2,1fr)}.layout{grid-template-columns:1fr}.wq-grid{grid-template-columns:1fr}.workspace{grid-template-columns:1fr}.badges{justify-content:flex-start}.event-head{flex-wrap:wrap}.tools{justify-content:flex-start}}
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <main>
111
+ <header><div><div class="eyebrow" id="eyebrow">Local coordination</div><h1>Workspace viewer</h1></div><div class="muted"><span class="pulse"></span><span id="connection">Connecting</span>${input.shell ? '<a id="shell-link" href="../">Back to the Workspace viewer</a>' : ''}</div></header>
112
+ <a class="rl-back" id="whole-view" href="./" hidden>\u2190 Whole view</a>
113
+ <section class="metrics" id="metrics" aria-label="Repository summary"></section>
114
+ <div class="layout">
115
+ <section class="panel span" id="threads-section" aria-labelledby="threads-heading"><div class="wq-head"><h2 id="threads-heading">Threads</h2><a class="popout" href="?section=threads" target="_blank" rel="opener" aria-label="Open Threads in its own tab" title="Open Threads in its own tab">\u2197</a></div><div id="threads-line" class="muted"></div><div id="threads-error" class="error" role="alert"></div><div id="threads"></div></section>
116
+ <section class="panel span tg" id="tasks" aria-labelledby="tasks-heading" hidden><div class="wq-head"><h2 id="tasks-heading">Tasks</h2><a class="popout" href="?section=tasks" target="_blank" rel="opener" aria-label="Open Tasks in its own tab" title="Open Tasks in its own tab">\u2197</a></div><div id="tasks-line" class="muted"></div><div id="tasks-body"></div></section>
117
+ <section class="panel span tg" id="issues" aria-labelledby="issues-heading" hidden><div class="wq-head"><h2 id="issues-heading">Issues</h2><a class="popout" href="?section=issues" target="_blank" rel="opener" aria-label="Open Issues in its own tab" title="Open Issues in its own tab">\u2197</a></div><div id="issues-line" class="muted"></div><div class="issue-toolbar" id="issue-toolbar" hidden><div class="tb-row"><input id="issue-search" type="search" placeholder="Search issues" aria-label="Search issues" title="Search issues (/)" aria-keyshortcuts="/" autocomplete="off"><select id="issue-owner" aria-label="Filter issues by owner"></select></div><div class="tb-row chips"><div class="tb-row" role="group" aria-label="Urgency"><button type="button" class="pin chip" data-key="urgent" data-label="Urgent" title="Open issues seen on staging or production that are not flakes" aria-describedby="issue-urgent-why"></button><span class="sr-only" id="issue-urgent-why">Open issues seen on staging or production that are not flakes</span></div><div class="tb-row" role="group" aria-labelledby="issue-seen-label"><span class="muted" id="issue-seen-label">Seen on</span><button type="button" class="pin chip" data-key="local" data-label="Local"></button><button type="button" class="pin chip" data-key="staging" data-label="Staging"></button><button type="button" class="pin chip" data-key="production" data-label="Production"></button></div></div><div class="tb-row"><span id="issue-count" class="muted" role="status" aria-live="polite"></span><span id="issue-clear-row" class="tb-row" hidden><span class="muted" aria-hidden="true">\u00b7</span><button type="button" class="pin" id="issue-clear" aria-label="Clear search and filters">Clear</button></span></div></div><div id="issues-error" class="error" role="alert"></div><div id="issues-body"></div></section>
118
+ <section class="panel span rl" id="releases" aria-labelledby="releases-heading" hidden><h2 id="releases-heading" tabindex="-1">Releases</h2><div id="releases-status" class="muted" role="status"></div><div id="releases-body"></div></section>
119
+ <section class="panel span" id="queues" aria-labelledby="queues-heading" hidden><div class="wq-head"><h2 id="queues-heading">Work queues</h2><span id="queues-caption" class="muted"></span></div><div id="queues-status" class="muted"></div><div id="queues-body"></div></section>
120
+ <section class="panel"><h2>Workspace lineage</h2><div id="workspaces"></div></section>
121
+ <section class="panel"><h2>Authority &amp; resolver</h2><div id="authority"></div></section>
122
+ <section class="panel span"><h2>Integration activity</h2><div id="activity"></div></section>
123
+ <section class="panel span"><h2>Handoffs</h2><div id="handoffs"></div></section>
124
+ </div>
125
+ </main>
126
+ <script nonce="${input.nonce}">
127
+ const q=(id)=>document.getElementById(id);
128
+ // One section on its own (sp-viewer-section): ?section=threads|tasks|issues draws the header, a link
129
+ // back to the whole view and that panel alone (Issues with Releases, as sdlc view --section issues
130
+ // carries the Release block); any other section draws the whole view. The whole view names its
131
+ // browser tab, so a section page's task and issue ids open their subject there, keeping the section.
132
+ const SECTION=(()=>{const name=new URLSearchParams(location.search).get('section');return ['threads','tasks','issues'].includes(name)?name:null})();
133
+ if(SECTION){const label=SECTION[0].toUpperCase()+SECTION.slice(1);document.title=label+' \u00b7 SDLC Workspace Viewer';q('eyebrow').textContent='Local coordination \u00b7 '+label;q('whole-view').hidden=false;document.body.classList.add('one-section');
134
+ for(const id of SECTION==='threads'?['threads-section']:SECTION==='tasks'?['tasks']:['issues','releases'])q(id).classList.add('keep');
135
+ // A tab that was the whole view keeps its name; a section page drops it, so its ids never open in itself.
136
+ if(window.name==='sdlc-viewer')window.name=''}
137
+ else window.name='sdlc-viewer';
138
+ // Where a section page's ids open: the whole view's tab, unless this page shows the Releases panel.
139
+ const ELSEWHERE=SECTION==='threads'||SECTION==='tasks';
140
+ const node=(tag,cls,text)=>{const value=document.createElement(tag);if(cls)value.className=cls;if(text!==undefined)value.textContent=String(text);return value};
141
+ const short=(value)=>value?String(value).slice(0,8):'—';
142
+ const badge=(text,kind)=>node('span','badge '+(kind||''),text);
143
+ const empty=(text)=>node('div','empty',text);
144
+ function metric(label,value){const box=node('div','metric');box.append(node('span','',label),node('strong','',value));return box}
145
+ function render(data){
146
+ const workspaces=data.workspaces;const queues=data.integration_queues.flatMap((queue)=>queue.entries);const blocked=queues.filter((item)=>item.entry.status.startsWith('blocked')).length;
147
+ q('metrics').replaceChildren(metric('Workspaces',workspaces.length),metric('Active',workspaces.filter((item)=>item.workspace.status==='active').length),metric('Queued',queues.filter((item)=>item.entry.status==='queued').length),metric('Blocked',blocked));
148
+ const byParent=new Map();for(const item of workspaces){const key=item.workspace.source_workspace_id||'root';const list=byParent.get(key)||[];list.push(item);byParent.set(key,list)}
149
+ const tree=node('div');const seen=new Set();
150
+ function append(item,depth){if(seen.has(item.workspace.id))return;seen.add(item.workspace.id);const w=item.workspace;const card=node('article','workspace '+(w.id===data.current_workspace_id?'current ':'')+(w.status==='closed'?'closed':''));card.style.marginLeft=Math.min(depth*22,110)+'px';const title=node('div');title.append(node('strong','mono',(w.actor_kind==='human'?'Human':'AI')+' · '+short(w.id)),node('small','mono',w.git_worktree_path));const labels=node('div','badges');labels.append(badge(w.status,w.status==='active'?'good':''));if(item.projection.status==='available')labels.append(badge(item.projection.value.projection_dirty?'dirty':'clean',item.projection.value.projection_dirty?'warn':'good'));else labels.append(badge('unreadable','bad'));if(w.id===data.current_workspace_id)labels.append(badge('current','good'));card.append(title,labels);tree.append(card);for(const child of byParent.get(w.id)||[])append(child,depth+1)}
151
+ for(const root of byParent.get('root')||[])append(root,0);for(const item of workspaces)append(item,0);q('workspaces').replaceChildren(tree);
152
+ const auth=node('dl');const rows=[['Workspace',short(data.authority.workspace_id)],['Actor',data.authority.actor_kind],['Own head',data.authority.local_authority.own_sdlc_head],['Source',data.authority.local_authority.source_sdlc_head],['Git refs',data.authority.local_authority.git_refs],['Remotes',data.remotes.length],['Remote transport',data.authority.remote_authority.push],['Resolver',data.resolver.configured?(data.resolver.ready?'ready':'needs attention'):'not configured']];for(const [term,value] of rows)auth.append(node('dt','',term),node('dd','mono',value));q('authority').replaceChildren(auth);
153
+ const activity=node('div');for(const queue of data.integration_queues){for(const item of queue.entries){const row=node('div','event');const head=node('div','event-head');head.append(node('strong','',short(item.entry.child_workspace_id)+' → '+short(queue.coordinator_workspace_id)),badge(item.entry.status,item.entry.status==='integrated'?'good':item.entry.status==='queued'?'': 'bad'));row.append(head,node('div','muted',item.attempts.length+' merge attempt(s) · '+item.resolution_attempts.length+' resolver attempt(s)'));activity.append(row)}}for(const group of data.reconciliations){for(const item of group.attempts){const row=node('div','event');const head=node('div','event-head');head.append(node('strong','',short(item.attempt.source_workspace_id)+' ⇢ '+short(group.child_workspace_id)),badge(item.attempt.status,item.attempt.status==='reconciled'?'good':'warn'));row.append(head,node('div','muted','Source reconciliation · '+item.resolution_attempts.length+' resolver attempt(s)'));activity.append(row)}}if(!activity.childNodes.length)activity.append(empty('No integration or reconciliation attempts yet.'));q('activity').replaceChildren(activity);
154
+ const handoffs=node('div');for(const item of data.handoffs){const row=node('div','event');const head=node('div','event-head');head.append(node('strong','',short(item.handoff.child_workspace_id)+' → '+short(item.handoff.source_workspace_id)),badge(item.status,item.status==='ready'?'good':item.status==='applied'?'good':'warn'));row.append(head,node('div','muted','Pinned '+short(item.handoff.child_commit_id)+' onto '+short(item.handoff.source_commit_id)));if(item.error)row.append(node('div','error',item.error));handoffs.append(row)}if(!handoffs.childNodes.length)handoffs.append(empty('No Handoffs prepared yet.'));q('handoffs').replaceChildren(handoffs);
155
+ }
156
+ // A snapshot read can take minutes: the next refresh waits for the one asked, never piling up.
157
+ let snapshotAsked=false;
158
+ async function refresh(){if(snapshotAsked)return;snapshotAsked=true;try{const response=await fetch('./api/snapshot',{cache:'no-store'});if(!response.ok)throw new Error('HTTP '+response.status);render(await response.json());q('connection').textContent='Live · refreshes every 2s'}catch(error){q('connection').textContent='Disconnected';console.error(error)}finally{snapshotAsked=false}}
159
+ // A section page draws none of the snapshot's panels, so it does not read it.
160
+ if(!SECTION){refresh();setInterval(refresh,2000)}
161
+ // Threads: the same view sdlc view prints (terminal.js panelData), refreshed every 10s. The
162
+ // threads the user pinned lead under Pinned, as on every SDLC view; each row's Pin or Unpin
163
+ // posts to api/pin, which writes the store they all share.
164
+ const ROLES={ok:'good',you:'warn',alert:'bad',waiting:'info',blocked:'purple',muted:'',plain:''};
165
+ const ROLE=new Proxy(ROLES,{get:(map,key)=>Object.hasOwn(map,key)?map[key]:''});
166
+ // focusKey: the thread whose button takes a focus nobody holds, until its save settles.
167
+ // movedKey: the thread whose held button is scrolled into view, until the read after its save.
168
+ let lastThreads=null;let drawn='';let canPin=false;let pinning=null;let focusKey=null;let movedKey=null;
169
+ // A group's name is its label and count, as the sidebar says it; its hint is its heading's
170
+ // description, beside it, never part of its name.
171
+ function groupHeading(label,count,hint){const h=node('h3','group',label);h.append(node('span','count',count));h.setAttribute('aria-label',label+', '+count);if(!hint)return [h];const id='hint-'+label.toLowerCase().replace(/\\W+/g,'-');const s=node('span','sr-only',hint);s.id=id;h.setAttribute('aria-describedby',id);return [h,s]}
172
+ function pinButton(t){const b=node('button','pin'+(t.pinned?' on':''),t.pinned?'Unpin':'Pin');b.type='button';b.dataset.key=t.key;b.setAttribute('aria-label',t.pinned?'Unpin '+t.title:'Pin '+t.title+' to the top');if(pinning){b.setAttribute('aria-disabled','true');if(pinning===t.key)b.setAttribute('aria-busy','true')}b.addEventListener('click',()=>{if(!pinning)pin(t.key,!t.pinned)});return b}
173
+ async function pin(key,on){pinning=key;focusKey=key;movedKey=key;if(lastThreads)renderThreads(lastThreads);let failed='';try{const response=await fetch('./api/pin',{method:'POST',cache:'no-store',headers:{'Content-Type':'application/json'},body:JSON.stringify({key,on})});if(!response.ok)failed=await response.text()}catch(error){failed=error&&error.message?error.message:String(error)}pinning=null;q('threads-error').textContent=failed?'The pin was not saved: '+failed:'';
174
+ // The save settled: the button is no longer busy and shows what was saved, whether or not
175
+ // the read after it works.
176
+ if(lastThreads){if(!failed)lastThreads={...lastThreads,threads:lastThreads.threads.map((t)=>t.key===key?{...t,pinned:on}:t)};renderThreads(lastThreads)}await refreshThreads();
177
+ // The read that moves the row is done, unless another pin has started since.
178
+ if(movedKey===key)movedKey=null}
179
+ function renderThreads(data){
180
+ lastThreads=data;
181
+ // A refresh that changes nothing drawn leaves the list, and the focus in it, alone.
182
+ const state=JSON.stringify([data,canPin,pinning]);if(state===drawn)return;drawn=state;
183
+ q('threads-line').textContent=data.line;
184
+ const list=node('div');const pinned=data.pinnedCount||0;
185
+ data.threads.forEach((t,i)=>{if(pinned&&i===0)list.append(...groupHeading('Pinned',pinned,'Threads you pinned; they stay first in every order'));if(pinned&&i===pinned)list.append(...groupHeading('Other threads',data.threads.length-pinned));
186
+ const row=node('div','event');row.style.marginLeft=Math.min(t.depth*22,110)+'px';const head=node('div','event-head');const tools=node('div','tools');if(canPin&&t.key&&typeof t.pinned==='boolean')tools.append(pinButton(t));
187
+ // Landed work another thread's deploy carried to staging: its tag takes the status badge's
188
+ // place, then the release batch checking it in that batch's role; the tags' titles follow
189
+ // the summary as a line (carriedDetail), so the next step is read, not hovered.
190
+ if(t.carried){tools.append(badge(t.carried,'info'));if(t.carriedBatch)tools.append(badge(t.carriedBatch.text,ROLE[t.carriedBatch.role]))}else tools.append(badge(t.status,ROLE[t.role]));head.append(node('strong','',t.title),tools);row.append(head);
191
+ row.append(node('div','muted',[t.agent,t.phase,t.age,t.waitsOn?'waits on '+t.waitsOn:''].filter(Boolean).join(' · ')));
192
+ if(t.summary)row.append(node('div','thread-line muted',t.summary));if(t.carriedDetail)row.append(node('div','thread-line muted',t.carriedDetail));if(t.step)row.append(node('div','thread-line muted','step: '+t.step));
193
+ for(const l of t.lines)row.append(node('div','thread-line '+(ROLE[l.role]||''),(l.state==='working'?'▶ ':l.state==='prod'?'↑ ':l.state==='planned'?'○ ':'• ')+l.text));
194
+ for(const h of t.holds)row.append(node('div','thread-line '+(ROLE[h.role]||''),h.text));
195
+ if(t.plan)row.append(node('div','thread-line muted',t.plan));list.append(row)});
196
+ if(!data.threads.length)list.append(empty('No threads at work or waiting.'));
197
+ if(data.hidden)list.append(empty(data.hidden+' older thread(s) not shown; sdlc view --all lists them.'));
198
+ // A redraw replaces the buttons: the button that had the focus gives it to the same thread's
199
+ // new button without scrolling the page, except a pin's own button, whose thread may have
200
+ // moved, which is scrolled into view. With no focus held, a pin's button takes it while its
201
+ // save runs and when it settles; a later read never pulls the focus back.
202
+ const active=document.activeElement;const held=active&&active.matches&&active.matches('#threads button.pin')?active.dataset.key:null;
203
+ q('threads').replaceChildren(list);
204
+ const key=held||focusKey;
205
+ if(key){const again=[...list.querySelectorAll('button.pin')].find((b)=>b.dataset.key===key);if(again)again.focus(key===focusKey||key===movedKey?undefined:{preventScroll:true})}
206
+ if(!pinning)focusKey=null}
207
+ fetch('./api/pin',{method:'HEAD',cache:'no-store'}).then((response)=>{canPin=response.ok;if(canPin&&lastThreads)renderThreads(lastThreads)}).catch(()=>{});
208
+ async function refreshThreads(){try{const response=await fetch('./api/threads',{cache:'no-store'});if(response.status===404){const none='The threads view needs a SoftwareDLC checkout (tools/vscode-threads); this repository has none.';q('threads-line').textContent=none;q('threads').replaceChildren();drawn='';renderGroups({},none);if(SECTION)q('connection').textContent='Live \u00b7 refreshes every 10s';return}if(!response.ok)throw new Error(await response.text());const data=await response.json();renderThreads(data);renderGroups(data);
209
+ // A section page reads no snapshot, so its live line follows the threads read.
210
+ if(SECTION)q('connection').textContent='Live \u00b7 refreshes every 10s'}catch(error){const cause=error&&error.message?error.message:String(error);q('threads-line').textContent='Threads unavailable: '+cause;drawn='';if(SECTION)q('connection').textContent='Disconnected';
211
+ // A Tasks or Issues page with nothing drawn yet says why in its panel; one drawn keeps it.
212
+ if((SECTION==='tasks'||SECTION==='issues')&&!lastGroups)renderGroups({},SECTION[0].toUpperCase()+SECTION.slice(1)+' unavailable: '+cause)}}
213
+ // Tasks and Issues (sp-viewer-ti): the shared task and issue group views api/threads carries
214
+ // beside the threads (terminal.js panelData's groups), so a task or an issue reads as on every
215
+ // SDLC view. Each group is a disclosure, an h3 holding a button with aria-expanded, folded as the
216
+ // tab folds it; a fold the reader changes is kept across refreshes. Rows act only through their
217
+ // links: a web page cannot open a local session. Left out with no checkout, or view code without
218
+ // the groups; a failed read keeps them as last drawn.
219
+ const tgOpen=new Map();let tgDrawn='';
220
+ const tgTone=(tone)=>tone==='muted'||tone==='neutral'?'muted':ROLE[tone];
221
+ // Parts set apart by the gap alone run together for assistive technology, so a space separates them.
222
+ function tgRow(parts){const row=node('div','tg-row');parts.forEach((part,i)=>{if(i)row.append(node('span','sr-only','\u00a0'));row.append(part)});return row}
223
+ const tgSep=()=>node('span','muted','\u00b7');
224
+ // A link opens its subject in the Releases panel; while that panel is off, the id is text.
225
+ let rlOn=false;const tgLinks=()=>ELSEWHERE?rlOn:!q('releases').hidden;
226
+ function tgLink(text,hash){if(!tgLinks())return node('span','id',text);const a=node('a','',text);a.href=(ELSEWHERE?'./':'')+hash;if(ELSEWHERE){a.target='sdlc-viewer';a.title="Opens in the viewer's tab"}return a}
227
+ function tgMarks(marks,before){return marks.filter((m)=>Boolean(m.before)===before)}
228
+ // An id that names a task page links to it; a wait's id (a queue or hold Needs you lists) has
229
+ // none, so it is muted text, as sdlc view prints it.
230
+ function taskRow(r){const parts=[RL_REF.task.test(r.id)?tgLink(r.id,'#task='+encodeURIComponent(r.id)):node('span','muted id',r.id)];for(const m of tgMarks(r.marks,true))parts.push(node('span',tgTone(m.tone),m.word));parts.push(node('span','title',r.title));
231
+ if(r.owner)parts.push(tgSep(),node('span','muted',r.owner));if(r.detail)parts.push(tgSep(),node('span',tgTone(r.tone),r.detail));
232
+ for(const m of tgMarks(r.marks,false))parts.push(tgSep(),node('span',tgTone(m.tone),m.word));if(r.after&&r.after.length)parts.push(tgSep(),node('span','muted','after '+r.after.join(', ')));return tgRow(parts)}
233
+ // While Urgent narrows, every row it keeps reads 'urgent', as on the VS Code tab: the group view
234
+ // marks only Unowned's urgent rows.
235
+ function issueRow(r){const parts=[tgLink(r.issue,'#issue='+encodeURIComponent(r.issue))];const before=tgMarks(r.marks,true);
236
+ if(issueFilter.urgent&&(r.match||{}).urgentAny&&!before.some((m)=>m.word==='urgent'))before.unshift({word:'urgent',tone:'alert',before:true});for(const m of before)parts.push(node('span',tgTone(m.tone),m.word));parts.push(node('span','title',r.title));
237
+ if(r.detail)parts.push(tgSep(),node('span','muted',r.detail));return tgRow(parts)}
238
+ function tgGroup(kind,g,row){const key=kind+':'+g.key;const open=tgOpen.has(key)?tgOpen.get(key):!g.folded;const h=node('h3','group');const b=node('button','fold');b.type='button';b.dataset.key=key;b.setAttribute('aria-expanded',String(open));b.setAttribute('aria-label',g.label+', '+g.count);
239
+ const mark=node('span','',open?'\u25be':'\u25b8');mark.setAttribute('aria-hidden','true');b.append(mark,node('span','',g.label),node('span','count',g.count));b.addEventListener('click',()=>{tgOpen.set(key,!open);if(lastGroups)renderGroups(lastGroups)});h.append(b);
240
+ const out=[h];if(open){for(const r of g.rows)out.push(row(r));if(g.hidden)out.push(node('div','empty more','+'+g.hidden+' more; '+(g.more||'sdlc view --all lists them.')))}return out}
241
+ // A redraw gives the focus a fold button or link had to the same one in the new list.
242
+ function tgDraw(id,out){const body=q(id);const active=document.activeElement;const inside=active&&body.contains(active);
243
+ const held=inside?(active.dataset&&active.dataset.key?{key:active.dataset.key}:active.tagName==='A'?{href:active.getAttribute('href')}:null):null;body.replaceChildren(...out);
244
+ if(held){const again=held.key?body.querySelector('button.fold[data-key="'+held.key+'"]'):[...body.querySelectorAll('a')].find((a)=>a.getAttribute('href')===held.href);if(again)again.focus({preventScroll:true})}}
245
+ let lastGroups=null;
246
+ // Issue search (is-viewer): the VS Code Issues tab's toolbar and rule, which the spec holds to
247
+ // model.issueMatches on the desktop pane's cases (plugins/sdlc-view/hooks/issues.fixture.ts): every
248
+ // word of the search, ignoring case, in the ID, title, what happens, 'you reported', the caption,
249
+ // the owner and 'seen on <env>'; the owner exact, or 'none'; Urgent and Seen on narrow each other,
250
+ // and the Seen on buttons widen among themselves. The controls are built once, in the page, so a
251
+ // refresh never takes the typing or the focus; the search lasts while the page is open.
252
+ const SEEN=['local','staging','production'];
253
+ let issueFilter={search:'',owner:'any',urgent:false,seen:[]};
254
+ const issueText=(i)=>{const m=i.match||{};return [i.issue,i.title,m.actual,m.yours&&'you reported',i.detail,m.owner,m.seen&&'seen on '+m.seen].filter(Boolean).join(' ').toLowerCase()};
255
+ function issueBase(i){const words=issueFilter.search.toLowerCase().split(/\\s+/).filter(Boolean);if(words.length&&!words.every((w)=>issueText(i).includes(w)))return false;
256
+ const by=(i.match||{}).owner;if(issueFilter.owner==='none')return !by;return issueFilter.owner==='any'||by===issueFilter.owner}
257
+ const passUrgent=(i)=>!issueFilter.urgent||Boolean((i.match||{}).urgentAny);
258
+ const passSeen=(i)=>!issueFilter.seen.length||issueFilter.seen.includes((i.match||{}).seen||'');
259
+ const issuePasses=(i)=>issueBase(i)&&passUrgent(i)&&passSeen(i);
260
+ const issueNarrowed=()=>Boolean(issueFilter.search.trim()||issueFilter.urgent||issueFilter.seen.length||issueFilter.owner!=='any');
261
+ // The toolbar's counts and owners for the issues listed: each button reads what it would show
262
+ // given the search, the owner and the other buttons; a chosen owner with no issues stays listed.
263
+ function drawToolbar(all){
264
+ const count=(key)=>key==='urgent'?all.filter((i)=>issueBase(i)&&passSeen(i)&&(i.match||{}).urgentAny).length:all.filter((i)=>issueBase(i)&&passUrgent(i)&&(i.match||{}).seen===key).length;
265
+ for(const b of q('issue-toolbar').querySelectorAll('button.chip')){const key=b.dataset.key;const on=key==='urgent'?issueFilter.urgent:issueFilter.seen.includes(key);const n=count(key);
266
+ const mark=node('span','',on?'\u2713 ':'');mark.setAttribute('aria-hidden','true');b.replaceChildren(mark,document.createTextNode(b.dataset.label+' '+n));
267
+ b.classList.toggle('on',on);b.setAttribute('aria-pressed',String(on));if(!on&&n===0)b.setAttribute('aria-disabled','true');else b.removeAttribute('aria-disabled')}
268
+ const owners=new Map();for(const i of all){const by=(i.match||{}).owner;if(by)owners.set(by,(owners.get(by)||0)+1)}
269
+ if(issueFilter.owner!=='any'&&issueFilter.owner!=='none'&&!owners.has(issueFilter.owner))owners.set(issueFilter.owner,0);
270
+ const options=[['any','Any owner'],['none','No owner ('+all.filter((i)=>!(i.match||{}).owner).length+')']].concat([...owners].sort((a,b)=>b[1]-a[1]||a[0].localeCompare(b[0])).map(([name,n])=>[name,name+' ('+n+')']));
271
+ const select=q('issue-owner');const was=JSON.stringify([...select.options].map((o)=>[o.value,o.textContent]));
272
+ if(was!==JSON.stringify(options)){select.replaceChildren(...options.map(([value,label])=>{const o=node('option','',label);o.value=value;return o}))}
273
+ select.value=issueFilter.owner}
274
+ function searchAgain(){tgDrawn='';if(lastGroups)renderGroups(lastGroups)}
275
+ q('issue-search').addEventListener('input',(event)=>{issueFilter={...issueFilter,search:event.target.value};searchAgain()});
276
+ q('issue-owner').addEventListener('change',(event)=>{issueFilter={...issueFilter,owner:event.target.value};searchAgain()});
277
+ for(const b of q('issue-toolbar').querySelectorAll('button.chip'))b.addEventListener('click',()=>{if(b.getAttribute('aria-disabled')==='true')return;const key=b.dataset.key;
278
+ issueFilter=key==='urgent'?{...issueFilter,urgent:!issueFilter.urgent}:{...issueFilter,seen:issueFilter.seen.includes(key)?issueFilter.seen.filter((s)=>s!==key):[...issueFilter.seen,key]};searchAgain()});
279
+ // Clear takes the line it sits in away, so the focus goes to the search.
280
+ q('issue-clear').addEventListener('click',()=>{issueFilter={search:'',owner:'any',urgent:false,seen:[]};q('issue-search').value='';searchAgain();q('issue-search').focus()});
281
+ // '/' focuses the search, as on VS Code's Issues tab: not with a modifier, not from a field, and
282
+ // only while the toolbar is shown, so the browser keeps its own '/' otherwise.
283
+ document.addEventListener('keydown',(event)=>{if(event.key!=='/'||event.ctrlKey||event.metaKey||event.altKey)return;const at=document.activeElement;
284
+ if(at&&(at.matches('input,textarea,select')||at.isContentEditable))return;if(q('issue-toolbar').hidden||q('issues').hidden||getComputedStyle(q('issues')).display==='none')return;
285
+ event.preventDefault();q('issue-search').focus();q('issue-search').select()});
286
+ // why: what the Threads line says when there is no answer; on a Tasks or Issues page, whose Threads
287
+ // line is not shown, the panel stays and says it in place of its groups.
288
+ function renderGroups(data,why){const has=Boolean(data&&data.tasks&&data.issues);q('tasks').hidden=!has;q('issues').hidden=!has;
289
+ if(!has){lastGroups=null;tgDrawn='';if(SECTION==='tasks'||SECTION==='issues'){q(SECTION).hidden=false;q(SECTION+'-line').textContent='';const error=q(SECTION+'-error');if(error)error.textContent='';
290
+ q(SECTION+'-body').replaceChildren(empty(why||'This view code has no '+SECTION[0].toUpperCase()+SECTION.slice(1)+' panel; update the SoftwareDLC checkout.'))}return}lastGroups=data;
291
+ // A refresh that changes nothing drawn leaves the panels, and the focus in them, alone.
292
+ const state=JSON.stringify([data.tasks,data.issues,[...tgOpen],tgLinks(),issueFilter]);if(state===tgDrawn)return;tgDrawn=state;
293
+ const tasks=data.tasks;q('tasks-line').textContent=tasks.open?tasks.open+' open':'';
294
+ tgDraw('tasks-body',tasks.groups.length?tasks.groups.flatMap((g)=>tgGroup('task',g,taskRow)):[empty('No tasks recorded yet.')]);
295
+ // An unreadable store: its error, kept in place and changed only when its words do, so a
296
+ // refresh does not announce it again; no count and no groups beside it.
297
+ const issues=data.issues;const error=issues.error!==undefined?'Could not read issues: '+issues.error:'';if(q('issues-error').textContent!==error)q('issues-error').textContent=error;
298
+ q('issues-line').textContent=!error&&issues.open?issues.open+' open':'';
299
+ // View code that gives every row and how many a group shows can be searched; older code's rows
300
+ // are already cut, so it gets no toolbar.
301
+ const all=error?[]:issues.groups.flatMap((g)=>g.rows);const searchable=!error&&typeof issues.shown==='number'&&all.length>0;
302
+ q('issue-toolbar').hidden=!searchable;const narrowed=searchable&&issueNarrowed();if(searchable)drawToolbar(all);
303
+ const matched=narrowed?issues.groups.map((g)=>({...g,rows:g.rows.filter(issuePasses)})).filter((g)=>g.rows.length):[];
304
+ const words=narrowed?matched.reduce((n,g)=>n+g.rows.length,0)+' of '+all.length+' issues':'';if(q('issue-count').textContent!==words)q('issue-count').textContent=words;q('issue-clear-row').hidden=!narrowed;
305
+ // Narrowed: each group with a match under its heading, every match; otherwise each group folds and
306
+ // lists the rows it shows.
307
+ const cap=typeof issues.shown==='number'?issues.shown:Infinity;
308
+ tgDraw('issues-body',error?[]:narrowed?(matched.length?matched.flatMap((g)=>[...groupHeading(g.label,g.rows.length),...g.rows.map(issueRow)]):[empty('No issues match.')])
309
+ :(issues.open?[]:[empty('No open issues.')]).concat(issues.groups.flatMap((g)=>tgGroup('issue',{...g,rows:g.rows.slice(0,cap)},issueRow))))}
310
+ refreshThreads();setInterval(refreshThreads,10000);
311
+ // Releases (rl-ui-viewer): release links in sdlc view's words (terminal.js releasePanelData, read
312
+ // through api/release-links). The address's hash names the subject, so Back and a reload return to
313
+ // it; the summary is read again every five minutes. The page adds only its chrome.
314
+ const RL_REF={release:/^(candidate|v?\\d{1,4}\\.\\d{1,4}\\.\\d{1,6}(?:-[0-9A-Za-z.]{1,40})?|[a-z][a-z0-9-]{0,63})$/,planned:/^([a-z0-9][a-z0-9-]{0,63})?$/i,issue:/^iss-\\d{1,9}$/,task:/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/};
315
+ // rlMove: a move whose read has not drawn yet, so the read that draws (even a later one) moves the focus.
316
+ // rlAfter: the section heading a section's Try again came from, which takes the focus once it redraws.
317
+ let rlSeq=0;let rlShown=null;let rlAnswer=null;let rlDrawn='';const rlOpen=new Set();let rlLookup=null;let rlMove=false;let rlAfter=null;
318
+ // A task page is read from task_page.py, not the release links (tg-task-page-viewer).
319
+ const rlReading=(route)=>route&&route.kind==='task'?'Reading the task\\u2026':'Reading release links\\u2026';
320
+ // The subject the hash names, its query, or the hash itself when it names none.
321
+ function rlRoute(){let h='';try{h=decodeURIComponent(location.hash.slice(1))}catch{return{refused:location.hash}}
322
+ if(!h)return{kind:'summary',q:''};if(h==='releases')return{kind:'releases',q:'releases'};
323
+ const m=/^(release|planned|issue|task)(?:=(.*))?$/.exec(h);if(!m)return{refused:location.hash};
324
+ const kind=m[1];const ref=m[2]===undefined?'':m[2];
325
+ if(!RL_REF[kind].test(ref)||(kind!=='planned'&&!ref))return{refused:location.hash};
326
+ return{kind,ref,q:m[2]===undefined?kind:kind+'='+encodeURIComponent(ref)}}
327
+ // A thread part (a Planned Release's Serving threads) links to its row in the shell's Threads;
328
+ // with no shell beside this page it stays text, since this page's hash names the Releases subject.
329
+ const rlTo=(p)=>p.link||(p.thread&&q('shell-link')?'../threads#thread-'+encodeURIComponent(p.thread):'');
330
+ const rlIndent=(b)=>b.indent===2?'in2':b.indent?'in1':'';
331
+ function rlPart(p){const to=rlTo(p);const el=to?node('a','',p.text):node(p.code?'code':'span','',p.text);if(to)el.href=to;
332
+ const cls=[p.tone==='muted'?'muted':p.tone==='alert'?'error':p.tone==='ok'?'good':p.tone==='you'?'warn':'',p.small?'small':'',p.strong?'strong':'',p.clip?'clip':'',p.tight?'tight':'',p.below?'below':''].filter(Boolean).join(' ');if(cls)el.className=cls;
333
+ if(p.title)el.title=p.title;if(p.note)el.append(node('span','sr-only',' ('+p.note+')'));return el}
334
+ // spaced: a space between parts set apart by the gap alone, so assistive technology reads them apart.
335
+ function rlRow(parts,cls,spaced){const row=node('div','rl-row'+(cls?' '+cls:''));parts.forEach((p,i)=>{if(spaced&&i)row.append(node('span','sr-only','\\u00a0'));row.append(rlPart(p))});return row}
336
+ function rlBack(){const a=node('a','rl-back','\\u2190 Releases');a.href='#';return a}
337
+ // A list shows its first cap rows and a Show all N button, which shows the rest in place; the
338
+ // button's name adds the list it opens (context), after its visible words.
339
+ function rlList(list,key,cls,context){const out=[];const all=!list.cap||rlOpen.has(key)||list.items.length<=list.cap;
340
+ for(const parts of all?list.items:list.items.slice(0,list.cap))out.push(rlRow(parts,cls,list.spaced));
341
+ // Shown, the first row it revealed takes the focus the button had.
342
+ if(!all){const b=node('button','pin','Show all '+list.items.length);b.type='button';if(context)b.setAttribute('aria-label','Show all '+list.items.length+' \\u00b7 '+context);b.addEventListener('click',()=>{rlOpen.add(key);rlDraw(true);const row=document.querySelectorAll('#releases-body [data-list="'+key+'"]')[list.cap];if(row){row.tabIndex=-1;row.focus()}});const wrap=node('div','rl-row '+cls);wrap.append(b);out.push(wrap)}
343
+ for(const el of out)el.dataset.list=key;return out}
344
+ const rlWords=(parts)=>parts.map((p)=>p.text).join(' ');
345
+ // The issue lookup, kept across redraws so a half-typed id survives a refresh.
346
+ function rlLookupForm(){if(rlLookup)return rlLookup;const box=node('div');const row=node('div','rl-actions');const all=node('a','','All releases');all.href='#releases';
347
+ const form=node('form');form.noValidate=true;const label=node('label','','Issue');label.htmlFor='rl-issue';const input=node('input');input.id='rl-issue';input.placeholder='iss-12';input.autocomplete='off';input.setAttribute('aria-label','Issue id');input.setAttribute('aria-describedby','rl-lookup-error');
348
+ const show=node('button','pin','Show');show.type='submit';const error=node('div','error');error.id='rl-lookup-error';error.setAttribute('role','alert');
349
+ form.append(label,input,show);row.append(all,form);box.append(row,error);
350
+ form.addEventListener('submit',(event)=>{event.preventDefault();const id=input.value.trim();if(RL_REF.issue.test(id)){error.textContent='';input.removeAttribute('aria-invalid');location.hash='issue='+id}else{error.textContent='Enter an issue id, such as iss-12.';input.setAttribute('aria-invalid','true');input.focus()}});
351
+ input.addEventListener('input',()=>{if(error.textContent){error.textContent='';input.removeAttribute('aria-invalid')}});return rlLookup=box}
352
+ function rlSummary(panel){const out=[];if(panel.rows.length){const dl=node('dl','rl-terms');for(const r of panel.rows){const dd=node('dd');dd.append(rlRow(r.parts));dl.append(node('dt','',r.term),dd)}out.push(dl)}out.push(rlLookupForm());return out}
353
+ function rlSubject(panel){const out=[rlBack()];
354
+ // Words set apart by margins alone run together for assistive technology, so a space separates them.
355
+ if(panel.title){const h=node('h3');h.tabIndex=-1;if(panel.title.id){h.append(node('code','',panel.title.id),document.createTextNode(' '))}h.append(document.createTextNode(panel.title.text));
356
+ // A task's state, release and thread take their own row under its title, as the terminal prints
357
+ // them, its state keeping the body size it asks for; other subjects' parts are captions in the h3.
358
+ const head=panel.kind==='task'?node('div','rl-row rl-meta'):h;
359
+ for(const p of panel.title.parts||[])head.append(document.createTextNode(' '),rlPart(head!==h?{small:true,...p}:{...p,small:true}));out.push(h);if(head!==h&&head.childNodes.length)out.push(head)}
360
+ // A list's Show all is kept under its subject and its place among the lists, so a block without
361
+ // lists that comes or goes (the readiness line), or lists with their own key (Known issues), leave
362
+ // the others' keys as they were. section: the heading's own words, which a section's Try again
363
+ // finds again to give it the focus.
364
+ let heading='';let section='';let n=0;
365
+ panel.blocks.forEach((b)=>{const i=b.lists?(b.key||n++):'';if(b.heading){section=b.heading;heading=b.heading+(typeof b.count==='number'?' \\u00b7 '+b.count:'');const h=node('h4','',b.heading);if(b.count!==undefined)h.append(document.createTextNode(' '),node('span','count',b.count));for(const p of b.parts||[])h.append(document.createTextNode(' '),rlPart(p));out.push(h)}
366
+ // A section that could not be read: Try again reads the subject again, saying so in its place,
367
+ // and the section's heading takes the focus once it redraws. Whichever read answers next draws,
368
+ // even an answer equal to the last drawn, since the section no longer shows it.
369
+ else if(b.retry){const at=section;const retry=node('button','pin','Try again');retry.type='button';const wrap=node('div','rl-row'+(b.indent?' in1':''));wrap.append(retry);
370
+ retry.addEventListener('click',()=>{rlAfter=at;rlDrawn='';const said=node('span','muted',rlReading(rlShown));said.tabIndex=-1;wrap.replaceChildren(said);said.focus();rlLoad(false,true)});out.push(wrap)}
371
+ else if(b.group){out.push(rlRow(b.group,'in1 rl-head'));b.lists.forEach((l,j)=>out.push(...rlList(l,rlShown.q+':'+i+':'+j,'in2',rlWords(b.group))))}
372
+ else if(b.lists){b.lists.forEach((l,j)=>out.push(...rlList(l,rlShown.q+':'+i+':'+j,rlIndent(b),heading)))}
373
+ else if(b.line)out.push(rlRow(b.line,rlIndent(b),b.spaced))});return out}
374
+ // A line that says what came of a move (an unknown subject, an address that is not one): it takes
375
+ // the focus, so it is what is read.
376
+ function rlSaid(text){const line=node('div','muted rl-said',text);line.tabIndex=-1;return line}
377
+ // Where the focus goes after a move or a retry: the subject's h3, else what was said, else Try
378
+ // again, else the panel's heading.
379
+ function rlFocus(){const body=q('releases-body');const after=rlAfter;rlAfter=null;
380
+ const section=after?[...body.querySelectorAll('h4')].find((h)=>h.firstChild&&h.firstChild.textContent===after):null;if(section){section.tabIndex=-1;section.focus();return}
381
+ (body.querySelector('h3')||body.querySelector('.rl-said')||body.querySelector('.rl-retry')||q('releases-heading')).focus()}
382
+ // Draws the last answer; a refresh that changes nothing leaves the panel, and the focus in it, alone.
383
+ function rlDraw(force){const route=rlShown;const answer=rlAnswer;const state=JSON.stringify([route,answer,[...rlOpen]]);if(!force&&state===rlDrawn)return false;rlDrawn=state;
384
+ const body=q('releases-body');let out;
385
+ // A redraw keeps the focus where it was: on the same link or button (found again by its address,
386
+ // words, and place among those that share them), on the subject's h3 or the said line, or in the
387
+ // lookup field, which is kept across redraws and given its focus back as it is.
388
+ const active=document.activeElement;const inside=active&&body.contains(active);
389
+ const same=(el)=>(x)=>x.tagName===el.tagName&&x.getAttribute('href')===el.getAttribute('href')&&x.textContent===el.textContent;
390
+ const held=inside&&active.matches('a,button')?{el:active,at:[...body.querySelectorAll(active.tagName)].filter(same(active)).indexOf(active)}:null;
391
+ const spot=inside&&active.matches('h3,.rl-said')?(active.matches('h3')?'h3':'.rl-said'):null;const typing=active&&active.id==='rl-issue'?active:null;
392
+ if(route.refused)out=[rlBack(),rlSaid('Not a release, Planned Release, issue or task: '+route.refused)];
393
+ else if(answer.unknown)out=[rlBack(),rlSaid(answer.unknown)];
394
+ else if(answer.error!==undefined){const line=node('div','error',(route.kind==='task'?'The task could not be read: ':'Release links could not be read: ')+answer.error);line.setAttribute('role','alert');const retry=node('button','pin rl-retry','Try again');retry.type='button';retry.addEventListener('click',()=>rlLoad(false,true));const wrap=node('div','rl-row');wrap.append(retry);out=(route.kind==='summary'?[]:[rlBack()]).concat(line,wrap)}
395
+ else out=route.kind==='summary'?rlSummary(answer.panel):rlSubject(answer.panel);
396
+ body.replaceChildren(...out);
397
+ if(held){const again=[...body.querySelectorAll(held.el.tagName)].filter(same(held.el))[held.at];if(again)again.focus({preventScroll:true})}
398
+ if(spot){const again=body.querySelector(spot);if(again)again.focus({preventScroll:true})}
399
+ if(typing&&body.contains(typing))typing.focus({preventScroll:true});
400
+ return true}
401
+ // Reads the subject the hash names. moved: the reader moved to it, so the focus goes with them;
402
+ // retry: Try again, which says it is reading and keeps the focus in the panel.
403
+ async function rlLoad(moved,retry){const route=rlRoute();const seq=++rlSeq;if(moved||retry)rlMove=true;
404
+ // A move drops a section retry's focus: its heading belongs to the page it left.
405
+ if(moved)rlAfter=null;
406
+ // The same address shows the same thing: one the server refused stays the refused view.
407
+ const first=rlShown===null||(JSON.stringify(route)!==JSON.stringify(rlShown)&&!(rlShown.refused&&rlShown.refused===location.hash));
408
+ if(route.refused){rlShown=route;rlAnswer={};q('releases-status').textContent='';const drew=rlDraw(first);if(rlMove){rlMove=false;if(drew||moved)rlFocus()}return}
409
+ // A cleared panel has nothing drawn: the next answer draws, even one equal to the last drawn.
410
+ if(first||retry)q('releases-status').textContent=rlReading(route);
411
+ if(first&&(rlShown===null||rlShown.kind!==route.kind||moved)){q('releases-body').replaceChildren();rlDrawn=''}
412
+ let answer;try{const response=await fetch('./api/release-links'+(route.q?'?'+route.q:''),{cache:'no-store'});
413
+ if(response.status===404){q('releases').hidden=true;if(lastGroups)renderGroups(lastGroups);return}
414
+ if(response.status===400)answer={refused:true};else if(!response.ok)answer={error:await response.text()};else answer=await response.json()}
415
+ catch(error){answer={error:error&&error.message?error.message:String(error)}}
416
+ if(seq!==rlSeq)return;q('releases-status').textContent='';
417
+ rlShown=answer.refused?{refused:location.hash}:route;rlAnswer=answer.refused?{}:answer;
418
+ // The read that answers takes up a pending move or retry: it moves the focus when it drew or was
419
+ // itself the move or retry, and otherwise drops it, so a later refresh never moves the focus.
420
+ const drew=rlDraw(first||retry);if(rlMove){rlMove=false;if(drew||moved||retry)rlFocus()}}
421
+ fetch('./api/release-links',{method:'HEAD',cache:'no-store'}).then((response)=>{if(!response.ok)return;rlOn=true;
422
+ // A Threads or Tasks page shows no Releases panel: the HEAD only lets its ids link to the whole view.
423
+ if(ELSEWHERE){if(lastGroups)renderGroups(lastGroups);return}
424
+ q('releases').hidden=false;if(lastGroups)renderGroups(lastGroups);rlLoad(false);
425
+ window.addEventListener('hashchange',()=>rlLoad(true));setInterval(()=>rlLoad(false),300000)}).catch(()=>{});
426
+ // Work queues (qv-viewer): the Process tab's cards in its words (terminal.js queuePanelData, read
427
+ // through api/queues every 15 s), each turn as a badge in its role. Thread titles are plain text:
428
+ // the viewer has no sidebar to select a thread in. A failed read keeps the cards drawn. A staging
429
+ // entry whose deploy carries member work ends its detail with what it serves (plan fea8cf22).
430
+ let wqDrawn='';
431
+ function wqCard(c){const card=node('section','wq-card');card.setAttribute('aria-label',c.name);card.append(node('h3','',c.name),node('p','summary '+ROLE[c.role],c.summary));
432
+ if(c.alert){const a=node('div','wq-alert '+(ROLE[c.alert.role]||'bad'));a.setAttribute('role','note');a.append(node('strong','',c.alert.title));if(c.alert.text)a.append(node('div','',c.alert.text));card.append(a)}
433
+ for(const t of c.captions)card.append(node('p','muted',t));if(c.empty)card.append(node('p','muted',c.empty));
434
+ if(c.rows.length){const list=node('ol','wq-rows');for(const r of c.rows){const row=node('li','wq-row');const body=node('div');body.append(node('div','title',r.title));const detail=node('div','detail',r.serves?r.detail+' \\u00b7 '+r.serves:r.detail);if(r.estimateTitle){detail.title=r.estimateTitle;detail.setAttribute('aria-description',r.estimateTitle)}body.append(detail);
435
+ row.append(node('span','pos',r.position),body,badge(r.turn,ROLE[r.role]));list.append(row)}card.append(list)}return card}
436
+ function wqDraw(data){const state=JSON.stringify(data);if(state===wqDrawn)return;wqDrawn=state;q('queues-caption').textContent=data.caption;
437
+ if(data.state==='unreadable'){const line=node('div','error','Queue records could not be read: '+data.error);line.setAttribute('role','alert');q('queues-body').replaceChildren(line);return}
438
+ const grid=node('div','wq-grid');for(const c of data.cards)grid.append(wqCard(c));q('queues-body').replaceChildren(grid)}
439
+ async function refreshQueues(){try{const response=await fetch('./api/queues',{cache:'no-store'});if(response.status===404){q('queues').hidden=true;return false}if(!response.ok)throw new Error(await response.text());
440
+ const data=await response.json();q('queues').hidden=false;q('queues-status').textContent='';wqDraw(data);return true}
441
+ catch(error){q('queues').hidden=false;q('queues-status').textContent='Work queues unavailable: '+(error&&error.message?error.message:error);return true}}
442
+ // Every 15 s while the page is visible (a hidden tab asks for nothing, as each read blocks the
443
+ // viewer for about two seconds), and at once when it is shown again.
444
+ if(!SECTION)refreshQueues().then((on)=>{if(!on)return;setInterval(()=>{if(document.visibilityState!=='hidden')refreshQueues()},15000);
445
+ document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')refreshQueues()})});
446
+ </script>
447
+ </body>
448
+ </html>`;
449
+ }
450
+ /**
451
+ * The app shell's addresses (la-shell), which the bundle's index.html answers so a page can be
452
+ * reloaded and shared within one sdlc viewer run: '<token>/', the organization's pages
453
+ * (org/<org>, org/<org>/threads, …/queues, …/workspaces, …/worktrees, …/chart, …/tasks and …/tasks/<task>) and a project's
454
+ * (proj/<project>, proj/<project>/tasks, …/issues, …/releases), plus the earlier one-segment
455
+ * sections, which the shell forwards to their place. Every other address, the hosted-only ones
456
+ * included, is 404 on GET and HEAD alike. An unknown slug in an allowed shape serves the page,
457
+ * whose reads answer 404 not_found.
458
+ */
459
+ const ORG_SEGMENT = /^[a-z0-9][a-z0-9-]{0,63}$/;
460
+ // A project segment is a member name as folder.py writes it (folder-members.ts NAME).
461
+ const PROJECT_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
462
+ const ORG_PAGES = new Set(['threads', 'queues', 'workspaces', 'worktrees', 'chart', 'tasks']);
463
+ const PROJECT_PAGES = new Set(['tasks', 'issues', 'releases']);
464
+ // chart is no earlier section, but the nav links to it before the organization reads.
465
+ const EARLIER_SECTIONS = new Set([
466
+ 'threads',
467
+ 'tasks',
468
+ 'issues',
469
+ 'releases',
470
+ 'queues',
471
+ 'workspaces',
472
+ 'worktrees',
473
+ 'chart',
474
+ 'app-ui-probe'
475
+ ]);
476
+ /** Whether a path under the token is one of the shell's addresses. */
477
+ export function isShellAddress(rest) {
478
+ if (rest === '')
479
+ return true;
480
+ const parts = rest.split('/');
481
+ if (parts.length === 1)
482
+ return EARLIER_SECTIONS.has(parts[0]);
483
+ const [kind, slug, page, ...more] = parts;
484
+ // One task's page (org/<org>/tasks/<task>), the only address deeper than a page.
485
+ if (kind === 'org' && page === 'tasks' && more.length === 1)
486
+ return ORG_SEGMENT.test(slug) && APP_SEGMENT.test(more[0]);
487
+ if (more.length > 0)
488
+ return false;
489
+ if (kind === 'org')
490
+ return ORG_SEGMENT.test(slug) && (page === undefined || ORG_PAGES.has(page));
491
+ if (kind === 'proj')
492
+ return PROJECT_SEGMENT.test(slug) && (page === undefined || PROJECT_PAGES.has(page));
493
+ return false;
494
+ }
495
+ // A path segment under api/app/ (docs/specs/app-ui-contracts.md), decoded: a task id, an issue id,
496
+ // a version or a word, never a separator, an option or a leading dot.
497
+ const APP_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
498
+ // A bundle file's name: Vite's hashed names, never a separator or a leading dot.
499
+ const ASSET_NAME = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,127}$/;
500
+ const ASSET_TYPES = {
501
+ '.js': 'text/javascript; charset=utf-8',
502
+ '.css': 'text/css; charset=utf-8',
503
+ '.svg': 'image/svg+xml',
504
+ '.png': 'image/png',
505
+ '.woff2': 'font/woff2',
506
+ '.map': 'application/json; charset=utf-8'
507
+ };
508
+ /** Where the sdlc package ships the React viewer's built files: viewer/ beside dist/. */
509
+ export function defaultViewerBundle() {
510
+ return fileURLToPath(new URL('../../viewer/', import.meta.url));
511
+ }
512
+ /** The bundle's index.html, or null when the directory holds no built viewer. */
513
+ async function readBundleIndex(bundle) {
514
+ if (!bundle)
515
+ return null;
516
+ try {
517
+ return await readFile(join(bundle, 'index.html'), 'utf8');
518
+ }
519
+ catch {
520
+ return null;
521
+ }
522
+ }
523
+ function secureHeaders(input) {
524
+ input.response.setHeader('Content-Type', input.content_type);
525
+ input.response.setHeader('Cache-Control', 'no-store');
526
+ input.response.setHeader('X-Content-Type-Options', 'nosniff');
527
+ input.response.setHeader('X-Frame-Options', 'DENY');
528
+ input.response.setHeader('Referrer-Policy', 'no-referrer');
529
+ const sources = input.bundle
530
+ ? `script-src 'self'; style-src 'self'`
531
+ : `script-src 'nonce-${input.nonce}'; style-src 'nonce-${input.nonce}'`;
532
+ // The React shell's page carries a base of its own token path; nothing else may set one.
533
+ const base = input.bundle ? `base-uri 'self'` : `base-uri 'none'`;
534
+ input.response.setHeader('Content-Security-Policy', `default-src 'none'; ${sources}; connect-src 'self'; img-src 'self' data:; font-src 'self'; ${base}; form-action 'none'; frame-ancestors 'none'`);
535
+ }
536
+ // The request body as text, refused (rejected) past `limit` bytes.
537
+ function readBody(request, limit) {
538
+ return new Promise((resolve, refuse) => {
539
+ const chunks = [];
540
+ let size = 0;
541
+ request.on('data', (chunk) => {
542
+ size += chunk.length;
543
+ if (size > limit) {
544
+ request.removeAllListeners('data');
545
+ request.resume();
546
+ refuse(new Error('too large'));
547
+ return;
548
+ }
549
+ chunks.push(chunk);
550
+ });
551
+ request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
552
+ request.on('error', refuse);
553
+ });
554
+ }
555
+ function redirect(response, location) {
556
+ response.statusCode = 302;
557
+ response.setHeader('Location', location);
558
+ response.setHeader('Cache-Control', 'no-store');
559
+ response.setHeader('Referrer-Policy', 'no-referrer');
560
+ response.end();
561
+ }
562
+ function reject(response, status, message) {
563
+ response.statusCode = status;
564
+ response.setHeader('Content-Type', 'text/plain; charset=utf-8');
565
+ response.setHeader('Cache-Control', 'no-store');
566
+ response.setHeader('X-Content-Type-Options', 'nosniff');
567
+ response.end(message);
568
+ }
569
+ export async function startLocalWorkspaceViewer(input) {
570
+ const now = input.now ?? Date.now;
571
+ const bundle = input.bundle === undefined ? defaultViewerBundle() : input.bundle;
572
+ const bundle_index = await readBundleIndex(bundle);
573
+ const requested_port = input.port ?? 0;
574
+ if (!Number.isInteger(requested_port) ||
575
+ requested_port < 0 ||
576
+ requested_port > 65_535) {
577
+ throw new Error('Viewer port must be an integer from 0 through 65535.');
578
+ }
579
+ await inspectLocalWorkspaceViewerSnapshot(input.runtime);
580
+ const token = randomBytes(32).toString('hex');
581
+ const nonce = randomBytes(18).toString('base64url');
582
+ let exact_origin = '';
583
+ let exact_host = '';
584
+ const root_path = `/${token}/`;
585
+ // The classic page under the React shell; its relative api/ reads arrive under it too.
586
+ const classic_path = `${root_path}classic/`;
587
+ const assets_path = `${root_path}assets/`;
588
+ const isShellPath = (path) => path.startsWith(root_path) && isShellAddress(path.slice(root_path.length));
589
+ const api_path = `${root_path}api/snapshot`;
590
+ const threads_path = `${root_path}api/threads`;
591
+ const pin_path = `${root_path}api/pin`;
592
+ const task_done_path = `${root_path}api/task-done`;
593
+ const worktree_paths = {
594
+ [`${root_path}api/worktree-merge`]: 'merge',
595
+ [`${root_path}api/worktree-remove`]: 'remove',
596
+ [`${root_path}api/worktree-dismiss`]: 'dismiss'
597
+ };
598
+ const release_links_path = `${root_path}api/release-links`;
599
+ const queues_path = `${root_path}api/queues`;
600
+ const app_path = `${root_path}api/app/`;
601
+ // One read per subject at a time: a request that comes while a read runs waits for that read.
602
+ // An answer is kept for its time; a failed read is not kept, so the next request reads again.
603
+ const release_kept = new Map();
604
+ const release_reading = new Map();
605
+ const releaseAnswer = (read, subject) => {
606
+ const key = JSON.stringify(subject);
607
+ const kept = release_kept.get(key);
608
+ if (kept && kept.until > now())
609
+ return Promise.resolve(kept.answer);
610
+ const running = release_reading.get(key);
611
+ if (running)
612
+ return running;
613
+ const started = now();
614
+ const reading = read(subject)
615
+ .then((answer) => {
616
+ if (!('error' in answer) && !('partial' in answer && answer.partial))
617
+ release_kept.set(key, {
618
+ until: started + (subject.kind === 'summary' ? RELEASE_SUMMARY_TTL_MS : RELEASE_SUBJECT_TTL_MS),
619
+ answer
620
+ });
621
+ return answer;
622
+ })
623
+ .finally(() => release_reading.delete(key));
624
+ release_reading.set(key, reading);
625
+ return reading;
626
+ };
627
+ // One workspace snapshot read at a time (iss-239): on a plan workspace a read takes minutes and
628
+ // about 1 GB, and the page asks every 2 s, so reads that overlapped ran the process out of heap.
629
+ // A request that comes while a read runs waits for that read and gets its answer.
630
+ let snapshot_reading;
631
+ const snapshotBody = () => {
632
+ if (snapshot_reading)
633
+ return snapshot_reading;
634
+ const reading = inspectLocalWorkspaceViewerSnapshot(input.runtime)
635
+ .then((snapshot) => JSON.stringify(snapshot))
636
+ .finally(() => {
637
+ snapshot_reading = undefined;
638
+ });
639
+ snapshot_reading = reading;
640
+ return reading;
641
+ };
642
+ // Reading the threads takes about a second and blocks this process, so one result serves every
643
+ // request and tab for THREADS_TTL_MS; the workspace snapshot polls keep their 2 s pace.
644
+ let threads_cache;
645
+ const threadsBody = (read) => {
646
+ const at = now();
647
+ if (threads_cache && at - threads_cache.at < THREADS_TTL_MS)
648
+ return threads_cache.body;
649
+ const body = JSON.stringify(read());
650
+ threads_cache = { at: now(), body };
651
+ return body;
652
+ };
653
+ // The Work queues read (qv-viewer-async) takes about 20 s on a busy machine, so a request never
654
+ // waits for one when there is an answer: the last answer serves every request and tab, a read
655
+ // under QUEUES_FRESH_MS old as it is, an older one while one newer read runs. Only a request with
656
+ // no answer yet waits, for that one read. A failed read keeps the last answer (the page's clock
657
+ // says how old it is); with none it is the request's failure, and the next request reads again.
658
+ let queues_kept;
659
+ let queues_reading;
660
+ const queuesRead = (read) => {
661
+ if (queues_reading)
662
+ return queues_reading;
663
+ const reading = read()
664
+ .then((value) => {
665
+ const body = JSON.stringify(value);
666
+ queues_kept = { at: now(), body };
667
+ return body;
668
+ })
669
+ .finally(() => {
670
+ queues_reading = undefined;
671
+ });
672
+ queues_reading = reading;
673
+ return reading;
674
+ };
675
+ const queuesBody = (read) => {
676
+ const kept = queues_kept;
677
+ if (kept && now() - kept.at < QUEUES_FRESH_MS)
678
+ return Promise.resolve(kept.body);
679
+ if (kept) {
680
+ queuesRead(read).catch((error) => {
681
+ // The detail (a path or a database error) stays on this terminal, not the page.
682
+ process.stderr.write(`sdlc viewer: work queues failed: ${error instanceof Error ? error.message : String(error)}\n`);
683
+ });
684
+ return Promise.resolve(kept.body);
685
+ }
686
+ return queuesRead(read);
687
+ };
688
+ // A write's body: only from the page itself (an Origin header naming this viewer is required, so
689
+ // another site, which cannot read the token path anyway, cannot post a form), as JSON, at most
690
+ // PIN_BODY_LIMIT bytes. Undefined when it was refused, the answer already sent.
691
+ const writeBody = async (request, response) => {
692
+ if (request.headers.origin !== exact_origin) {
693
+ reject(response, 403, 'Invalid origin.');
694
+ return undefined;
695
+ }
696
+ if (!/^application\/json(\s*;|$)/i.test(request.headers['content-type'] ?? '')) {
697
+ reject(response, 415, 'Send JSON.');
698
+ return undefined;
699
+ }
700
+ let text = '';
701
+ try {
702
+ if (Number(request.headers['content-length'] ?? 0) > PIN_BODY_LIMIT)
703
+ throw new Error('too large');
704
+ text = await readBody(request, PIN_BODY_LIMIT);
705
+ }
706
+ catch {
707
+ request.resume(); // drain what was not read, so the answer reaches the page
708
+ reject(response, 413, 'Too large.');
709
+ return undefined;
710
+ }
711
+ try {
712
+ return JSON.parse(text) ?? {};
713
+ }
714
+ catch {
715
+ return {};
716
+ }
717
+ };
718
+ // POST api/pin {key, on}. The answer is the pins after, or why not as plain text the page shows
719
+ // after 'The pin was not saved: '.
720
+ const answerPin = async (request, response, pin) => {
721
+ const body = await writeBody(request, response);
722
+ if (body === undefined)
723
+ return;
724
+ const { key, on } = body;
725
+ if (typeof key !== 'string' || !key || key.length > PIN_KEY_LIMIT || typeof on !== 'boolean') {
726
+ reject(response, 400, 'Send {key, on}.');
727
+ return;
728
+ }
729
+ let result;
730
+ try {
731
+ result = pin(key, on);
732
+ }
733
+ catch (error) {
734
+ // The detail (a path or a database error) stays on this terminal, not the page.
735
+ process.stderr.write(`sdlc viewer: pin failed: ${error instanceof Error ? error.message : String(error)}\n`);
736
+ threads_cache = undefined;
737
+ reject(response, 500, 'see the sdlc viewer terminal.');
738
+ return;
739
+ }
740
+ // The next read shows the pin, or the store as it is after a save that failed.
741
+ threads_cache = undefined;
742
+ if ('refused' in result) {
743
+ reject(response, 400, result.refused);
744
+ return;
745
+ }
746
+ if ('failed' in result) {
747
+ reject(response, 500, result.failed);
748
+ return;
749
+ }
750
+ secureHeaders({ response, nonce, content_type: 'application/json; charset=utf-8' });
751
+ response.statusCode = 200;
752
+ response.end(JSON.stringify({ pinned: result.pinned }));
753
+ };
754
+ // POST api/task-done {task} or {hold}: the user marks it done with `status user-done`. The answer
755
+ // is the words the threads were told, or the engine's refusal (what closes it instead) as plain
756
+ // text the page shows in the row's alert.
757
+ const answerTaskDone = async (request, response, taskDone) => {
758
+ const body = await writeBody(request, response);
759
+ if (body === undefined)
760
+ return;
761
+ const { task, hold } = body;
762
+ const target = typeof task === 'string' && TASK_REF.test(task) && hold === undefined
763
+ ? { task }
764
+ : typeof hold === 'string' && HOLD_REF.test(hold) && task === undefined
765
+ ? { hold }
766
+ : null;
767
+ if (!target) {
768
+ reject(response, 400, 'Send {task} or {hold}.');
769
+ return;
770
+ }
771
+ let result;
772
+ try {
773
+ result = await taskDone(target);
774
+ }
775
+ catch (error) {
776
+ result = { failed: error instanceof Error ? error.message : String(error) };
777
+ }
778
+ // The next read shows the row gone, or as it is after a refusal.
779
+ threads_cache = undefined;
780
+ input.app?.invalidate?.();
781
+ if ('refused' in result) {
782
+ reject(response, 409, result.refused);
783
+ return;
784
+ }
785
+ if ('failed' in result) {
786
+ // The detail (a path or a database error) stays on this terminal, not the page.
787
+ process.stderr.write(`sdlc viewer: task done failed: ${result.failed}\n`);
788
+ reject(response, 500, 'it could not be recorded; see the sdlc viewer terminal.');
789
+ return;
790
+ }
791
+ secureHeaders({ response, nonce, content_type: 'application/json; charset=utf-8' });
792
+ response.statusCode = 200;
793
+ response.end(JSON.stringify({ message: result.message }));
794
+ };
795
+ // POST api/worktree-merge {path}, api/worktree-remove {path} and api/worktree-dismiss: the answer is
796
+ // the job (202), or why not as {error, reason?} the page shows on the row.
797
+ const answerWorktree = async (request, response, write) => {
798
+ const body = await writeBody(request, response);
799
+ if (body === undefined)
800
+ return;
801
+ let answer;
802
+ try {
803
+ answer = await write(body);
804
+ }
805
+ catch (error) {
806
+ // The detail (a path or an engine error) stays on this terminal, not the page.
807
+ process.stderr.write(`sdlc viewer: worktree write failed: ${error instanceof Error ? error.message : String(error)}\n`);
808
+ answer = { status: 500, body: { error: 'It was not written; see the sdlc viewer terminal.' } };
809
+ }
810
+ // The next read shows the write on its row.
811
+ secureHeaders({ response, nonce, content_type: 'application/json; charset=utf-8' });
812
+ response.statusCode = answer.status;
813
+ response.end(JSON.stringify(answer.body));
814
+ };
815
+ const server = createServer(async (request, response) => {
816
+ const writes = request.method === 'POST';
817
+ if (request.method !== 'GET' && request.method !== 'HEAD' && !writes) {
818
+ reject(response, 405, 'Method not allowed.');
819
+ return;
820
+ }
821
+ if (request.headers.host !== exact_host) {
822
+ reject(response, 403, 'Invalid host.');
823
+ return;
824
+ }
825
+ if (request.headers.origin && request.headers.origin !== exact_origin) {
826
+ reject(response, 403, 'Invalid origin.');
827
+ return;
828
+ }
829
+ const url = new URL(request.url ?? '/', exact_origin);
830
+ // The classic page under the React shell reads its api/ routes relative to classic/.
831
+ const path = url.pathname.startsWith(`${classic_path}api/`)
832
+ ? root_path + url.pathname.slice(classic_path.length)
833
+ : url.pathname;
834
+ if (path === pin_path && input.pin) {
835
+ if (writes)
836
+ await answerPin(request, response, input.pin);
837
+ else if (request.method === 'HEAD') {
838
+ // A HEAD asks only whether the page may pin.
839
+ secureHeaders({ response, nonce, content_type: 'application/json; charset=utf-8' });
840
+ response.statusCode = 200;
841
+ response.end();
842
+ }
843
+ else
844
+ reject(response, 405, 'Method not allowed.');
845
+ return;
846
+ }
847
+ // Whether Done is offered comes with each row (local_done), so the route only writes.
848
+ if (path === task_done_path && input.taskDone) {
849
+ if (writes)
850
+ await answerTaskDone(request, response, input.taskDone);
851
+ else
852
+ reject(response, 405, 'Method not allowed.');
853
+ return;
854
+ }
855
+ const worktree_write = Object.hasOwn(worktree_paths, path) ? worktree_paths[path] : undefined;
856
+ if (worktree_write && input.worktrees) {
857
+ if (writes)
858
+ await answerWorktree(request, response, input.worktrees[worktree_write]);
859
+ else
860
+ reject(response, 405, 'Method not allowed.');
861
+ return;
862
+ }
863
+ // Every other path only reads.
864
+ if (writes) {
865
+ reject(response, 405, 'Method not allowed.');
866
+ return;
867
+ }
868
+ // The classic page's own address ends with a slash, so its relative reads resolve under it.
869
+ if (path === classic_path.slice(0, -1)) {
870
+ redirect(response, classic_path + url.search);
871
+ return;
872
+ }
873
+ if (bundle_index !== null) {
874
+ // An older section address ('<token>/?section=tasks') opens that section in the classic page.
875
+ if (path === root_path && url.searchParams.has('section')) {
876
+ redirect(response, classic_path + url.search);
877
+ return;
878
+ }
879
+ // The React shell answers the shell's addresses (isShellAddress's allow-list); with no
880
+ // bundle built, '<token>/' is the classic page as before. The classic page is always at classic/.
881
+ if (isShellPath(path)) {
882
+ // Read on each request, so a rebuild while the viewer runs serves the index that names its
883
+ // new assets; the one read at start answers only while a rebuild is replacing the directory.
884
+ const index = (await readBundleIndex(bundle)) ?? bundle_index;
885
+ // The page's relative addresses (its assets, api/ reads and links) resolve from the token
886
+ // path at every depth, so org/<org>/threads reads the same api/threads as '<token>/'.
887
+ // First in the document, before any element that uses an address.
888
+ const page = index.replace(/^(\s*<!doctype html>)?/i, `$1<base href="${root_path}">`);
889
+ secureHeaders({ response, nonce, content_type: 'text/html; charset=utf-8', bundle: true });
890
+ response.statusCode = 200;
891
+ response.end(request.method === 'HEAD' ? undefined : page);
892
+ return;
893
+ }
894
+ }
895
+ if (path === root_path || path === classic_path) {
896
+ secureHeaders({
897
+ response,
898
+ nonce,
899
+ content_type: 'text/html; charset=utf-8'
900
+ });
901
+ // Under classic/ beside the React shell, its header links back to the shell.
902
+ const body = viewerHtml({ nonce, shell: bundle_index !== null && path === classic_path });
903
+ response.statusCode = 200;
904
+ response.end(request.method === 'HEAD' ? undefined : body);
905
+ return;
906
+ }
907
+ // The bundle's files: one name under assets/, of a known type, read from that directory only.
908
+ if (bundle && bundle_index !== null && path.startsWith(assets_path)) {
909
+ const name = path.slice(assets_path.length);
910
+ const type = ASSET_TYPES[extname(name)];
911
+ if (!ASSET_NAME.test(name) || name.includes('..') || !type) {
912
+ reject(response, 404, 'Not found.');
913
+ return;
914
+ }
915
+ let body;
916
+ try {
917
+ body = await readFile(join(bundle, 'assets', name));
918
+ }
919
+ catch {
920
+ reject(response, 404, 'Not found.');
921
+ return;
922
+ }
923
+ secureHeaders({ response, nonce, content_type: type, bundle: true });
924
+ // Vite names each file by its content, so a name never changes what it holds.
925
+ response.setHeader('Cache-Control', 'private, max-age=31536000, immutable');
926
+ response.statusCode = 200;
927
+ response.end(request.method === 'HEAD' ? undefined : body);
928
+ return;
929
+ }
930
+ if (path === api_path) {
931
+ try {
932
+ const body = await snapshotBody();
933
+ secureHeaders({
934
+ response,
935
+ nonce,
936
+ content_type: 'application/json; charset=utf-8'
937
+ });
938
+ response.statusCode = 200;
939
+ response.end(request.method === 'HEAD' ? undefined : body);
940
+ }
941
+ catch (error) {
942
+ // The detail (a path or a database error) stays on this terminal, not the page.
943
+ process.stderr.write(`sdlc viewer: the snapshot failed: ${error instanceof Error ? error.message : String(error)}\n`);
944
+ reject(response, 500, 'The snapshot failed; see the sdlc viewer terminal.');
945
+ }
946
+ return;
947
+ }
948
+ if (path === threads_path && input.threads) {
949
+ secureHeaders({
950
+ response,
951
+ nonce,
952
+ content_type: 'application/json; charset=utf-8'
953
+ });
954
+ // A HEAD asks only whether the panel exists: no snapshot is read for it.
955
+ if (request.method === 'HEAD') {
956
+ response.statusCode = 200;
957
+ response.end();
958
+ return;
959
+ }
960
+ try {
961
+ response.statusCode = 200;
962
+ response.end(threadsBody(input.threads));
963
+ }
964
+ catch (error) {
965
+ // The detail (a path or a database error) stays on this terminal, not the page.
966
+ process.stderr.write(`sdlc viewer: threads failed: ${error instanceof Error ? error.message : String(error)}\n`);
967
+ reject(response, 500, 'Threads failed; see the sdlc viewer terminal.');
968
+ }
969
+ return;
970
+ }
971
+ if (path === queues_path && input.queues) {
972
+ let body;
973
+ try {
974
+ body = await queuesBody(input.queues);
975
+ }
976
+ catch (error) {
977
+ // The detail (a path or a database error) stays on this terminal, not the page.
978
+ process.stderr.write(`sdlc viewer: work queues failed: ${error instanceof Error ? error.message : String(error)}\n`);
979
+ reject(response, 500, 'Work queues failed; see the sdlc viewer terminal.');
980
+ return;
981
+ }
982
+ // No queue model (no workflow template): there is no panel to draw.
983
+ if (body === 'null') {
984
+ reject(response, 404, 'Not found.');
985
+ return;
986
+ }
987
+ secureHeaders({ response, nonce, content_type: 'application/json; charset=utf-8' });
988
+ response.statusCode = 200;
989
+ response.end(request.method === 'HEAD' ? undefined : body);
990
+ return;
991
+ }
992
+ if (path.startsWith(app_path) && input.app) {
993
+ // Each segment decoded and checked before any reader sees it; one that is not a plain name is 404.
994
+ let segments;
995
+ try {
996
+ segments = path.slice(app_path.length).split('/').map((s) => decodeURIComponent(s));
997
+ }
998
+ catch {
999
+ segments = null;
1000
+ }
1001
+ secureHeaders({ response, nonce, content_type: 'application/json; charset=utf-8' });
1002
+ if (!segments || !segments.every((s) => APP_SEGMENT.test(s))) {
1003
+ response.statusCode = 404;
1004
+ response.end(request.method === 'HEAD' ? undefined : JSON.stringify({ error: 'Not found.', reason: 'not_found' }));
1005
+ return;
1006
+ }
1007
+ // A HEAD asks only whether the route exists: nothing is read for it.
1008
+ if (request.method === 'HEAD') {
1009
+ response.statusCode = input.app.head ? input.app.head(segments) : 200;
1010
+ response.end();
1011
+ return;
1012
+ }
1013
+ let answer;
1014
+ try {
1015
+ answer = await input.app({ segments, query: url.searchParams });
1016
+ }
1017
+ catch (error) {
1018
+ // The detail (a path or a stack) stays on this terminal, not the page.
1019
+ process.stderr.write(`sdlc viewer: ${segments.join('/')} failed: ${error instanceof Error ? error.message : String(error)}\n`);
1020
+ answer = {
1021
+ status: 500,
1022
+ body: { error: 'The read failed; see the sdlc viewer terminal.', reason: 'engine_failed', retry: true }
1023
+ };
1024
+ }
1025
+ response.statusCode = answer.status;
1026
+ response.end(JSON.stringify(answer.body));
1027
+ return;
1028
+ }
1029
+ if (path === release_links_path && input.releaseLinks) {
1030
+ // A HEAD asks only whether the panel exists: nothing is read for it.
1031
+ if (request.method === 'HEAD') {
1032
+ secureHeaders({ response, nonce, content_type: 'application/json; charset=utf-8' });
1033
+ response.statusCode = 200;
1034
+ response.end();
1035
+ return;
1036
+ }
1037
+ const subject = releaseSubjectOf(url.searchParams);
1038
+ if ('refused' in subject) {
1039
+ reject(response, 400, subject.refused);
1040
+ return;
1041
+ }
1042
+ let answer;
1043
+ try {
1044
+ answer = await releaseAnswer(input.releaseLinks, subject);
1045
+ }
1046
+ catch (error) {
1047
+ // The detail (a path or a stack) stays on this terminal, not the page.
1048
+ const what = subject.kind === 'task' ? 'The task page' : 'Release links';
1049
+ process.stderr.write(`sdlc viewer: ${what.toLowerCase()} failed: ${error instanceof Error ? error.message : String(error)}\n`);
1050
+ reject(response, 500, `${what} failed; see the sdlc viewer terminal.`);
1051
+ return;
1052
+ }
1053
+ // An engine without release links: there is no panel to draw.
1054
+ if ('absent' in answer) {
1055
+ reject(response, 404, 'Not found.');
1056
+ return;
1057
+ }
1058
+ secureHeaders({ response, nonce, content_type: 'application/json; charset=utf-8' });
1059
+ response.statusCode = 200;
1060
+ response.end(JSON.stringify(answer));
1061
+ return;
1062
+ }
1063
+ reject(response, 404, 'Not found.');
1064
+ });
1065
+ await new Promise((resolve_listen, reject_listen) => {
1066
+ server.once('error', reject_listen);
1067
+ server.listen(requested_port, LOOPBACK_HOST, () => {
1068
+ server.off('error', reject_listen);
1069
+ resolve_listen();
1070
+ });
1071
+ });
1072
+ const address = server.address();
1073
+ if (!address || typeof address === 'string') {
1074
+ server.close();
1075
+ throw new Error('Viewer did not receive a loopback TCP address.');
1076
+ }
1077
+ exact_host = `${LOOPBACK_HOST}:${address.port}`;
1078
+ exact_origin = `http://${exact_host}`;
1079
+ const closed = new Promise((resolve_closed) => {
1080
+ server.once('close', resolve_closed);
1081
+ });
1082
+ return {
1083
+ url: `${exact_origin}${root_path}`,
1084
+ origin: exact_origin,
1085
+ port: address.port,
1086
+ page: bundle_index === null ? 'classic' : 'react',
1087
+ close: () => new Promise((resolve_close, reject_close) => {
1088
+ if (!server.listening) {
1089
+ resolve_close();
1090
+ return;
1091
+ }
1092
+ server.close((error) => error ? reject_close(error) : resolve_close());
1093
+ }),
1094
+ closed
1095
+ };
1096
+ }
1097
+ //# sourceMappingURL=workspace-viewer-server.js.map