elanous 0.2.14 → 0.2.15

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 (293) 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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
  53. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +14 -14
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +14 -14
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +14 -14
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +14 -14
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +14 -14
  64. package/apps/pwa/out/ceo/index.html +1 -0
  65. package/apps/pwa/out/ceo/index.txt +33 -0
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +14 -14
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +14 -14
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +14 -14
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +14 -14
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +14 -14
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +14 -14
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +14 -14
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +14 -14
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +14 -14
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +14 -14
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +14 -14
  89. package/apps/pwa/out/loops/index.html +1 -1
  90. package/apps/pwa/out/loops/index.txt +14 -14
  91. package/apps/pwa/out/market/index.html +1 -1
  92. package/apps/pwa/out/market/index.txt +14 -14
  93. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  94. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  95. package/apps/pwa/out/morning/index.html +1 -1
  96. package/apps/pwa/out/morning/index.txt +14 -14
  97. package/apps/pwa/out/observatory/index.html +1 -1
  98. package/apps/pwa/out/observatory/index.txt +14 -14
  99. package/apps/pwa/out/ops/checklist/index.html +1 -1
  100. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  101. package/apps/pwa/out/ops/release/index.html +1 -1
  102. package/apps/pwa/out/ops/release/index.txt +14 -14
  103. package/apps/pwa/out/ops/seats/index.html +1 -1
  104. package/apps/pwa/out/ops/seats/index.txt +14 -14
  105. package/apps/pwa/out/outputs/index.html +1 -1
  106. package/apps/pwa/out/outputs/index.txt +14 -14
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +15 -15
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +14 -14
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +14 -14
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +14 -14
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +14 -14
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +14 -14
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +14 -14
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +14 -14
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +14 -14
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +14 -14
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +14 -14
  129. package/apps/pwa/out/today/index.html +1 -1
  130. package/apps/pwa/out/today/index.txt +14 -14
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +14 -14
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +14 -14
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +14 -14
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +14 -14
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +14 -14
  143. package/graphs/heal/heal-loop.yaml +1 -1
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +4 -2
  147. package/package.json +1 -1
  148. package/scripts/brand/check.ts +7 -5
  149. package/scripts/ci-typecheck-changed.ts +50 -18
  150. package/scripts/docs/generate-graph-anatomy.ts +315 -0
  151. package/scripts/expose-rubric-suggest.ts +18 -12
  152. package/scripts/lecture-note/node.ts +31 -11
  153. package/scripts/lessons/handbook-page.ts +30 -3
  154. package/scripts/marketing/generate-ax-brief.ts +140 -0
  155. package/scripts/marketing/generate-feature-map.ts +118 -0
  156. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  157. package/scripts/release-loop/gate-node.ts +80 -14
  158. package/scripts/release-loop/vault-note-node.ts +240 -8
  159. package/scripts/release-story/draft.ts +57 -9
  160. package/scripts/rhythm/daily.ts +263 -76
  161. package/scripts/rhythm/weekly.ts +30 -9
  162. package/scripts/smart-merge/resync-shadow.ts +147 -0
  163. package/src/agent-env/profile.ts +161 -0
  164. package/src/agent-substrate/pr-reviewer.ts +2 -0
  165. package/src/ask-user-question/pending-questions.ts +5 -0
  166. package/src/auth/github-app-token.ts +112 -13
  167. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  168. package/src/briefing/brief-items.ts +121 -0
  169. package/src/claims/claims-render.ts +14 -5
  170. package/src/cli/agent-env-cli.ts +21 -0
  171. package/src/cli/decisions-cli.ts +9 -0
  172. package/src/cli/doctor-cli.ts +23 -1
  173. package/src/cli/doctor-node-modules.ts +147 -0
  174. package/src/cli/intake-cli.ts +46 -17
  175. package/src/cli/pod-cli.ts +2 -0
  176. package/src/cli/release-cli.ts +61 -27
  177. package/src/cli/seat-requests-cli.ts +52 -0
  178. package/src/context-bus/context-now-public.ts +16 -6
  179. package/src/context-bus/context-now.ts +3 -1
  180. package/src/context-bus/coord-events.ts +6 -4
  181. package/src/decisions/decision-cards.ts +33 -7
  182. package/src/decisions/proact-meter.ts +286 -0
  183. package/src/decisions/proact1-lite.ts +86 -0
  184. package/src/decisions/web-push-decision-cards.ts +1 -1
  185. package/src/domains/alert-audit.ts +271 -0
  186. package/src/domains/core-tools.ts +4 -3
  187. package/src/domains/ops-facts-tool.ts +31 -1
  188. package/src/harness/harness-cli-command.ts +111 -15
  189. package/src/harness/harness-incidents.ts +2 -0
  190. package/src/harness/harness-pod-dispatch.ts +100 -11
  191. package/src/harness/harness-queue.ts +380 -50
  192. package/src/harness/helper-repair.ts +87 -0
  193. package/src/harness/helper-scan.ts +16 -4
  194. package/src/harness/launch-stamp.ts +236 -0
  195. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  196. package/src/harness/ledger-write-guard.ts +35 -0
  197. package/src/harness/pod-memory-policy.ts +15 -0
  198. package/src/harness/repository-name.ts +55 -0
  199. package/src/hq/fence-audit.ts +90 -0
  200. package/src/hq/hq.ts +62 -4
  201. package/src/hq/lease.ts +4 -4
  202. package/src/hq/move-plan.ts +126 -0
  203. package/src/hq/promote.ts +31 -0
  204. package/src/index.ts +77 -7
  205. package/src/intake-plane/check.ts +63 -0
  206. package/src/intake-plane/digest.ts +37 -15
  207. package/src/intake-plane/front-classifier.ts +6 -0
  208. package/src/intake-plane/items.ts +3 -2
  209. package/src/intake-plane/lens.ts +177 -0
  210. package/src/intake-plane/route.ts +145 -1
  211. package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
  212. package/src/launch-head/seat-goal-queue.ts +198 -0
  213. package/src/loops/budget.ts +36 -5
  214. package/src/loops/checker.ts +2 -2
  215. package/src/loops/orchestrator/author-depth.ts +65 -9
  216. package/src/loops/orchestrator/author-input-check.ts +8 -3
  217. package/src/loops/orchestrator/author-ledger.ts +3 -1
  218. package/src/loops/orchestrator/seat-share.ts +56 -0
  219. package/src/loops/orchestrator/tick.ts +11 -4
  220. package/src/loops/orchestrator/traffic.ts +117 -11
  221. package/src/loops/registry.ts +142 -61
  222. package/src/maturity/feature-maturity.ts +2 -1
  223. package/src/maturity/tui-slash-availability.ts +31 -0
  224. package/src/mss/logging/log-store.ts +35 -4
  225. package/src/nexus/api/http-server.ts +28 -14
  226. package/src/nexus/api/loop-edges.ts +173 -0
  227. package/src/nexus/api/schedules-read.ts +13 -0
  228. package/src/nexus/api/seat-requests.ts +79 -56
  229. package/src/prompt-enhance/enhance.ts +6 -1
  230. package/src/release-loop/auto-start.ts +6 -3
  231. package/src/release-loop/checklist.ts +13 -4
  232. package/src/release-loop/feature-store.ts +58 -31
  233. package/src/release-loop/merged-pr-checklist.ts +125 -0
  234. package/src/release-loop/placement.ts +86 -26
  235. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  236. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  237. package/src/seat-loop/seat-loop.ts +126 -10
  238. package/src/self-dev/dev-pipeline.ts +8 -4
  239. package/src/self-dev/draft-sweep.ts +90 -10
  240. package/src/self-dev/draft-triage-rules.ts +25 -8
  241. package/src/self-dev/orchestrate.ts +15 -1
  242. package/src/self-dev/review-provider-fallback.ts +6 -1
  243. package/src/self-dev/run-supervisor.ts +49 -2
  244. package/src/self-implement/frozen-merges.ts +3 -2
  245. package/src/self-implement/goal-author.ts +43 -5
  246. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  247. package/src/self-implement/heal-intake.ts +120 -2
  248. package/src/self-implement/host-regate.ts +9 -2
  249. package/src/self-implement/l8-merge-queue.ts +132 -0
  250. package/src/self-implement/orchestrator.ts +187 -40
  251. package/src/self-implement/pipeline-shape.ts +1 -1
  252. package/src/self-implement/revert-guard.ts +42 -7
  253. package/src/self-implement/run-ledger.ts +2 -0
  254. package/src/self-implement/run-status-mapping.ts +2 -0
  255. package/src/self-implement/seams.ts +1 -1
  256. package/src/self-implement/self-implement-cli.ts +1 -0
  257. package/src/self-implement/speed-claims.ts +338 -0
  258. package/src/steward/triage.ts +74 -3
  259. package/src/steward/working-backwards.ts +4 -1
  260. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  261. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  262. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  263. package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
  264. package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
  265. package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
  266. package/src/telegram.ts +25 -4
  267. package/src/user-config.ts +29 -5
  268. package/src/version/packed-revision.json +1 -1
  269. package/src/workflow-runtime/nodes/bash.ts +9 -1
  270. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
  291. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  292. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  293. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
