elanous 0.2.12 → 0.2.14

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 (534) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1307-6e7e0377c1db0bdc.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
  9. package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
  31. package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/{9906-482c6619b3fdda20.js → 8060-cc008cc6fedfa818.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
  42. package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-7b2b4f378354869a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-1512d91c7491ed62.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-a627db94ec6904a0.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-fed69f9759d248d0.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/{page-a67e11ba962337b5.js → page-15374909d8f1aebd.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-012a742f81218b01.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/intake/page-99effd9d893a43dc.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/layout-938de57ae0315e46.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/market/page-629e1ede482ec6eb.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-2ce5471926761fdd.js} +1 -1
  69. package/apps/pwa/out/_next/static/chunks/app/morning/page-27e99b4e54a849d4.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-32cde713be47cddb.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e64caff084f7ce21.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-db07eaffba0df7e3.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/settings/page-86f91034bd5cabe5.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/share/page-ce4d8a1215ce53a3.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-274703bc41b71333.js} +1 -1
  89. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/vault/page-3b7e821dc492bdfd.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2f810b18d89ecba6.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
  97. package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  98. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
  104. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +1 -0
  105. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +1 -0
  106. package/apps/pwa/out/approvals/index.html +1 -1
  107. package/apps/pwa/out/approvals/index.txt +23 -23
  108. package/apps/pwa/out/autopilot/index.html +1 -1
  109. package/apps/pwa/out/autopilot/index.txt +23 -23
  110. package/apps/pwa/out/board/index.html +1 -1
  111. package/apps/pwa/out/board/index.txt +23 -23
  112. package/apps/pwa/out/botlab/index.html +1 -1
  113. package/apps/pwa/out/botlab/index.txt +22 -22
  114. package/apps/pwa/out/bots/index.html +1 -1
  115. package/apps/pwa/out/bots/index.txt +22 -22
  116. package/apps/pwa/out/chat/index.html +1 -1
  117. package/apps/pwa/out/chat/index.txt +22 -22
  118. package/apps/pwa/out/consult/index.html +1 -1
  119. package/apps/pwa/out/consult/index.txt +23 -23
  120. package/apps/pwa/out/control/index.html +1 -1
  121. package/apps/pwa/out/control/index.txt +23 -23
  122. package/apps/pwa/out/design-check/index.html +1 -1
  123. package/apps/pwa/out/design-check/index.txt +23 -23
  124. package/apps/pwa/out/editor/index.html +1 -1
  125. package/apps/pwa/out/editor/index.txt +23 -23
  126. package/apps/pwa/out/exec/index.html +1 -1
  127. package/apps/pwa/out/exec/index.txt +23 -23
  128. package/apps/pwa/out/field/index.html +1 -1
  129. package/apps/pwa/out/field/index.txt +23 -23
  130. package/apps/pwa/out/healthz +1 -1
  131. package/apps/pwa/out/index.html +1 -1
  132. package/apps/pwa/out/index.txt +22 -22
  133. package/apps/pwa/out/inside/index.html +1 -1
  134. package/apps/pwa/out/inside/index.txt +22 -22
  135. package/apps/pwa/out/intake/index.html +1 -1
  136. package/apps/pwa/out/intake/index.txt +23 -23
  137. package/apps/pwa/out/live/index.html +1 -1
  138. package/apps/pwa/out/live/index.txt +23 -23
  139. package/apps/pwa/out/loops/index.html +1 -0
  140. package/apps/pwa/out/loops/index.txt +30 -0
  141. package/apps/pwa/out/market/index.html +1 -1
  142. package/apps/pwa/out/market/index.txt +22 -22
  143. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  144. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  145. package/apps/pwa/out/morning/index.html +1 -1
  146. package/apps/pwa/out/morning/index.txt +23 -23
  147. package/apps/pwa/out/observatory/index.html +1 -1
  148. package/apps/pwa/out/observatory/index.txt +23 -23
  149. package/apps/pwa/out/ops/checklist/index.html +1 -1
  150. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  151. package/apps/pwa/out/ops/release/index.html +1 -1
  152. package/apps/pwa/out/ops/release/index.txt +22 -22
  153. package/apps/pwa/out/ops/seats/index.html +1 -1
  154. package/apps/pwa/out/ops/seats/index.txt +22 -22
  155. package/apps/pwa/out/outputs/index.html +1 -1
  156. package/apps/pwa/out/outputs/index.txt +22 -22
  157. package/apps/pwa/out/reflection/index.html +1 -1
  158. package/apps/pwa/out/reflection/index.txt +23 -23
  159. package/apps/pwa/out/scheduler/index.html +1 -1
  160. package/apps/pwa/out/scheduler/index.txt +23 -23
  161. package/apps/pwa/out/sessions/index.html +1 -1
  162. package/apps/pwa/out/sessions/index.txt +22 -22
  163. package/apps/pwa/out/settings/devices/index.html +1 -1
  164. package/apps/pwa/out/settings/devices/index.txt +23 -23
  165. package/apps/pwa/out/settings/index.html +1 -1
  166. package/apps/pwa/out/settings/index.txt +23 -23
  167. package/apps/pwa/out/setup/done/index.html +1 -1
  168. package/apps/pwa/out/setup/done/index.txt +23 -23
  169. package/apps/pwa/out/setup/index.html +1 -1
  170. package/apps/pwa/out/setup/index.txt +23 -23
  171. package/apps/pwa/out/share/index.html +1 -1
  172. package/apps/pwa/out/share/index.txt +23 -23
  173. package/apps/pwa/out/showroom/index.html +1 -1
  174. package/apps/pwa/out/showroom/index.txt +22 -22
  175. package/apps/pwa/out/tasks/index.html +1 -1
  176. package/apps/pwa/out/tasks/index.txt +23 -23
  177. package/apps/pwa/out/term/index.html +1 -1
  178. package/apps/pwa/out/term/index.txt +23 -23
  179. package/apps/pwa/out/today/index.html +1 -1
  180. package/apps/pwa/out/today/index.txt +23 -23
  181. package/apps/pwa/out/trace/index.html +1 -1
  182. package/apps/pwa/out/trace/index.txt +23 -23
  183. package/apps/pwa/out/vault/index.html +1 -1
  184. package/apps/pwa/out/vault/index.txt +23 -23
  185. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  186. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  187. package/apps/pwa/out/workflows/index.html +1 -1
  188. package/apps/pwa/out/workflows/index.txt +23 -23
  189. package/apps/pwa/out/workspace/index.html +1 -1
  190. package/apps/pwa/out/workspace/index.txt +23 -23
  191. package/apps/pwa/out/worktrees/index.html +1 -1
  192. package/apps/pwa/out/worktrees/index.txt +23 -23
  193. package/examples/teams/personas/backend.yaml +4 -0
  194. package/examples/teams/personas/pm.yaml +4 -0
  195. package/examples/teams/personas/qa.yaml +4 -0
  196. package/examples/teams/small-product.yaml +20 -0
  197. package/graphs/lecture/lecture-note.yaml +28 -0
  198. package/graphs/lecture/recipes.yaml +17 -0
  199. package/graphs/orchestrator/orchestrator.yaml +59 -0
  200. package/graphs/orchestrator/recipes.yaml +18 -0
  201. package/graphs/release/recipes.yaml +3 -0
  202. package/graphs/release/release-loop.yaml +4 -1
  203. package/graphs/rhythm/daily.yaml +38 -0
  204. package/graphs/rhythm/recipes.yaml +18 -0
  205. package/graphs/rhythm/weekly.yaml +39 -0
  206. package/graphs/video/recipes.yaml +3 -0
  207. package/graphs/video/youtube-summary.yaml +17 -0
  208. package/package.json +2 -1
  209. package/scripts/audit-test-state-writes.ts +72 -17
  210. package/scripts/brand/check.ts +186 -0
  211. package/scripts/context-hooks/emit.ts +36 -14
  212. package/scripts/coord-channel-watch.sh +1 -0
  213. package/scripts/coord-post.sh +11 -1
  214. package/scripts/discord-seat-probe.ts +144 -0
  215. package/scripts/docs-cli-check.ts +23 -6
  216. package/scripts/expose-rubric-check.ts +118 -0
  217. package/scripts/expose-rubric-suggest.ts +163 -0
  218. package/scripts/heal/run-role.ts +14 -0
  219. package/scripts/hq/hq-standby.sh +65 -0
  220. package/scripts/hq/standby-promote.ts +173 -0
  221. package/scripts/hq/standby-role.ts +59 -0
  222. package/scripts/hq/standby-snapshot.ts +226 -0
  223. package/scripts/hq/standby-verify.ts +223 -0
  224. package/scripts/lecture-note/node.ts +256 -0
  225. package/scripts/lessons/handbook-page.ts +85 -0
  226. package/scripts/release-loop/gate-node.ts +62 -15
  227. package/scripts/release-loop/ops-upgrade-node.ts +17 -5
  228. package/scripts/release-loop/publish-node.ts +14 -1
  229. package/scripts/release-loop/unattended-release.ts +23 -1
  230. package/scripts/release-loop/vault-note-node.ts +196 -0
  231. package/scripts/release-story/draft.ts +172 -0
  232. package/scripts/rhythm/daily.ts +383 -0
  233. package/scripts/rhythm/weekly.ts +294 -0
  234. package/scripts/skills/public-check.ts +169 -0
  235. package/scripts/test-deterministic.ts +57 -11
  236. package/scripts/test-diet/effectiveness.ts +195 -0
  237. package/scripts/test-diet/lib.ts +16 -8
  238. package/scripts/test-diet/node.ts +77 -9
  239. package/scripts/ux/demo-rehearsal.ts +2 -2
  240. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  241. package/scripts/video/youtube-summary.ts +133 -0
  242. package/skills/diagram-master/SKILL.md +42 -49
  243. package/skills/diagram-master/references/pyproject.toml +4 -1
  244. package/skills/diagram-master/references/routing-guide.md +4 -2
  245. package/skills/grill-me/SKILL.md +1 -0
  246. package/skills/omni-crawl/SKILL.md +16 -16
  247. package/skills/omni-crawl/package-lock.json +1 -544
  248. package/skills/omni-crawl/package.json +3 -4
  249. package/skills/omni-crawl/scripts/main.ts +13 -13
  250. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  251. package/skills/omni-crawl/src/apify.ts +13 -2
  252. package/skills/omni-crawl/src/capture.ts +6 -0
  253. package/skills/omni-crawl/src/registry.ts +2 -2
  254. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  255. package/skills/omni-digest/SKILL.md +10 -11
  256. package/skills/omni-digest/package-lock.json +0 -545
  257. package/skills/omni-digest/package.json +2 -5
  258. package/skills/omni-digest/scripts/main.ts +3 -3
  259. package/skills/omni-digest/src/delegate.ts +5 -5
  260. package/skills/omni-digest/src/fetch.ts +2 -2
  261. package/skills/omni-digest/src/media.ts +25 -11
  262. package/skills/omni-digest/src/visual.ts +3 -2
  263. package/skills/omni-market/scripts/main.ts +1 -1
  264. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  265. package/skills/youtube-master/SKILL.md +13 -13
  266. package/skills/youtube-master/package-lock.json +0 -545
  267. package/skills/youtube-master/package.json +2 -5
  268. package/skills/youtube-master/scripts/main.ts +2 -2
  269. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  270. package/skills/youtube-master/src/util.ts +30 -7
  271. package/src/agent/identity-env.ts +11 -0
  272. package/src/agent-mission/driver.ts +15 -10
  273. package/src/agent-mission/duo.ts +94 -0
  274. package/src/agent-mission/pty-decision.ts +1 -1
  275. package/src/autopilot/build/llm-conflict-merge.ts +45 -8
  276. package/src/autopilot/build/merge-intent.ts +53 -0
  277. package/src/autopilot/track-agent-shadow.ts +2 -1
  278. package/src/autopilot/track-agent.ts +4 -1
  279. package/src/boot/daemon-prompt-request.ts +3 -0
  280. package/src/boot/daemon-prompt-turn.ts +6 -2
  281. package/src/boot/daemon-runtime.ts +48 -16
  282. package/src/chat/index.ts +2 -0
  283. package/src/claims/claims-ledger.ts +232 -0
  284. package/src/claims/claims-render.ts +157 -0
  285. package/src/cli/claims-cli.ts +94 -0
  286. package/src/cli/decisions-cli.ts +84 -20
  287. package/src/cli/docs-cli.ts +2 -0
  288. package/src/cli/flow-cli.ts +11 -0
  289. package/src/cli/freeze-cli.ts +45 -0
  290. package/src/cli/lesson-cli.ts +108 -0
  291. package/src/cli/logs-cli.ts +104 -2
  292. package/src/cli/logs-model-input.ts +131 -0
  293. package/src/cli/outputs-cli.ts +92 -0
  294. package/src/cli/persona-cli.ts +29 -0
  295. package/src/cli/pod-cli.ts +10 -3
  296. package/src/cli/pr-cli.ts +44 -2
  297. package/src/cli/private-ledger-commands.ts +54 -4
  298. package/src/cli/pty-drive-verdict.ts +11 -2
  299. package/src/cli/release-cli.ts +248 -45
  300. package/src/cli/rfc-status.ts +91 -0
  301. package/src/connectors/linear.ts +5 -3
  302. package/src/context-bus/condense.ts +189 -0
  303. package/src/context-bus/context-day.ts +82 -0
  304. package/src/context-bus/context-now-public.ts +86 -0
  305. package/src/context-bus/context-now-surfaces.ts +70 -4
  306. package/src/context-bus/context-now.ts +4 -3
  307. package/src/context-bus/long-term-memory.ts +201 -24
  308. package/src/context-bus/nightly-condense.ts +1 -1
  309. package/src/dashboard/index.ts +75 -22
  310. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  311. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  312. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  313. package/src/decisions/decision-cards.ts +13 -7
  314. package/src/decisions/decision-ledger.ts +83 -18
  315. package/src/discord/sprint21-runtime.ts +16 -0
  316. package/src/discord-self-message.ts +8 -0
  317. package/src/discord-slash-wire.ts +31 -2
  318. package/src/domains/outbound-alert.ts +1 -1
  319. package/src/elanous-config-dir.ts +29 -4
  320. package/src/execution-loop/launch-gate.ts +29 -14
  321. package/src/flow/cards.ts +239 -0
  322. package/src/graph-kinds/registry.ts +2 -2
  323. package/src/graph-runner/graph-approval-card.ts +2 -2
  324. package/src/graph-runner/graph-grow.ts +64 -11
  325. package/src/graph-runner/graph-tick.ts +13 -2
  326. package/src/graph-runner/runner.ts +111 -22
  327. package/src/harness/harness-cli-command.ts +178 -27
  328. package/src/harness/harness-incidents.ts +43 -12
  329. package/src/harness/harness-pod-dispatch.ts +2 -0
  330. package/src/harness/harness-queue-child.ts +37 -0
  331. package/src/harness/harness-queue.ts +494 -0
  332. package/src/harness/helper-scan.ts +139 -0
  333. package/src/harness/model-input-observation.ts +37 -0
  334. package/src/hooks/github-shadow.ts +72 -2
  335. package/src/hooks/receiver.ts +4 -2
  336. package/src/hq/hq.ts +318 -0
  337. package/src/hq/lease.ts +210 -0
  338. package/src/hq/ledger-write-target.ts +41 -0
  339. package/src/hq/promote.ts +301 -0
  340. package/src/index.ts +281 -27
  341. package/src/intake-plane/discord-seat-work.ts +13 -6
  342. package/src/intake-plane/linear-wish.ts +72 -0
  343. package/src/intake-plane/persona-answer.ts +5 -0
  344. package/src/intake-plane/telegram-seat-work.ts +13 -6
  345. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  346. package/src/intake-plane/wish-card.ts +53 -2
  347. package/src/intake-plane/wish-folder.ts +5 -2
  348. package/src/intake-plane/wish-reply.ts +72 -0
  349. package/src/lessons/lesson-import.ts +106 -0
  350. package/src/lessons/lesson-ledger.ts +206 -0
  351. package/src/loops/activity.ts +203 -0
  352. package/src/loops/budget.ts +32 -0
  353. package/src/loops/checker.ts +180 -0
  354. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  355. package/src/loops/loop-cli.ts +72 -6
  356. package/src/loops/neighbors.ts +40 -0
  357. package/src/loops/observe.ts +59 -0
  358. package/src/loops/orchestrator/author-depth.ts +164 -0
  359. package/src/loops/orchestrator/author-input-check.ts +77 -0
  360. package/src/loops/orchestrator/author-ledger.ts +230 -0
  361. package/src/loops/orchestrator/finish-rate.ts +175 -0
  362. package/src/loops/orchestrator/overlap.ts +180 -0
  363. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  364. package/src/loops/orchestrator/tick.ts +302 -0
  365. package/src/loops/orchestrator/traffic.ts +165 -0
  366. package/src/loops/package/verify.ts +125 -0
  367. package/src/loops/registry.ts +454 -9
  368. package/src/loops/status-rows.ts +118 -0
  369. package/src/loops/verdict.ts +34 -0
  370. package/src/maturity/bot-slash-catalog.ts +66 -0
  371. package/src/maturity/feature-maturity.ts +8 -5
  372. package/src/memory.ts +37 -4
  373. package/src/nexus/api/card-followup-route.ts +118 -0
  374. package/src/nexus/api/dashboard.ts +6 -1
  375. package/src/nexus/api/decisions-route.ts +68 -0
  376. package/src/nexus/api/http-server.ts +65 -2
  377. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  378. package/src/nexus/api/personas.ts +2 -1
  379. package/src/nexus/api/schedules-read.ts +5 -1
  380. package/src/nexus/api/skill-exec-route.ts +97 -0
  381. package/src/nexus/api/task-cards-api.ts +44 -5
  382. package/src/nexus/api/workflows.ts +2 -1
  383. package/src/persona/loader.ts +18 -0
  384. package/src/persona/persona-todo.ts +14 -9
  385. package/src/persona/presets.ts +2 -1
  386. package/src/persona/team.ts +105 -0
  387. package/src/persona/types.ts +4 -0
  388. package/src/pod-lease/host-lease.ts +130 -20
  389. package/src/release-loop/auto-start.ts +48 -0
  390. package/src/release-loop/checklist.ts +13 -4
  391. package/src/release-loop/feature-store.ts +28 -16
  392. package/src/release-loop/landed-but-yellow.ts +12 -0
  393. package/src/release-loop/landing-freeze.ts +121 -0
  394. package/src/release-loop/placement.ts +242 -0
  395. package/src/release-loop/release-schedule.ts +3 -1
  396. package/src/seat-dispatch/ceo-commands.ts +2 -2
  397. package/src/seat-dispatch/seat-ask.ts +73 -15
  398. package/src/seat-dispatch/seat-questions.ts +58 -0
  399. package/src/seat-loop/seat-loop.ts +495 -46
  400. package/src/seat-loop/seat-pack.ts +127 -0
  401. package/src/self-dev/ask-launch-flow.ts +3 -0
  402. package/src/self-dev/dev-pipeline.ts +6 -2
  403. package/src/self-dev/draft-sweep.ts +76 -0
  404. package/src/self-dev/run-store.ts +73 -1
  405. package/src/self-dev/run-supervisor.ts +8 -0
  406. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  407. package/src/self-implement/budget-gate.ts +29 -0
  408. package/src/self-implement/expose-gate.ts +47 -0
  409. package/src/self-implement/frozen-merges.ts +294 -0
  410. package/src/self-implement/gate-cli.ts +10 -4
  411. package/src/self-implement/gate-scope.ts +46 -9
  412. package/src/self-implement/goal-archive.ts +222 -0
  413. package/src/self-implement/goal-lookup.ts +147 -0
  414. package/src/self-implement/heal-intake.ts +79 -0
  415. package/src/self-implement/host-regate.ts +25 -2
  416. package/src/self-implement/l8-merge-queue.ts +157 -1
  417. package/src/self-implement/orchestrator.ts +114 -10
  418. package/src/self-implement/revert-guard.ts +62 -0
  419. package/src/self-implement/run-ledger.ts +1 -1
  420. package/src/self-implement/seams.ts +28 -8
  421. package/src/session/index.ts +2 -0
  422. package/src/steward/launch.ts +5 -1
  423. package/src/steward/readiness.ts +125 -0
  424. package/src/steward/rescue.ts +75 -0
  425. package/src/steward/triage.ts +48 -9
  426. package/src/task-cards/card-cli.ts +86 -0
  427. package/src/task-cards/card-store.ts +10 -6
  428. package/src/task-orchestrator/surfaces/pod-lease.ts +23 -8
  429. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  430. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  431. package/src/task-orchestrator/surfaces/self-implement-pod.ts +112 -18
  432. package/src/telegram-commands.ts +27 -3
  433. package/src/telegram-dispatch-forward.ts +1 -1
  434. package/src/telegram-fabric-plan.ts +90 -0
  435. package/src/telegram-loops-command.ts +44 -0
  436. package/src/telegram-project-command.ts +10 -4
  437. package/src/telegram.ts +5 -0
  438. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  439. package/src/user-config.ts +204 -16
  440. package/src/version/packed-revision.json +1 -1
  441. package/src/workflow-runtime/executor.ts +4 -0
  442. package/src/workflow-runtime/node-catalog.ts +13 -0
  443. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  444. package/src/workflow-runtime/schema.ts +17 -1
  445. package/src/workflow-runtime/storage.ts +31 -13
  446. package/src/workflow-runtime/types.ts +13 -1
  447. package/apps/pwa/out/_next/static/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +0 -25
  459. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  463. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/app/chat/page-813c4a52c9d05ca8.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +0 -1
  492. package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +0 -1
  494. package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/app/market/page-dfca9b7d2de0b135.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/app/observatory/page-77447356baad75d2.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/app/outputs/page-6d604227e1664b71.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
  515. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  516. package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.js +0 -1
  517. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
  518. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
  519. package/apps/pwa/out/_next/static/chunks/app/tasks/page-2cddeb9e2aeedc55.js +0 -1
  520. package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +0 -1
  521. package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.js +0 -1
  522. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
  523. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
  524. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
  525. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +0 -1
  526. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +0 -1
  527. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  528. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  529. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  530. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  531. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  532. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  533. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  534. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