@@ -1,5 +1,7 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { join } from 'node:path';
4
+ import { effectiveInstanceRoot } from '../../instance/resolve.js';
3
5
  import { debug } from '../../debug/log.js';
4
6
  import { defaultListHarnessProcesses } from '../../harness/harness-cli-command.js';
5
7
  import { listHarnessQueue, type QueueItem } from '../../harness/harness-queue.js';
@@ -7,15 +9,19 @@ import { releaseLedgerRoot } from '../../instance/resolve.js';
7
9
  import { listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
8
10
  import { listSchedules } from '../../release-loop/release-schedule.js';
9
11
  import { getUserConfig, ORCHESTRATOR_DEFAULTS, type OrchestratorSeat } from '../../user-config.js';
12
+ import { checkAuthorInput } from './author-input-check.js';
13
+ import { AuthorLedger } from './author-ledger.js';
10
14
  import { collectWork, findOverlaps, type WorkItem } from './overlap.js';
11
15
  import { seatOfTree, trafficTick, TRAFFIC_SEATS } from './traffic.js';
12
16
 
13
- export type AuthorCell = Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status'> & { version: string };
17
+ export type AuthorCell = Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'> & { version: string };
14
18
  export type AuthorQueueRow = Pick<QueueItem, 'seat' | 'status'> & Partial<Pick<QueueItem, 'input' | 'kind'>>;
15
19
  export type AuthorDepthResult = {
16
20
  seats: Array<{ seat: OrchestratorSeat; cap: number | null; running: number | null; queued: number | null;
17
21
  target: number | null; short: number | null; wouldAuthor: Array<{ cellId: string; version: string; title: string }> }>;
18
22
  unreadable: Array<{ source: string; reason: string }>;
23
+ receiptCounts?: { held: number; queuedForAuthor: number };
24
+ receiptFailures?: number;
19
25
  };
20
26
  export type AuthorDepthInput = {
21
27
  seats: readonly OrchestratorSeat[];
@@ -84,6 +90,7 @@ export function authorDepth({ seats, caps, running, queued, cells, overlaps, now
84
90
  }
85
91
 
86
92
  export interface CollectAuthorDepthDeps {
93
+ root?: string;
87
94
  seats?: readonly OrchestratorSeat[];
88
95
  now?: Date;
89
96
  caps?: () => Partial<Record<OrchestratorSeat, number>>;
@@ -92,12 +99,13 @@ export interface CollectAuthorDepthDeps {
92
99
  cells?: (version: string) => readonly AuthorCell[];
93
100
  overlaps?: () => readonly WorkItem[];
94
101
  versions?: () => readonly [string, string];
102
+ ledger?: Pick<AuthorLedger, 'request'>;
95
103
  log?: (category: string, event: string, data: Record<string, unknown>) => void;
96
104
  }
97
105
 
98
- function defaultVersions(now: Date): readonly [string, string] {
99
- if (!existsSync(join(releaseLedgerRoot(), 'release', 'features.sqlite'))) throw new Error('release schedule ledger unavailable');
100
- const upcoming = listSchedules().filter(row => row.cutAt && Date.parse(row.cutAt) > now.getTime())
106
+ function defaultVersions(now: Date, root = releaseLedgerRoot()): readonly [string, string] {
107
+ if (!existsSync(join(root, 'release', 'features.sqlite'))) throw new Error('release schedule ledger unavailable');
108
+ const upcoming = listSchedules(root).filter(row => row.cutAt && Date.parse(row.cutAt) > now.getTime())
101
109
  .sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt));
102
110
  if (upcoming.length < 2) throw new Error('current and next release schedules unavailable');
103
111
  return [upcoming[0]!.version, upcoming[1]!.version];
@@ -123,12 +131,13 @@ export function collectAuthorDepth(deps: CollectAuthorDepthDeps = {}): AuthorDep
123
131
  if (measured.unassigned) throw new Error(`${measured.unassigned} unassigned harness launch process(es)`);
124
132
  return Object.fromEntries(measured.seats.map(row => [row.seat, row.running]));
125
133
  });
126
- const queued = observe('queue', deps.queued ?? (() => listHarnessQueue()));
127
- const versions = observe('versions', deps.versions ?? (() => defaultVersions(now)));
134
+ const queued = observe('queue', deps.queued ?? (() => listHarnessQueue({ root: deps.root })));
135
+ const releaseRoot = deps.root ?? releaseLedgerRoot();
136
+ const versions = observe('versions', deps.versions ?? (() => defaultVersions(now, releaseRoot)));
128
137
  const cells = versions === null ? null : observe('checklist', () => versions.flatMap(version => {
129
- if (!deps.cells && !existsSync(join(releaseLedgerRoot(), 'release', 'features.sqlite')))
138
+ if (!deps.cells && !existsSync(join(releaseRoot, 'release', 'features.sqlite')))
130
139
  throw new Error('checklist ledger unavailable');
131
- return deps.cells?.(version) ?? listChecklist(version).items.map(item => ({ ...item, version }));
140
+ return deps.cells?.(version) ?? listChecklist(version, deps.root).items.map(item => ({ ...item, version }));
132
141
  }));
133
142
  const overlaps = observe('overlap', deps.overlaps ?? (() => {
134
143
  const observed = collectWork();
@@ -141,9 +150,56 @@ export function collectAuthorDepth(deps: CollectAuthorDepthDeps = {}): AuthorDep
141
150
  for (const row of result.seats) log('loop.orchestrator', 'would-author', {
142
151
  seat: row.seat, target: row.target, have: row.queued, short: row.short, cells: row.wouldAuthor,
143
152
  });
153
+ const ledger = deps.ledger ?? new AuthorLedger(deps.root === undefined ? {} : { path: join(deps.root, 'orchestrator', 'author-ledger.sqlite') });
154
+ const receiptCounts = { held: 0, queuedForAuthor: 0 };
155
+ let receiptFailures = 0;
156
+ for (const row of result.seats) for (const candidate of row.wouldAuthor) {
157
+ const cell = cells?.find(item => item.id === candidate.cellId && item.version === candidate.version);
158
+ try {
159
+ if (!cell) throw new Error('selected cell unavailable');
160
+ const text = cell.evidence ?? '';
161
+ const check = checkAuthorInput({ title: cell.title, text, cellId: cell.id, version: cell.version });
162
+ const receipt = ledger.request({ seat: row.seat, cellId: cell.id, version: cell.version, title: cell.title,
163
+ text, check: { verdict: check.verdict, signals: check.signals, ratio: check.implementationRatio } });
164
+ if (receipt.status === 'held') receiptCounts.held++;
165
+ if (receipt.status === 'queued-for-author') receiptCounts.queuedForAuthor++;
166
+ } catch (error) {
167
+ receiptFailures++;
168
+ result.unreadable.push({ source: 'author-ledger', reason: `${candidate.version} ${candidate.cellId}: ${error instanceof Error ? error.message : String(error)}` });
169
+ }
170
+ }
171
+ result.receiptCounts = receiptCounts;
172
+ result.receiptFailures = receiptFailures;
144
173
  return result;
145
174
  }
146
175
 
176
+ const SHADOW_INTERVAL_MS = 30 * 60_000;
177
+
178
+ /** The queue tick serializes callers; stamp before collection so even a failing shadow cannot run every two minutes. */
179
+ export function runAuthorDepthShadow(root = effectiveInstanceRoot(), now = new Date(), collect = collectAuthorDepth): void {
180
+ const path = join(root, 'orchestrator', 'author-depth-shadow-at');
181
+ const time = now.getTime();
182
+ if (!Number.isFinite(time)) throw new Error('invalid author-depth shadow clock');
183
+ if (existsSync(path)) {
184
+ const last = Date.parse(readFileSync(path, 'utf8').trim());
185
+ if (!Number.isFinite(last)) throw new Error('invalid author-depth shadow marker');
186
+ if (time - last < SHADOW_INTERVAL_MS) return;
187
+ }
188
+ mkdirSync(join(root, 'orchestrator'), { recursive: true });
189
+ const temporary = `${path}.${randomUUID()}.tmp`;
190
+ try {
191
+ writeFileSync(temporary, now.toISOString(), { flag: 'wx', mode: 0o600 });
192
+ renameSync(temporary, path);
193
+ } catch (error) {
194
+ try { unlinkSync(temporary); } catch { /* No temporary file to remove. */ }
195
+ throw error;
196
+ }
197
+ const result = collect({ root, now });
198
+ if (result.unreadable.length || result.receiptFailures || !result.receiptCounts) {
199
+ throw new Error(`author-depth shadow incomplete: ${JSON.stringify({ unreadable: result.unreadable, receiptFailures: result.receiptFailures })}`);
200
+ }
201
+ }
202
+
147
203
  export function authorDepthLines(result: AuthorDepthResult): string[] {
148
204
  return result.seats.map(row => `${row.seat} ${row.running ?? '?'}/${row.cap ?? '?'} · 대기 ${row.queued ?? '?'} · 목표 ${row.target ?? '?'} · 모자람 ${row.short ?? '?'} → ${row.wouldAuthor.map(cell => cell.cellId).join(', ') || '없음'}`);
149
205
  }
@@ -8,7 +8,7 @@ export interface AuthorInput {
8
8
  }
9
9
 
10
10
  export interface AuthorInputSignal {
11
- kind: 'path' | 'symbol' | 'command' | 'marker';
11
+ kind: 'path' | 'symbol' | 'command' | 'marker' | 'missing';
12
12
  match: string;
13
13
  field: 'title' | 'text';
14
14
  reason: string;
@@ -40,15 +40,19 @@ function sentences(value: string): string[] {
40
40
  }
41
41
 
42
42
  export function checkAuthorInput({ title, text, cellId, version }: AuthorInput): AuthorInputCheck {
43
- const valid = [title, text, cellId, version].every(value => typeof value === 'string' && !!value.trim())
43
+ const validIdentity = [title, cellId, version].every(value => typeof value === 'string' && !!value.trim())
44
44
  && /^\d+\.\d+\.\d+$/.test(version);
45
+ const missingText = typeof text !== 'string' || !text.trim();
46
+ const valid = validIdentity && !missingText;
45
47
  const signals: AuthorInputSignal[] = [];
48
+ if (validIdentity && missingText) signals.push({ kind: 'missing', match: '', field: 'text', reason: 'missing cell text' });
46
49
  let checked = 0;
47
50
  let candidates = 0;
48
51
  let directed = false;
49
52
 
50
- if (valid) {
53
+ if (validIdentity) {
51
54
  for (const field of ['title', 'text'] as const) {
55
+ if (field === 'text' && missingText) continue;
52
56
  for (const sentence of sentences(field === 'title' ? title : text)) {
53
57
  checked++;
54
58
  let found = false;
@@ -70,6 +74,7 @@ export function checkAuthorInput({ title, text, cellId, version }: AuthorInput):
70
74
  const verdict: AuthorInputCheck['verdict'] = !valid ? 'uncheckable'
71
75
  : signals.length === 0 ? 'approved' : directed ? 'resubmit' : 'confirm';
72
76
  const implementationRatio = valid ? candidates / checked : null;
77
+ if (!valid) { checked = 0; candidates = 0; }
73
78
  debug.log('author.par', 'input-checked', {
74
79
  cellId, version, verdict, signals: signals.length, ratio: implementationRatio,
75
80
  });
@@ -120,9 +120,11 @@ export class AuthorLedger {
120
120
  }
121
121
 
122
122
  request(input: AuthorRequest): AuthorRow & { duplicate: boolean } {
123
- for (const key of ['seat', 'cellId', 'version', 'title', 'text'] as const) {
123
+ for (const key of ['seat', 'cellId', 'version', 'title'] as const) {
124
124
  if (typeof input[key] !== 'string' || !input[key].trim()) throw new Error(`${key} is required`);
125
125
  }
126
+ if (typeof input.text !== 'string' || (!input.text.trim() && input.check?.verdict !== 'uncheckable'))
127
+ throw new Error('text is required unless uncheckable');
126
128
  if (!['approved', 'resubmit', 'confirm', 'uncheckable'].includes(input.check?.verdict)
127
129
  || !Array.isArray(input.check.signals)
128
130
  || (input.check.ratio !== null && (typeof input.check.ratio !== 'number' || !Number.isFinite(input.check.ratio))))
@@ -0,0 +1,56 @@
1
+ import type { ChecklistItem } from '../../release-loop/checklist.js';
2
+ import type { OrchestratorSeat } from '../../user-config.js';
3
+
4
+ const SEATS: readonly OrchestratorSeat[] = ['OP', 'TC', 'MK', 'UX'];
5
+ type WorkCell = Pick<ChecklistItem, 'owner' | 'status'>;
6
+
7
+ /** Allocate whole base slots by remaining work; this calculation does not decide launches. */
8
+ export function calculateSeatBaseShares({ totalSlots, currentRound, nextRound, seatCaps }: {
9
+ totalSlots: number;
10
+ currentRound: readonly WorkCell[];
11
+ nextRound: readonly WorkCell[];
12
+ seatCaps: Readonly<Record<OrchestratorSeat, number>>;
13
+ }): Record<OrchestratorSeat, number> {
14
+ if (!Number.isSafeInteger(totalSlots) || totalSlots < 0 ||
15
+ SEATS.some(seat => !Number.isSafeInteger(seatCaps[seat]) || seatCaps[seat] < 0)) {
16
+ throw new RangeError('totalSlots and seatCaps must be nonnegative safe integers');
17
+ }
18
+ const shares: Record<OrchestratorSeat, number> = { OP: 0, TC: 0, MK: 0, UX: 0 };
19
+ const weights: Record<OrchestratorSeat, number> = { OP: 0, TC: 0, MK: 0, UX: 0 };
20
+ for (const [cells, weight] of [[currentRound, 2], [nextRound, 1]] as const) {
21
+ for (const cell of cells) {
22
+ if (cell.status !== 'yellow') continue;
23
+ const seat = cell.owner?.split('/')[0];
24
+ if (SEATS.some(id => id === seat)) weights[seat as OrchestratorSeat] += weight;
25
+ }
26
+ }
27
+
28
+ let available = Math.min(totalSlots, SEATS.reduce((sum, seat) => sum + seatCaps[seat], 0));
29
+ let eligible = SEATS.filter(seat => weights[seat] > 0 && seatCaps[seat] > 0);
30
+ while (available > 0 && eligible.length > 0) {
31
+ const totalWeight = eligible.reduce((sum, seat) => sum + weights[seat], 0);
32
+ const ideal = (seat: OrchestratorSeat) => available * weights[seat] / totalWeight;
33
+ const capped = eligible.filter(seat => ideal(seat) >= seatCaps[seat]);
34
+ if (capped.length > 0) {
35
+ for (const seat of capped) {
36
+ shares[seat] = seatCaps[seat];
37
+ available -= seatCaps[seat];
38
+ }
39
+ eligible = eligible.filter(seat => !capped.includes(seat));
40
+ continue;
41
+ }
42
+ const remainders = eligible.map(seat => ({ seat, exact: ideal(seat) }));
43
+ for (const { seat, exact } of remainders) {
44
+ shares[seat] = Math.floor(exact);
45
+ available -= shares[seat];
46
+ }
47
+ remainders.sort((a, b) => (b.exact - Math.floor(b.exact)) - (a.exact - Math.floor(a.exact)));
48
+ for (const { seat } of remainders) {
49
+ if (available === 0) break;
50
+ shares[seat]++;
51
+ available--;
52
+ }
53
+ break;
54
+ }
55
+ return shares;
56
+ }
@@ -16,7 +16,7 @@ const SEAT_LOOP_IDS: Readonly<Record<string, string>> = { OP: 'op-seat', MK: 'cm
16
16
  export interface Cell { id: string; title: string; seat?: string; host?: string; version?: string; cardId: string; origin: 'candidate' | 'flow1a'; priority?: 'P0' | 'P1' | 'P2'; predecessors?: string[] }
17
17
  export interface TickState {
18
18
  runId: string; window: Window; mode: Mode; day: string;
19
- cards: TaskCard[]; skippedCards: Array<{ id: string; reason: string }>; cells: Cell[]; placed: number; unplaced?: number; delegated: number; wouldDelegate: number; rebalanced?: number;
19
+ cards: TaskCard[]; skippedCards: Array<{ id: string; reason: string }>; cells: Cell[]; placed: number; unplaced?: number; delegated: number; wouldDelegate: number; rebalanced?: number; rebalanceBlocked?: Array<{ id: string; reason: string }>;
20
20
  reconciled: { reached: number; progressing: number; blocked: number; unknown: number };
21
21
  runSummaries?: string[];
22
22
  nodes: Partial<Record<Node, 'ok' | 'skipped'>>;
@@ -27,7 +27,7 @@ export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Prom
27
27
  /** RELPLAN1 `placeCell(input: PlacementCell)` — owner·priority·predecessors are required; the result carries no seat. */
28
28
  export type PlacementInput = { id: string; title: string; owner: string; priority: 'P0' | 'P1' | 'P2'; predecessors: string[] };
29
29
  export type PlaceAdapter = (cell: PlacementInput) => Promise<{ version?: string } | null> | { version?: string } | null;
30
- export type RebalanceAdapter = (version: string) => Promise<unknown[]> | unknown[];
30
+ export type RebalanceAdapter = (version: string) => Promise<{ decisions: unknown[]; blocked: Array<{ id: string; reason: string }> }> | { decisions: unknown[]; blocked: Array<{ id: string; reason: string }> };
31
31
  export interface TickDeps {
32
32
  root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
33
33
  cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
@@ -72,7 +72,8 @@ export function reconciliation(cells: readonly Cell[], requests: readonly { key:
72
72
  export function reportLine(state: TickState): string {
73
73
  const r = state.reconciled;
74
74
  const line = `orchestrator ${state.window} cards=${state.cards.length} cells=${state.cells.length} placed=${state.missing.includes('relplan1-absent') ? 'missing' : state.placed} delegated=${state.mode === 'shadow' ? `would ${state.wouldDelegate}` : state.delegated} reconciled=${r.reached}/${r.progressing}/${r.blocked}/${r.unknown} mode=${state.mode}`;
75
- return state.runSummaries?.length ? `${line}\n${state.runSummaries.join('\n')}` : line;
75
+ const details = [...(state.rebalanceBlocked ?? []).map(row => `⛔ 이월 거부 ${row.id}: ${row.reason}`), ...(state.runSummaries ?? [])];
76
+ return details.length ? `${line}\n${details.join('\n')}` : line;
76
77
  }
77
78
  function readCardSnapshot(root: string): { cards: TaskCard[]; skippedCards: TickState['skippedCards'] } {
78
79
  const dir = taskCardsDir(root);
@@ -268,7 +269,13 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
268
269
  if (mode === 'shadow') observe('exchange', 'rebalance-shadow-not-invoked', versions.length);
269
270
  else {
270
271
  state.rebalanced = 0;
271
- for (const version of versions) state.rebalanced += (await rebalance(version)).length;
272
+ state.rebalanceBlocked = [];
273
+ for (const version of versions) {
274
+ const result = await rebalance(version);
275
+ state.rebalanced += result.decisions.length;
276
+ state.rebalanceBlocked.push(...result.blocked);
277
+ for (const blocked of result.blocked) observe('exchange', `rebalance-blocked: ${blocked.id} ${blocked.reason}`, 1);
278
+ }
272
279
  }
273
280
  }
274
281
  }
@@ -2,16 +2,19 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync } fro
2
2
  import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
3
3
  import { debug } from '../../debug/log.js';
4
4
  import { defaultListHarnessProcesses, type HarnessProcessRecord } from '../../harness/harness-cli-command.js';
5
- import { effectiveInstanceRoot } from '../../instance/resolve.js';
5
+ import { effectiveInstanceRoot, releaseLedgerRoot } from '../../instance/resolve.js';
6
6
  import { devVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
7
+ import { listSchedules } from '../../release-loop/release-schedule.js';
7
8
  import { withFileLockSync } from '../../storage/file-lock.js';
8
9
  import { getUserConfig, ORCHESTRATOR_DEFAULTS, type OrchestratorLoopConfig, type OrchestratorSeat } from '../../user-config.js';
9
10
  import { finishAdvice, measureFinish, type FinishAdvice, type FinishMetrics, type MeasureFinishDeps } from './finish-rate.js';
11
+ import { collectAuthorDepth, type AuthorDepthResult, type CollectAuthorDepthDeps } from './author-depth.js';
12
+ import { calculateSeatBaseShares } from './seat-share.js';
10
13
 
11
14
  export const TRAFFIC_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
12
15
  export type TrafficCell = Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status'>;
13
16
  export type TrafficProcess = Pick<HarnessProcessRecord, 'command' | 'elapsedSeconds' | 'cwd'> & { seat: OrchestratorSeat | null };
14
- export type TrafficSeatRow = { seat: OrchestratorSeat; running: number; cap: number; idleFor: number | null; idle: boolean; nextCell: TrafficCell | null };
17
+ export type TrafficSeatRow = { seat: OrchestratorSeat; running: number; cap: number; baseShare: number | null; borrowed: number; lent: number; launchCap: number; idleFor: number | null; idle: boolean; nextCell: TrafficCell | null };
15
18
  export type TrafficResult = { seats: TrafficSeatRow[]; unassigned: number; now: Date; idleMinutes: number; finish?: { metrics: FinishMetrics; advice: FinishAdvice } };
16
19
 
17
20
  /** Same bounded walk as the CTX1 hook: a marker at the git root is checked before stopping. */
@@ -49,15 +52,23 @@ function realOrResolved(path: string): string {
49
52
  const IS_LAUNCH = /(?:^|\s)(?:\S*\/)?elanous\.mjs\s+(?:(?:--test|--config-dir\s+\S+)\s+)?harness\s+(?:ask|say)(?=\s|$)/;
50
53
 
51
54
  /** Process-to-seat resolution occurs at the boundary; this decision does no IO. */
52
- export function trafficTick({ processes, now, caps, lastLaunchAt, openCells, idleMinutes = 30 }: {
55
+ export function trafficTick({ processes, now, caps, lastLaunchAt, idleSince, openCells, nextRound, totalSlots, idleMinutes = 30 }: {
53
56
  processes: readonly TrafficProcess[];
54
57
  now: Date;
55
58
  caps: Readonly<Record<OrchestratorSeat, number>>;
56
59
  lastLaunchAt?: Partial<Record<OrchestratorSeat, Date | null>>;
60
+ idleSince?: Partial<Record<OrchestratorSeat, Date | null>>;
57
61
  openCells: readonly TrafficCell[];
62
+ nextRound?: readonly TrafficCell[];
63
+ totalSlots?: number;
58
64
  idleMinutes?: number;
59
65
  }): TrafficResult {
60
66
  const launches = processes.filter(process => IS_LAUNCH.test(process.command));
67
+ const slotBudget = totalSlots ?? TRAFFIC_SEATS.reduce((sum, seat) => sum + caps[seat], 0);
68
+ // A missing next round is unknown, not an empty round with zero remaining work.
69
+ const baseShares = nextRound === undefined ? null : calculateSeatBaseShares({
70
+ totalSlots: slotBudget, currentRound: openCells, nextRound, seatCaps: caps,
71
+ });
61
72
  const seats = TRAFFIC_SEATS.map((seat): TrafficSeatRow => {
62
73
  const owned = launches.filter(process => process.seat === seat);
63
74
  const latest = owned.reduce<number | null>((current, process) => {
@@ -66,12 +77,57 @@ export function trafficTick({ processes, now, caps, lastLaunchAt, openCells, idl
66
77
  return current === null ? start : Math.max(current, start);
67
78
  }, null);
68
79
  const fallback = owned.length ? lastLaunchAt?.[seat]?.getTime() : undefined;
69
- const last = latest ?? (fallback !== undefined && Number.isFinite(fallback) ? fallback : null);
80
+ const idleStart = idleSince?.[seat]?.getTime();
81
+ const last = idleStart !== undefined && Number.isFinite(idleStart) ? idleStart
82
+ : latest ?? (fallback !== undefined && Number.isFinite(fallback) ? fallback : null);
70
83
  const idleFor = last === null ? null : Math.max(0, Math.floor((now.getTime() - last) / 60_000));
71
- const idle = owned.length < caps[seat] && last !== null && now.getTime() - last > idleMinutes * 60_000;
72
- return { seat, running: owned.length, cap: caps[seat], idleFor, idle,
73
- nextCell: idle ? openCells.find(cell => cell.status !== 'done' && cell.owner?.split('/')[0] === seat) ?? null : null };
84
+ const nextCell = openCells.find(cell => cell.status !== 'done' && cell.owner?.split('/')[0] === seat) ?? null;
85
+ const baseShare = baseShares?.[seat] ?? null;
86
+ return { seat, running: owned.length, cap: caps[seat], baseShare, borrowed: 0, lent: 0,
87
+ launchCap: baseShare === null ? caps[seat] : Math.max(owned.length, baseShare), idleFor,
88
+ idle: owned.length < caps[seat] && last !== null && now.getTime() - last > idleMinutes * 60_000, nextCell };
74
89
  });
90
+ if (baseShares !== null) {
91
+ // Account for outstanding loans before lending new capacity. A returning
92
+ // lender cannot launch into a slot still occupied by the borrower's run.
93
+ for (const borrower of seats) {
94
+ let excess = Math.max(0, borrower.running - borrower.baseShare!);
95
+ borrower.borrowed += excess;
96
+ for (const lender of seats) {
97
+ if (!excess) break;
98
+ if (lender === borrower) continue;
99
+ const amount = Math.min(excess, Math.max(0, lender.baseShare! - lender.running - lender.lent));
100
+ lender.lent += amount;
101
+ excess -= amount;
102
+ }
103
+ }
104
+ for (const lender of seats) {
105
+ if (lender.nextCell) continue;
106
+ let available = Math.max(0, lender.baseShare! - lender.running - lender.lent);
107
+ for (const borrower of seats) {
108
+ if (!available) break;
109
+ if (borrower === lender || !borrower.nextCell) continue;
110
+ const needed = Math.max(0, Math.min(borrower.cap, borrower.running + 1) - borrower.launchCap);
111
+ const amount = Math.min(available, needed);
112
+ lender.lent += amount;
113
+ borrower.borrowed += amount;
114
+ borrower.launchCap += amount;
115
+ available -= amount;
116
+ }
117
+ }
118
+ for (const lender of seats) lender.launchCap = Math.max(lender.running, lender.launchCap - lender.lent);
119
+ }
120
+ // Reserve one available global slot per possible request, including unknown-seat
121
+ // launches in the occupied count; returning a lent share does not end its old run.
122
+ let freeSlots = Math.max(0, slotBudget - launches.length);
123
+ for (const row of seats) {
124
+ if (row.nextCell && row.idle && row.running < row.launchCap) {
125
+ if (freeSlots > 0) freeSlots--;
126
+ else row.launchCap = row.running;
127
+ }
128
+ row.idle = row.idle && (row.nextCell === null || row.running < row.launchCap);
129
+ if (!row.idle) row.nextCell = null;
130
+ }
75
131
  return { seats, unassigned: launches.filter(process => process.seat === null).length, now, idleMinutes };
76
132
  }
77
133
 
@@ -92,7 +148,8 @@ export interface TrafficApplyDeps {
92
148
  export function applyTraffic(result: TrafficResult, deps: TrafficApplyDeps = {}): number {
93
149
  const log = deps.log ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data));
94
150
  const mode = deps.mode ?? 'shadow';
95
- log('loop.orchestrator', 'tick', { mode, seats: result.seats.map(({ seat, running, cap, idleFor, idle }) => ({ seat, running, cap, idleFor, idle })), unassigned: result.unassigned });
151
+ log('loop.orchestrator', 'tick', { mode, seats: result.seats.map(({ seat, running, cap, baseShare, borrowed, lent, launchCap, idleFor, idle }) =>
152
+ ({ seat, running, cap, baseShare, borrowed, lent, launchCap, idleFor, idle })), unassigned: result.unassigned });
96
153
  if (result.finish) {
97
154
  const { finishSlots, launchSlots, state, reasons } = result.finish.advice;
98
155
  log('loop.orchestrator', 'would-rebalance', { finishSlots, launchSlots, state, reasons });
@@ -140,6 +197,49 @@ export function trafficLine(result: TrafficResult, mode: 'shadow' | 'live'): str
140
197
  return `${prefix} · finish rate=${rate(metrics.landingRate)} stale=${metrics.staleDrafts ?? '?'} conflict=${rate(metrics.conflictRatio)} → finish=${advice.finishSlots}/${advice.finishSlots + advice.launchSlots}`;
141
198
  }
142
199
 
200
+ export type AuthorShadow = { cells: number; held: number; queuedForAuthor: number; status: 'complete' | 'incomplete'; receiptFailures: number };
201
+
202
+ /** Traffic stays authoritative: shadow collection and its ledger cannot change its decision or exit status. */
203
+ export function runTrafficTick({ processes, now, caps, openCells, nextRound, totalSlots, mode, finishDeps, applyDeps, authorDeps, collectAuthor = collectAuthorDepth, log = debug.log.bind(debug) }: {
204
+ processes: readonly TrafficProcess[];
205
+ now: Date;
206
+ caps: Readonly<Record<OrchestratorSeat, number>>;
207
+ openCells: readonly TrafficCell[];
208
+ nextRound?: readonly TrafficCell[];
209
+ totalSlots?: number;
210
+ mode: 'shadow' | 'live';
211
+ finishDeps?: MeasureFinishDeps;
212
+ applyDeps?: TrafficApplyDeps;
213
+ authorDeps?: Omit<CollectAuthorDepthDeps, 'caps' | 'running' | 'now'>;
214
+ collectAuthor?: (deps: CollectAuthorDepthDeps) => AuthorDepthResult;
215
+ log?: (category: string, event: string, data: Record<string, unknown>) => void;
216
+ }): { result: TrafficResult; queued: number; authorShadow: AuthorShadow | null } {
217
+ const metrics = measureFinish(finishDeps, now);
218
+ const advice = finishAdvice(metrics, TRAFFIC_SEATS.reduce((sum, seat) => sum + caps[seat], 0));
219
+ const result = { ...trafficTick({ processes, now, caps, openCells, nextRound, totalSlots: totalSlots ?? advice.launchSlots }),
220
+ finish: { metrics, advice } };
221
+ const queued = applyTraffic(result, { ...applyDeps, mode });
222
+ let authorShadow: AuthorShadow | null = null;
223
+ let shadowFailure: Record<string, unknown> | null = null;
224
+ try {
225
+ const depth = collectAuthor({ ...authorDeps, now, caps: () => caps,
226
+ running: () => Object.fromEntries(result.seats.map(row => [row.seat, row.running])) });
227
+ const receiptFailures = depth.receiptFailures ?? depth.unreadable.filter(item => item.source === 'author-ledger').length;
228
+ const incomplete = depth.unreadable.length > 0 || !depth.receiptCounts;
229
+ authorShadow = { cells: depth.seats.reduce((total, row) => total + row.wouldAuthor.length, 0),
230
+ held: depth.receiptCounts?.held ?? 0, queuedForAuthor: depth.receiptCounts?.queuedForAuthor ?? 0,
231
+ status: incomplete ? 'incomplete' : 'complete', receiptFailures };
232
+ if (incomplete) shadowFailure = { unreadable: depth.unreadable, ...(depth.receiptCounts ? {} : { reason: 'receipt counts unavailable' }) };
233
+ } catch (error) {
234
+ shadowFailure = { reason: error instanceof Error ? error.message : String(error) };
235
+ }
236
+ if (shadowFailure) {
237
+ try { log('loops.author-depth', 'shadow-failed', shadowFailure); }
238
+ catch { /* Shadow logging cannot change traffic's outcome. */ }
239
+ }
240
+ return { result, queued, authorShadow };
241
+ }
242
+
143
243
  if (import.meta.main) {
144
244
  try {
145
245
  const args = process.argv.slice(2);
@@ -154,9 +254,15 @@ if (import.meta.main) {
154
254
  if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
155
255
  const now = new Date();
156
256
  const processes = observation.records.map(process => ({ ...process, seat: seatOfTree(process.cwdStatus === 'unknown' ? undefined : process.cwd, cfg) }));
157
- const result = withFinishAdvice(trafficTick({ processes, now, caps: cfg.seatCaps, openCells: listChecklist(version).items }));
158
- const queued = applyTraffic(result, { mode: cfg.trafficMode });
159
- if (args.includes('--json')) console.log(JSON.stringify({ ...result, mode: cfg.trafficMode, queued, version }));
257
+ const upcoming = listSchedules(releaseLedgerRoot()).filter(row => Date.parse(row.cutAt) > now.getTime())
258
+ .sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt));
259
+ const currentIndex = upcoming.findIndex(row => row.version === version);
260
+ const nextVersion = currentIndex < 0 ? upcoming[0]?.version : upcoming[currentIndex + 1]?.version;
261
+ const { result, queued, authorShadow } = runTrafficTick({
262
+ processes, now, caps: cfg.seatCaps, openCells: listChecklist(version).items,
263
+ ...(nextVersion ? { nextRound: listChecklist(nextVersion).items } : {}), mode: cfg.trafficMode,
264
+ });
265
+ if (args.includes('--json')) console.log(JSON.stringify({ ...result, mode: cfg.trafficMode, queued, version, authorShadow }));
160
266
  else console.log(trafficLine(result, cfg.trafficMode));
161
267
  } catch (error) {
162
268
  console.error(`traffic: ${error instanceof Error ? error.message : String(error)}`);