@@ -53,7 +53,7 @@ export function startApprovalGate(input: ApprovalGateInput, deps: ApprovalGateDe
53
53
  void Promise.resolve().then(() => (deps.runHostRegate ?? runHostRegate)({
54
54
  prNumber: input.prNumber, headCommit: input.headSha, repoRoot: input.repoRoot, verifyOnly: true,
55
55
  })).then(async (result) => {
56
- let status = result.status ?? (result.passed ? 'passed' : 'failed');
56
+ let status: ApprovalGateStatus = result.status === 'frozen' ? 'unmeasured' : result.status ?? (result.passed ? 'passed' : 'failed');
57
57
  let failures = result.failures.map(({ step, detail }) => `${step} — ${detail.replace(/\s+/g, ' ')}`.slice(0, 800));
58
58
  if (status === 'passed') {
59
59
  const current = await (deps.readPr ?? readPr)(input);
@@ -8,6 +8,7 @@ import {
8
8
  resolveStatePersonaDir,
9
9
  } from '../../persona/global-registry.js';
10
10
  import { clonePreset, editPersona, loadPresets } from '../../persona/presets.js';
11
+ import { resolvePersonaAddress } from '../../intake-plane/persona-answer.js';
11
12
  import type { PersonaRegistryEvent } from '../../persona/registry.js';
12
13
  import type { PersonaProfile } from '../../persona/types.js';
13
14
  import {
@@ -99,7 +100,7 @@ export async function handlePersonaGet(
99
100
  }
100
101
  await awaitGlobalPersonaLoad();
101
102
  const registry = getGlobalPersonaRegistry();
102
- const persona = registry.get(personaId);
103
+ const persona = resolvePersonaAddress(personaId, registry);
103
104
  if (!persona) return notFound(`persona ${personaId} not found`);
104
105
  return jsonResponse({ persona: toWire(persona) }, 200);
105
106
  }
@@ -193,9 +193,13 @@ function inventory(deps: SchedulesReadDeps): { cards: ScheduleCard[]; names: Map
193
193
  return { cards, names };
194
194
  }
195
195
 
196
+ export function readSchedulesInventory(deps: SchedulesReadDeps = {}): ScheduleCard[] {
197
+ return inventory(deps).cards;
198
+ }
199
+
196
200
  export function handleSchedulesList(req: Request, opts: MetaApiOpts, deps: SchedulesReadDeps = {}): Response {
197
201
  if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
198
- const { cards } = inventory(deps);
202
+ const cards = readSchedulesInventory(deps);
199
203
  const schedules = new URL(req.url).searchParams.get('includeOff') === '1' ? cards : cards.filter(c => c.state !== 'off');
200
204
  return jsonResponse({ schedules, count: schedules.length }, 200);
201
205
  }
@@ -0,0 +1,97 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { debug } from '../../debug/log.js';
5
+ import { effectiveInstanceRoot } from '../../instance/resolve.js';
6
+ import { dispatchSkillExec } from '../../tool-runtime/skill-exec-runtime.js';
7
+ import { jsonResponse } from './json-response.js';
8
+
9
+ export const SKILL_EXEC_PATH = '/v1/skills/exec';
10
+
11
+ type SkillJob = {
12
+ id: string;
13
+ skill: string;
14
+ status: 'running' | 'done' | 'rejected' | 'failed';
15
+ updatedAt: number;
16
+ ok?: boolean;
17
+ output?: string;
18
+ reason?: 'not-allowlisted';
19
+ error?: string;
20
+ };
21
+
22
+ type SkillExecDeps = {
23
+ rootDir?: string;
24
+ exec?: typeof dispatchSkillExec;
25
+ now?: () => number;
26
+ };
27
+
28
+ export function createSkillExecJobs({ rootDir = effectiveInstanceRoot(), exec = dispatchSkillExec, now = Date.now }: SkillExecDeps = {}) {
29
+ const base = join(rootDir, 'skill-exec');
30
+ const active = new Set<string>();
31
+ const unavailable = new Set<string>();
32
+ const file = (id: string) => join(base, `${id}.json`);
33
+ const save = (job: SkillJob) => {
34
+ mkdirSync(base, { recursive: true, mode: 0o700 });
35
+ chmodSync(base, 0o700);
36
+ writeFileSync(file(job.id), JSON.stringify(job), { mode: 0o600 });
37
+ chmodSync(file(job.id), 0o600);
38
+ };
39
+
40
+ return {
41
+ async post(req: Request): Promise<Response> {
42
+ let body: unknown;
43
+ try { body = await req.json(); } catch { body = null; }
44
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
45
+ const { skill, task } = body as Record<string, unknown>;
46
+ if (typeof skill !== 'string' || !skill.trim() || typeof task !== 'string' || !task.trim() || task.length > 4000) {
47
+ return jsonResponse({ error: 'bad_request' }, 400);
48
+ }
49
+ const id = randomUUID();
50
+ const update = (status: SkillJob['status'], fields: Partial<Pick<SkillJob, 'ok' | 'output' | 'reason' | 'error'>> = {}) =>
51
+ save({ id, skill, status, updatedAt: now(), ...fields });
52
+ update('running');
53
+ active.add(id);
54
+ debug.log('skill-exec.pwa', 'started', { id, skill });
55
+ setTimeout(() => {
56
+ void (async () => {
57
+ try {
58
+ const result = await exec({ skill, task });
59
+ if (!result.ok && result.output.startsWith(`자동 실행 대상 아님: ${skill}.`)) {
60
+ update('rejected', { reason: 'not-allowlisted' });
61
+ debug.log('skill-exec.pwa', 'rejected', { id, skill });
62
+ } else {
63
+ update('done', { ok: result.ok, output: result.output.slice(0, 20_000) });
64
+ debug.log('skill-exec.pwa', 'done', { id, skill });
65
+ }
66
+ } catch {
67
+ try {
68
+ update('failed', { error: 'skill-exec-failed' });
69
+ } catch {
70
+ unavailable.add(id);
71
+ }
72
+ debug.log('skill-exec.pwa', 'failed', { id, skill });
73
+ } finally {
74
+ active.delete(id);
75
+ }
76
+ })().catch(() => {
77
+ unavailable.add(id);
78
+ active.delete(id);
79
+ });
80
+ });
81
+ return jsonResponse({ id }, 202);
82
+ },
83
+ get(id: string): Response {
84
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) return jsonResponse({ error: 'not-found' }, 404);
85
+ if (unavailable.has(id)) return jsonResponse({ id, status: 'failed', error: 'skill-exec-failed' });
86
+ let job: SkillJob;
87
+ try { job = JSON.parse(readFileSync(file(id), 'utf8')) as SkillJob; }
88
+ catch { return jsonResponse({ error: 'not-found' }, 404); }
89
+ if (job.id !== id) return jsonResponse({ error: 'not-found' }, 404);
90
+ if (job.status === 'running' && !active.has(id)) return jsonResponse({ id, status: 'failed', error: 'daemon-restarted' });
91
+ const { status, ok, output, reason, error } = job;
92
+ return jsonResponse({ id, status, ...(ok !== undefined ? { ok } : {}), ...(output !== undefined ? { output } : {}), ...(reason ? { reason } : {}), ...(error ? { error } : {}) });
93
+ },
94
+ };
95
+ }
96
+
97
+ export type SkillExecJobs = ReturnType<typeof createSkillExecJobs>;
@@ -1,4 +1,8 @@
1
- import { CardStore } from '../../task-cards/card-store.js';
1
+ import { Database } from 'bun:sqlite';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { CardStore, type TaskCard } from '../../task-cards/card-store.js';
5
+ import { releaseLedgerRoot } from '../../instance/resolve.js';
2
6
  import { createWishCard } from '../../intake-plane/wish-card.js';
3
7
  import { jsonResponse } from './json-response.js';
4
8
 
@@ -7,19 +11,53 @@ export async function handleTaskCardsWishPost(req: Request, root?: string): Prom
7
11
  try { body = await req.json(); }
8
12
  catch { return jsonResponse({ error: 'bad_request' }, 400); }
9
13
  if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
10
- const { text, ref } = body as Record<string, unknown>;
11
- if (typeof text !== 'string' || !text.trim() || typeof ref !== 'string' || !ref.trim()) {
14
+ const { text, ref, sessionId } = body as Record<string, unknown>;
15
+ if (typeof text !== 'string' || !text.trim() || typeof ref !== 'string' || !ref.trim()
16
+ || typeof sessionId !== 'string' || !sessionId.trim()) {
12
17
  return jsonResponse({ error: 'bad_request' }, 400);
13
18
  }
14
19
  const store = new CardStore(root);
15
20
  try {
16
- const card = createWishCard({ text, source: 'pwa', ref }, store);
21
+ const card = createWishCard({ text, source: 'pwa', ref, replyTo: { surface: 'pwa', sessionId } }, store);
17
22
  return jsonResponse(card, card.created ? 201 : 200);
18
23
  } finally {
19
24
  store.close();
20
25
  }
21
26
  }
22
27
 
28
+ export interface WishPlacement { cellId: string; cellTitle: string; version: string; status: 'green' | 'yellow' | 'red' | 'done' }
29
+
30
+ export function isSplitCell(cell: unknown): cell is { id: string } {
31
+ return cell !== null && typeof cell === 'object' && 'id' in cell
32
+ && typeof cell.id === 'string' && cell.id.trim().length > 0;
33
+ }
34
+
35
+ /** Join the live checklist to the wish card by the split cell ID, never by title or card status. */
36
+ export function wishPlacements(card: TaskCard, root?: string): WishPlacement[] {
37
+ if (!card.goalId.startsWith('wish:')) return [];
38
+ const split = [...card.sections].reverse().find(section => section.key === 'orch:split');
39
+ if (!split) return [];
40
+ let cells: unknown;
41
+ try { cells = JSON.parse(split.content); } catch { return []; }
42
+ if (!Array.isArray(cells)) return [];
43
+ const ids = [...new Set(cells.filter(isSplitCell).map(cell => cell.id.trim()))];
44
+ if (!ids.length) return [];
45
+ const ledgerRoot = root ?? releaseLedgerRoot();
46
+ const path = join(ledgerRoot, 'release', 'features.sqlite');
47
+ if (!existsSync(path)) return [];
48
+ const db = new Database(path, { readonly: true, strict: true });
49
+ try {
50
+ const placements: WishPlacement[] = [];
51
+ for (const cellId of ids) {
52
+ const rows = db.query('SELECT a.version, a.status, COALESCE(a.title_override, f.title) AS title FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.feature_id = ? ORDER BY a.version').all(cellId) as Array<{ version: string; status: string; title: string }>;
53
+ for (const row of rows) if (['green', 'yellow', 'red', 'done'].includes(row.status)) {
54
+ placements.push({ cellId, cellTitle: row.title, version: row.version, status: row.status as WishPlacement['status'] });
55
+ }
56
+ }
57
+ return placements;
58
+ } finally { db.close(); }
59
+ }
60
+
23
61
  /** Read-only card API; route registration and authorization belong to the HTTP server. */
24
62
  export function handleTaskCardsGet(pathname: string, root?: string): Response {
25
63
  if (pathname !== '/v1/task-cards' && !/^\/v1\/task-cards\/[^/]+$/.test(pathname)) {
@@ -46,7 +84,8 @@ export function handleTaskCardsGet(pathname: string, root?: string): Response {
46
84
  const store = new CardStore(root);
47
85
  try {
48
86
  const card = store.getCard(id);
49
- return card ? jsonResponse({ card }) : jsonResponse({ error: 'not_found' }, 404);
87
+ return card ? jsonResponse({ card, ...(card.goalId.startsWith('wish:') ? { placements: wishPlacements(card, root) } : {}) })
88
+ : jsonResponse({ error: 'not_found' }, 404);
50
89
  } finally {
51
90
  store.close();
52
91
  }
@@ -302,6 +302,7 @@ export async function handleWorkflowPut(
302
302
  }
303
303
  const r = saveWorkflow(name, yamlText, { scope });
304
304
  if (!r.ok) {
305
+ if (r.readonly) return jsonResponse({ error: 'readonly_workflow', reason: r.error }, 409);
305
306
  return jsonResponse(
306
307
  {
307
308
  error: 'invalid_workflow',
@@ -323,7 +324,7 @@ export function handleWorkflowDelete(
323
324
  const url = new URL(req.url);
324
325
  const scope = url.searchParams.get('scope') === 'global' ? 'global' : 'project';
325
326
  const r = deleteWorkflow(name, { scope });
326
- if (!r.ok) return jsonResponse({ error: 'delete_failed', reason: r.error }, 404);
327
+ if (!r.ok) return jsonResponse({ error: r.readonly ? 'readonly_workflow' : 'delete_failed', reason: r.error }, r.readonly ? 409 : 404);
327
328
  return jsonResponse({ ok: true, path: r.path, scope }, 200);
328
329
  }
329
330
 
@@ -138,6 +138,22 @@ export function validatePersonaShape(
138
138
  models = mr.models;
139
139
  }
140
140
 
141
+ let seat: string | undefined;
142
+ if (Object.hasOwn(obj, 'seat')) {
143
+ if (typeof obj['seat'] !== 'string' || !obj['seat'].trim()) {
144
+ return err(path, 'seat must be a non-empty string');
145
+ }
146
+ seat = obj['seat'];
147
+ }
148
+
149
+ let todo: string | undefined;
150
+ if (Object.hasOwn(obj, 'todo')) {
151
+ if (typeof obj['todo'] !== 'string' || !/^[a-z0-9_][a-z0-9_-]*\.todo\.jsonl$/i.test(obj['todo'])) {
152
+ return err(path, 'todo must be a per-persona .todo.jsonl filename');
153
+ }
154
+ todo = obj['todo'];
155
+ }
156
+
141
157
  let mentionPatterns: readonly string[] | undefined;
142
158
  if (obj['mentionPatterns'] !== undefined) {
143
159
  if (!Array.isArray(obj['mentionPatterns'])
@@ -149,6 +165,8 @@ export function validatePersonaShape(
149
165
 
150
166
  const profile: PersonaProfile = {
151
167
  personaId, displayName,
168
+ ...(seat !== undefined ? { seat } : {}),
169
+ ...(todo !== undefined ? { todo } : {}),
152
170
  ...(description.value !== undefined ? { description: description.value } : {}),
153
171
  ...(systemPrompt.value !== undefined ? { systemPrompt: systemPrompt.value } : {}),
154
172
  ...(brand !== undefined ? { brand } : {}),
@@ -6,6 +6,8 @@ export interface PersonaTodo {
6
6
  title: string;
7
7
  status: 'open' | 'done';
8
8
  createdAt: string;
9
+ priority?: number;
10
+ dueAt?: string;
9
11
  }
10
12
 
11
13
  export interface PersonaTodoReadResult {
@@ -28,7 +30,9 @@ function isPersonaTodo(value: unknown): value is PersonaTodo {
28
30
  && typeof item.title === 'string' && item.title.length > 0
29
31
  && (item.status === 'open' || item.status === 'done')
30
32
  && typeof item.createdAt === 'string'
31
- && Number.isFinite(Date.parse(item.createdAt));
33
+ && Number.isFinite(Date.parse(item.createdAt))
34
+ && (item.priority === undefined || (typeof item.priority === 'number' && Number.isInteger(item.priority) && item.priority >= 0))
35
+ && (item.dueAt === undefined || (typeof item.dueAt === 'string' && Number.isFinite(Date.parse(item.dueAt))));
32
36
  }
33
37
 
34
38
  /** Missing files are empty; malformed lines are skipped and returned for observation. */
@@ -67,13 +71,14 @@ export function writePersonaTodos(dir: string, personaId: string, todos: readonl
67
71
  writeFileSync(path, todos.map(todo => JSON.stringify(todo)).join('\n') + (todos.length ? '\n' : ''), 'utf8');
68
72
  }
69
73
 
70
- /** Earliest created open item; original order breaks equal-timestamp ties. */
74
+ /** Higher priority first, then earlier deadline, then earlier creation; file order breaks ties. */
75
+ export function orderedPersonaTodos(todos: readonly PersonaTodo[]): PersonaTodo[] {
76
+ return [...todos].filter((todo) => todo.status === 'open').sort((a, b) =>
77
+ (b.priority ?? 0) - (a.priority ?? 0)
78
+ || (a.dueAt && b.dueAt ? Date.parse(a.dueAt) - Date.parse(b.dueAt) : a.dueAt ? -1 : b.dueAt ? 1 : 0)
79
+ || Date.parse(a.createdAt) - Date.parse(b.createdAt));
80
+ }
81
+
71
82
  export function nextPersonaTodo(todos: readonly PersonaTodo[]): PersonaTodo | null {
72
- let oldest: PersonaTodo | null = null;
73
- for (const todo of todos) {
74
- if (todo.status === 'open' && (!oldest || Date.parse(todo.createdAt) < Date.parse(oldest.createdAt))) {
75
- oldest = todo;
76
- }
77
- }
78
- return oldest;
83
+ return orderedPersonaTodos(todos)[0] ?? null;
79
84
  }
@@ -88,6 +88,7 @@ export function presetToProfileYaml(p: PersonaPreset, common: PresetCommon, opts
88
88
  displayName: opts.name?.trim() ? `${opts.name.trim()} · ${p.role}` : p.displayName,
89
89
  description: p.oneLine,
90
90
  systemPrompt: presetSystemPrompt(p, common, name),
91
+ todo: `${opts.personaId}.todo.jsonl`,
91
92
  preset: { id: p.personaId, role: p.role, ...(title ? { title } : {}), tools: p.tools.map((t) => t.name), firstQuestions: p.firstQuestions },
92
93
  };
93
94
  return `# 프리셋 «${p.personaId}» 에서 만든 페르소나 — elanous persona add (원본: persona-presets/${p.personaId}.yaml)\n${stringify(profile, { lineWidth: 0 })}`;
@@ -99,7 +100,7 @@ export interface PersonaWriteResult { personaId: string; displayName: string; pa
99
100
  // Only fields in the runtime persona schema may be edited; provenance (`preset`) is retained, not editable.
100
101
  const PROFILE_KEYS = new Set([
101
102
  'personaId', 'displayName', 'description', 'systemPrompt', 'actionHosts', 'offsiteNavigation',
102
- 'brand', 'models', 'mentionPatterns', 'avatarUrl', 'brandColor', 'browserPort', 'residence', 'capabilities',
103
+ 'brand', 'models', 'mentionPatterns', 'avatarUrl', 'brandColor', 'browserPort', 'residence', 'capabilities', 'todo',
103
104
  ]);
104
105
  const NESTED_KEYS: Record<string, readonly string[]> = {
105
106
  models: ['primary', 'fallback', 'providers'], 'models.providers': ['ollama', 'openai'],
@@ -0,0 +1,105 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { parseDocument } from 'yaml';
4
+ import { parsePersonaYaml } from './loader.js';
5
+
6
+ export interface TeamMember {
7
+ persona: string;
8
+ title: string;
9
+ checklistTemplate: string[];
10
+ }
11
+
12
+ export interface TeamHandoff {
13
+ from: string;
14
+ to: string;
15
+ }
16
+
17
+ export interface TeamDefinition {
18
+ name: string;
19
+ members: TeamMember[];
20
+ handoffs: TeamHandoff[];
21
+ }
22
+
23
+ function mapping(value: unknown): value is Record<string, unknown> {
24
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
25
+ }
26
+
27
+ function fields(value: Record<string, unknown>, required: readonly string[], label: string): void {
28
+ for (const key of required) {
29
+ if (!Object.hasOwn(value, key)) throw new Error(`${label}.${key} is required`);
30
+ }
31
+ for (const key of Object.keys(value)) {
32
+ if (!required.includes(key)) throw new Error(`${label}.${key} is not a team schema field`);
33
+ }
34
+ }
35
+
36
+ function text(value: unknown, label: string): string {
37
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${label} must be a non-empty string`);
38
+ return value;
39
+ }
40
+
41
+ /** Read-only validation: resolve persona paths relative to the team file, not the caller's cwd. */
42
+ export function validateTeamFile(file: string): TeamDefinition {
43
+ const document = parseDocument(readFileSync(file, 'utf8'), { uniqueKeys: true });
44
+ if (document.errors.length) throw new Error(`invalid team YAML: ${document.errors[0]!.message}`);
45
+ const raw: unknown = document.toJS();
46
+ if (!mapping(raw)) throw new Error('team must be a YAML mapping');
47
+ fields(raw, ['name', 'members', 'handoffs'], 'team');
48
+ const name = text(raw.name, 'team.name');
49
+ if (!Array.isArray(raw.members) || raw.members.length === 0) {
50
+ throw new Error('team.members must be a non-empty list');
51
+ }
52
+ const titles = new Set<string>();
53
+ const personaIds = new Set<string>();
54
+ const members: TeamMember[] = raw.members.map((item: unknown, index: number) => {
55
+ const label = `team.members[${index}]`;
56
+ if (!mapping(item)) throw new Error(`${label} must be a mapping`);
57
+ fields(item, ['persona', 'title', 'checklistTemplate'], label);
58
+ const persona = text(item.persona, `${label}.persona`);
59
+ const title = text(item.title, `${label}.title`);
60
+ if (titles.has(title)) throw new Error(`duplicate team title: ${title}`);
61
+ titles.add(title);
62
+ if (!Array.isArray(item.checklistTemplate) || item.checklistTemplate.length === 0) {
63
+ throw new Error(`${label}.checklistTemplate must be a non-empty list of strings`);
64
+ }
65
+ const checklistTemplate = item.checklistTemplate.map((entry: unknown) => text(entry, `${label}.checklistTemplate item`));
66
+ const personaPath = resolve(dirname(file), persona);
67
+ let yaml: string;
68
+ try {
69
+ yaml = readFileSync(personaPath, 'utf8');
70
+ } catch (error) {
71
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') throw new Error(`persona not found: ${persona} (${title})`);
72
+ throw error;
73
+ }
74
+ const parsed = parsePersonaYaml(yaml, personaPath);
75
+ if (!parsed.ok) throw new Error(`invalid persona ${persona}: ${parsed.error.message}`);
76
+ if (parsed.profile.seat !== title) throw new Error(`persona seat mismatch: ${persona} requires seat ${title}`);
77
+ if (personaIds.has(parsed.profile.personaId)) throw new Error(`duplicate personaId: ${parsed.profile.personaId}`);
78
+ personaIds.add(parsed.profile.personaId);
79
+ return { persona, title, checklistTemplate };
80
+ });
81
+ if (!Array.isArray(raw.handoffs)) throw new Error('team.handoffs must be a list');
82
+ const handoffs: TeamHandoff[] = raw.handoffs.map((item: unknown, index: number) => {
83
+ const label = `team.handoffs[${index}]`;
84
+ if (!mapping(item)) throw new Error(`${label} must be a mapping`);
85
+ fields(item, ['from', 'to'], label);
86
+ const from = text(item.from, `${label}.from`);
87
+ const to = text(item.to, `${label}.to`);
88
+ if (!titles.has(from) || !titles.has(to)) throw new Error(`handoff references missing title: ${from} -> ${to}`);
89
+ return { from, to };
90
+ });
91
+ const edges = new Map([...titles].map((title) => [title, [] as string[]]));
92
+ for (const { from, to } of handoffs) edges.get(from)!.push(to);
93
+ const visiting = new Set<string>();
94
+ const visited = new Set<string>();
95
+ const visit = (title: string): void => {
96
+ if (visiting.has(title)) throw new Error(`handoff cycle at ${title}`);
97
+ if (visited.has(title)) return;
98
+ visiting.add(title);
99
+ for (const next of edges.get(title)!) visit(next);
100
+ visiting.delete(title);
101
+ visited.add(title);
102
+ };
103
+ for (const title of titles) visit(title);
104
+ return { name, members, handoffs };
105
+ }
@@ -19,6 +19,10 @@ export type PersonaBrand =
19
19
  export interface PersonaProfile {
20
20
  readonly personaId: string;
21
21
  readonly displayName: string;
22
+ /** Seat address (title, id or alias) represented by this persona. */
23
+ readonly seat?: string;
24
+ /** Per-persona JSONL todo filename in the state persona directory. */
25
+ readonly todo?: string;
22
26
  readonly description?: string;
23
27
 
24
28
  /** System prompt to prepend in LLM calls. Markdown text. May
@@ -2,12 +2,32 @@
2
2
  // in a per-user directory; a launch is admitted only while live leases stay under the pool's measured total.
3
3
  // Without this, independent CLI processes each measured an empty pool and all launched (review round 2 of #23212).
4
4
  import { spawnSync } from 'node:child_process';
5
- import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
5
+ import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
6
6
  import { tmpdir, userInfo } from 'node:os';
7
7
  import { join } from 'node:path';
8
8
  import { debug } from '../debug/log.js';
9
9
 
10
- export interface HostLeaseRecord { pid: number; startedAt: string; at: string }
10
+ export interface HostLeaseRecord { pid: number; startedAt: string; at: string; stage?: 'authoring' | 'job'; claimed?: boolean; job?: string; context?: string; namespace?: string }
11
+
12
+ export interface PendingJobIdentity { context: string; namespace: string; job: string }
13
+
14
+ /** Only the same cluster, namespace and Job can account for an existing host lease. */
15
+ export function leaseHasPendingPod(lease: HostLeaseRecord, pending: readonly PendingJobIdentity[]): boolean {
16
+ return lease.stage === 'job' && !!lease.context && !!lease.namespace && !!lease.job &&
17
+ pending.some((pod) => pod.context === lease.context && pod.namespace === lease.namespace && pod.job === lease.job);
18
+ }
19
+
20
+ export interface HostLeaseHandle {
21
+ (): void;
22
+ readonly id: string;
23
+ /** Job creation is not release: retain the lease until its Pod is observed Running or terminal. */
24
+ applied: (job: string, context: string, namespace: string) => void;
25
+ }
26
+
27
+ /** A Job without a Running/terminal Pod still occupies a host reservation. */
28
+ export function hostLeaseCounts(records: readonly HostLeaseRecord[]): { reserved: number; waitingJobs: number } {
29
+ return { reserved: records.filter((r) => r.stage !== 'job').length, waitingJobs: records.filter((r) => r.stage === 'job').length };
30
+ }
11
31
 
12
32
  export interface HostPoolLeaseOptions {
13
33
  /** Base directory — default `$TMPDIR/elanous-pod-leases-<uid>` (0700). */
@@ -53,8 +73,42 @@ export class HostPoolLease {
53
73
  this.staleLockMs = options.staleLockMs ?? 10_000;
54
74
  }
55
75
 
76
+ reservationIsOther(id: string): boolean {
77
+ if (!/^[A-Za-z0-9._-]+\.json$/.test(id)) return false;
78
+ try {
79
+ const record = JSON.parse(readFileSync(join(this.dir, id), 'utf8')) as HostLeaseRecord;
80
+ return record.stage === 'authoring' && Number.isSafeInteger(record.pid) && record.pid > 0 &&
81
+ typeof record.startedAt === 'string' && this.processStart(record.pid) === record.startedAt &&
82
+ (record.pid !== this.pid || record.startedAt !== this.selfStart());
83
+ } catch { return false; }
84
+ }
85
+
86
+ /** Transfer a reservation across the authoring → orchestrator process boundary. */
87
+ claim(id: string): HostLeaseHandle | null {
88
+ if (!/^[A-Za-z0-9._-]+\.json$/.test(id)) return null;
89
+ const file = join(this.dir, id);
90
+ const deadline = Date.now() + this.staleLockMs;
91
+ while (!this.lock()) {
92
+ if (Date.now() >= deadline) return null;
93
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
94
+ }
95
+ try {
96
+ if (this.ownStart === undefined) this.ownStart = this.processStart(this.pid);
97
+ if (!this.ownStart) return null;
98
+ const original = JSON.parse(readFileSync(file, 'utf8')) as HostLeaseRecord;
99
+ if (original.stage !== 'authoring' || original.claimed || !Number.isSafeInteger(original.pid) || original.pid <= 0 ||
100
+ typeof original.startedAt !== 'string' || this.processStart(original.pid) !== original.startedAt) return null;
101
+ const record: HostLeaseRecord = { ...original, pid: this.pid, startedAt: this.ownStart, claimed: true };
102
+ this.replaceRecord(file, record);
103
+ return this.handle(file, record, id);
104
+ } catch { return null; }
105
+ finally { this.unlock(); }
106
+ }
107
+
56
108
  /** Live leases after removing stale ones (dead pid or a recycled pid whose start time differs). */
57
- live(): HostLeaseRecord[] {
109
+ live(): HostLeaseRecord[] { return this.readLive(false); }
110
+
111
+ private readLive(lockHeld: boolean): HostLeaseRecord[] {
58
112
  const out: HostLeaseRecord[] = [];
59
113
  let names: string[] = [];
60
114
  try { names = readdirSync(this.dir).filter((name) => name.endsWith('.json')); } catch { return out; }
@@ -62,50 +116,106 @@ export class HostPoolLease {
62
116
  const file = join(this.dir, name);
63
117
  let record: HostLeaseRecord | null = null;
64
118
  try { record = JSON.parse(readFileSync(file, 'utf8')) as HostLeaseRecord; } catch { record = null; }
65
- const alive = record && this.processStart(record.pid) === record.startedAt;
119
+ const alive = record && Number.isSafeInteger(record.pid) && record.pid > 0 && typeof record.startedAt === 'string'
120
+ && this.processStart(record.pid) === record.startedAt;
66
121
  if (record && alive) { out.push(record); continue; }
67
- try { unlinkSync(file); } catch { /* another process cleaned it */ }
68
- debug.log('pod.lease', 'host-lease-stale-cleared', { file: name, pid: record?.pid ?? null });
122
+ // A concurrent claim can replace the record while we check the original PID.
123
+ if (!lockHeld && !this.lock()) { if (record) out.push(record); continue; }
124
+ try {
125
+ let current: HostLeaseRecord | null;
126
+ try { current = JSON.parse(readFileSync(file, 'utf8')) as HostLeaseRecord; } catch { current = null; }
127
+ if (current && Number.isSafeInteger(current.pid) && current.pid > 0 && typeof current.startedAt === 'string' &&
128
+ this.processStart(current.pid) === current.startedAt) { out.push(current); continue; }
129
+ try { unlinkSync(file); } catch { /* already cleaned */ }
130
+ debug.log('pod.lease', 'host-lease-stale-cleared', { file: name, pid: current?.pid ?? null });
131
+ } finally { if (!lockHeld) this.unlock(); }
69
132
  }
70
133
  return out;
71
134
  }
72
135
 
73
- /** Unapplied leases held by other processes — launches admitted elsewhere whose Job the cluster can't see yet. */
74
- othersPending(): number {
136
+ selfStart(): string | null {
75
137
  if (this.ownStart === undefined) this.ownStart = this.processStart(this.pid);
76
- return this.live().filter((r) => !(r.pid === this.pid && r.startedAt === this.ownStart)).length;
138
+ return this.ownStart;
139
+ }
140
+
141
+ /** Reservations held by other processes while the cluster cannot yet observe them. */
142
+ othersPending(): number {
143
+ return this.live().filter((r) => !(r.pid === this.pid && r.startedAt === this.selfStart())).length;
77
144
  }
78
145
 
79
146
  /**
80
- * Under the host lock, take a lease only if `allowed(othersPending)` holds — the check and the write are one step,
81
- * so independent processes measuring the same empty pool cannot all pass. Null = refused or lock busy (retry later).
147
+ * Under the host lock, take a lease only if `allowed(pending)` holds — the check and the write are one step.
148
+ * By default pending excludes this process's permits (the local admission gate owns them); includeOwn
149
+ * counts every reservation for independent authoring launches from the same process.
150
+ * Null = refused or lock busy (retry later).
82
151
  */
83
- tryReserve(allowed: (othersPending: number) => boolean): (() => void) | null {
152
+ tryReserve(allowed: (othersPending: number) => boolean, includeOwn = false): HostLeaseHandle | null {
84
153
  try { mkdirSync(this.dir, { recursive: true, mode: 0o700 }); } catch { return null; }
85
154
  if (!this.lock()) return null;
86
155
  try {
87
156
  if (this.ownStart === undefined) this.ownStart = this.processStart(this.pid);
88
157
  if (!this.ownStart) return null;
89
- const others = this.live().filter((r) => !(r.pid === this.pid && r.startedAt === this.ownStart)).length;
158
+ const others = this.readLive(true).filter((r) => includeOwn || !(r.pid === this.pid && r.startedAt === this.ownStart)).length;
90
159
  if (!allowed(others)) {
91
160
  debug.log('pod.lease', 'host-lease-refused', { othersPending: others });
92
161
  return null;
93
162
  }
94
163
  const file = join(this.dir, `${this.pid}-${this.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
95
- const record: HostLeaseRecord = { pid: this.pid, startedAt: this.ownStart, at: new Date(this.now()).toISOString() };
164
+ const record: HostLeaseRecord = { pid: this.pid, startedAt: this.ownStart, at: new Date(this.now()).toISOString(), stage: 'authoring' };
96
165
  writeFileSync(file, JSON.stringify(record), { flag: 'wx', mode: 0o600 });
97
166
  debug.log('pod.lease', 'host-lease-acquired', { othersPending: others });
98
- let released = false;
99
- return () => {
100
- if (released) return;
101
- released = true;
102
- try { unlinkSync(file); } catch { /* already cleaned */ }
103
- };
167
+ return this.handle(file, record, file.slice(this.dir.length + 1));
104
168
  } finally {
105
169
  this.unlock();
106
170
  }
107
171
  }
108
172
 
173
+ private replaceRecord(file: string, record: HostLeaseRecord): void {
174
+ const temp = `${file}.${this.pid}-${Math.random().toString(36).slice(2)}.tmp`;
175
+ try {
176
+ writeFileSync(temp, JSON.stringify(record), { flag: 'wx', mode: 0o600 });
177
+ renameSync(temp, file);
178
+ } catch (error) {
179
+ try { unlinkSync(temp); } catch { /* already gone */ }
180
+ throw error;
181
+ }
182
+ }
183
+
184
+ private handle(file: string, record: HostLeaseRecord, id: string): HostLeaseHandle {
185
+ let released = false;
186
+ const release = (() => {
187
+ if (released) return;
188
+ // Claim replaces the same file under this lock. An old owner must not unlink a
189
+ // newly transferred reservation between reading and removing its record.
190
+ const deadline = Date.now() + this.staleLockMs;
191
+ while (!this.lock()) {
192
+ if (Date.now() >= deadline) return;
193
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
194
+ }
195
+ try {
196
+ const current = JSON.parse(readFileSync(file, 'utf8')) as HostLeaseRecord;
197
+ if (current.pid === record.pid && current.startedAt === record.startedAt && current.claimed === record.claimed) unlinkSync(file);
198
+ } catch { /* transferred or already cleaned */ }
199
+ finally { released = true; this.unlock(); }
200
+ }) as HostLeaseHandle;
201
+ Object.defineProperty(release, 'id', { value: id });
202
+ release.applied = (job, context, namespace) => {
203
+ if (released) return;
204
+ const deadline = Date.now() + this.staleLockMs;
205
+ while (!this.lock()) {
206
+ if (Date.now() >= deadline) throw new Error('pod lease: cannot update Job reservation');
207
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
208
+ }
209
+ try {
210
+ const current = JSON.parse(readFileSync(file, 'utf8')) as HostLeaseRecord;
211
+ if (current.pid !== record.pid || current.startedAt !== record.startedAt || current.claimed !== record.claimed) throw new Error('pod lease: reservation owner changed');
212
+ if (!job || !context || !namespace) throw new Error('pod lease: Job identity requires context, namespace and name');
213
+ this.replaceRecord(file, { ...record, stage: 'job', job, context, namespace });
214
+ } finally { this.unlock(); }
215
+ };
216
+ return release;
217
+ }
218
+
109
219
  private lockDir(): string { return join(this.dir, '.lock'); }
110
220
 
111
221
  private lock(): boolean {