elanous 0.2.15 → 0.2.16

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 (254) 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/{1307-33ffcc49ab45123b.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  51. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  52. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +15 -15
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +15 -15
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +15 -15
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +15 -15
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +15 -15
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +15 -15
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +15 -15
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +15 -15
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +15 -15
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +15 -15
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +15 -15
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +15 -15
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +15 -15
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +15 -15
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +15 -15
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +15 -15
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +15 -15
  88. package/apps/pwa/out/loops/index.html +1 -1
  89. package/apps/pwa/out/loops/index.txt +15 -15
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +15 -15
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +15 -15
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +15 -15
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +15 -15
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +15 -15
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +15 -15
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +16 -16
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +15 -15
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +15 -15
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +15 -15
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +15 -15
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +15 -15
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +15 -15
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +15 -15
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +15 -15
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +15 -15
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +15 -15
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +15 -15
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +15 -15
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +15 -15
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +15 -15
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +15 -15
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +15 -15
  142. package/graphs/doc-gen/doc-gen.yaml +24 -0
  143. package/graphs/doc-gen/recipes.yaml +3 -0
  144. package/graphs/orchestrator/orchestrator.yaml +7 -0
  145. package/graphs/orchestrator/recipes.yaml +3 -0
  146. package/graphs/steward/recipes.yaml +2 -1
  147. package/package.json +1 -1
  148. package/scripts/ci-public-export-import-gate.ts +154 -0
  149. package/scripts/ci-test-interference-gate.ts +5 -1
  150. package/scripts/ci-typecheck-changed.ts +34 -15
  151. package/scripts/docs/doc-rot.ts +164 -0
  152. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  153. package/scripts/docs-cli-check.ts +4 -4
  154. package/scripts/goals/goal-census.ts +343 -0
  155. package/scripts/measure/author-section-contribution.ts +84 -0
  156. package/scripts/release-loop/cut-branch.ts +43 -13
  157. package/scripts/release-loop/gate-node.ts +3 -1
  158. package/scripts/release-loop/vault-note-node.ts +19 -2
  159. package/scripts/rhythm/monthly.ts +97 -0
  160. package/src/acp/client.ts +17 -2
  161. package/src/agent-mission/driver.ts +51 -1
  162. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  163. package/src/agent-mission/supervisor-metrics.ts +218 -0
  164. package/src/autopilot/track-agent-shadow.ts +2 -1
  165. package/src/briefing/brief-items.ts +191 -36
  166. package/src/budget/usage-outcomes.ts +204 -0
  167. package/src/cli/brief-cli.ts +73 -0
  168. package/src/cli/know-cli.ts +20 -0
  169. package/src/cli/logs-cli.ts +43 -1
  170. package/src/cli/pr-cli.ts +80 -12
  171. package/src/cli/release-cli.ts +11 -8
  172. package/src/cli/self-update.ts +200 -1
  173. package/src/cli/usage-cli.ts +26 -0
  174. package/src/discord-self-message.ts +9 -0
  175. package/src/flow-loop/split.ts +24 -0
  176. package/src/harness/harness-cli-command.ts +36 -2
  177. package/src/harness/harness-pod-dispatch.ts +25 -55
  178. package/src/harness/harness-seams.ts +13 -3
  179. package/src/harness/helper-repair.ts +139 -8
  180. package/src/harness/helper-scan.ts +13 -2
  181. package/src/harness/nested-elanous-policy.ts +114 -0
  182. package/src/hq/hq.ts +8 -2
  183. package/src/hq/lease.ts +5 -3
  184. package/src/hq/transfer.ts +185 -0
  185. package/src/index.ts +112 -6
  186. package/src/intake-plane/digest.ts +48 -26
  187. package/src/knowledge/know-find.ts +105 -0
  188. package/src/llm.ts +75 -19
  189. package/src/loops/orchestrator/finish-rate.ts +14 -3
  190. package/src/loops/orchestrator/tick.ts +96 -25
  191. package/src/maturity/bot-slash-catalog.ts +11 -2
  192. package/src/maturity/feature-maturity.ts +2 -2
  193. package/src/maturity/tui-slash-availability.ts +9 -0
  194. package/src/oauth/overload-failover-launch.ts +75 -0
  195. package/src/oauth/overload-failover.ts +137 -0
  196. package/src/release-loop/checklist.ts +10 -5
  197. package/src/release-loop/feature-store.ts +10 -9
  198. package/src/release-loop/placement.ts +17 -8
  199. package/src/seat-dispatch/seat-ask.ts +33 -6
  200. package/src/seat-loop/seat-loop.ts +90 -3
  201. package/src/self-dev/ask-launch-flow.ts +22 -1
  202. package/src/self-dev/authored-goal-trace.ts +64 -0
  203. package/src/self-dev/dev-cli.ts +19 -1
  204. package/src/self-dev/dev-pipeline.ts +6 -5
  205. package/src/self-dev/entrance-registry.ts +10 -6
  206. package/src/self-dev/old-door.ts +73 -0
  207. package/src/self-implement/app-api-budget.ts +166 -0
  208. package/src/self-implement/budget-gate.ts +7 -5
  209. package/src/self-implement/follow-up-goals.ts +289 -0
  210. package/src/self-implement/goal-author.ts +19 -13
  211. package/src/self-implement/heal-triage.ts +44 -0
  212. package/src/self-implement/orchestrator.ts +138 -7
  213. package/src/self-implement/run-ledger.ts +4 -0
  214. package/src/self-implement/seams.ts +44 -15
  215. package/src/session-runtime/index.ts +2 -0
  216. package/src/session-runtime/retry-api.ts +87 -0
  217. package/src/session-runtime/retry-policy.ts +107 -6
  218. package/src/steward/working-backwards.ts +16 -1
  219. package/src/task-cards/card-cli.ts +25 -0
  220. package/src/task-cards/card-store.ts +5 -3
  221. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  222. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  223. package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
  224. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  225. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
  226. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  227. package/src/telegram-commands.ts +5 -0
  228. package/src/telegram.ts +47 -0
  229. package/src/user-config.ts +36 -3
  230. package/src/version/packed-revision.json +1 -1
  231. package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  244. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  245. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  246. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  247. package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
  248. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  249. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  250. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  251. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  252. package/src/launch-head/seat-goal-queue.ts +0 -198
  253. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
  254. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -33,7 +33,7 @@ const walWait = new Int32Array(new SharedArrayBuffer(4));
33
33
  const SCHEMA_COLUMNS: Record<string, readonly string[]> = {
34
34
  imported_versions: ['json_hash', 'imported_at'],
35
35
  events: ['reason'],
36
- assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date'],
36
+ assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator'],
37
37
  release_schedules: ['freeze_from', 'freeze_until'],
38
38
  };
39
39
  function schemaCurrent(db: Database): boolean {
@@ -75,7 +75,7 @@ function open(root = releaseLedgerRoot()): Database {
75
75
  if (!(db.query('PRAGMA table_info(events)').all() as Array<{ name: string }>).some((column) => column.name === 'reason')) db.exec('ALTER TABLE events ADD COLUMN reason TEXT');
76
76
  db.exec('CREATE TABLE IF NOT EXISTS release_schedules (version TEXT PRIMARY KEY, cut_at TEXT NOT NULL, land_by TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL)');
77
77
  const assignmentColumns = db.query('PRAGMA table_info(assignments)').all() as Array<{ name: string }>;
78
- for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT']] as const) {
78
+ for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT'], ['accelerator', 'INTEGER']] as const) {
79
79
  if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
80
80
  }
81
81
  const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
@@ -169,7 +169,7 @@ function evidenceRefs(db: Database, version: string, id: string): string[] {
169
169
  return (db.query('SELECT ref FROM evidence WHERE feature_id = ? AND version = ? ORDER BY rowid').all(id, version) as Array<{ ref: string }>).map((row) => row.ref);
170
170
  }
171
171
  function items(db: Database, version: string): ChecklistItem[] {
172
- const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
172
+ const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.accelerator, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
173
173
  FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.version = ? ORDER BY a.rowid`).all(version) as Array<Record<string, string | number | null>>;
174
174
  return rows.map((r) => {
175
175
  const evidence = [r.evidence, ...evidenceRefs(db, version, r.id as string)].filter((value) => value !== null).join('\n');
@@ -178,6 +178,7 @@ function items(db: Database, version: string): ChecklistItem[] {
178
178
  ...(r.priority !== null ? { priority: r.priority as ChecklistItem['priority'] } : {}), ...(r.predecessors !== null ? { predecessors: JSON.parse(r.predecessors as string) as string[] } : {}),
179
179
  ...(r.deadline_version !== null ? { deadlineVersion: r.deadline_version as string } : {}),
180
180
  ...(r.ceo_minutes !== null ? { ceoMinutes: r.ceo_minutes as number } : {}), ...(r.ceo_date !== null ? { ceoDate: r.ceo_date as string } : {}),
181
+ ...(r.accelerator === 1 ? { accelerator: true as const } : {}),
181
182
  ...(r.evidence !== null || evidence ? { evidence } : {}), ...(r.disposition !== null ? { disposition: r.disposition as ChecklistItem['disposition'] } : {}),
182
183
  updatedAt: r.updated_at as string, updatedBy: r.updated_by as string };
183
184
  });
@@ -191,9 +192,9 @@ function putItem(db: Database, version: string, item: ChecklistItem): void {
191
192
  if (!feature) db.query('INSERT INTO features (id, title, owner, kind, created_at) VALUES (?, ?, ?, ?, ?)').run(item.id, item.title, item.owner ?? null, item.kind ?? null, item.updatedAt);
192
193
  // features.owner/kind follow the latest assignment so details() and the checklist never disagree after a set.
193
194
  else if (item.owner !== undefined || item.kind !== undefined) db.query('UPDATE features SET owner = COALESCE(?, owner), kind = COALESCE(?, kind) WHERE id = ?').run(item.owner ?? null, item.kind ?? null, item.id);
194
- db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
195
- ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
196
- .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.updatedAt, item.updatedBy);
195
+ db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
196
+ ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, accelerator=excluded.accelerator, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
197
+ .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.updatedAt, item.updatedBy);
197
198
  }
198
199
  function jsonHash(contents: string | Buffer): string { return createHash('sha256').update(contents).digest('hex'); }
199
200
 
@@ -234,8 +235,8 @@ function importOnDb(db: Database, version: string): boolean {
234
235
  // Keep a legacy per-version title until an explicit retitle unifies the feature identity.
235
236
  if (!existing) putItem(db, version, item);
236
237
  else {
237
- db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
238
- .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.updatedAt, item.updatedBy);
238
+ db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
239
+ .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.updatedAt, item.updatedBy);
239
240
  }
240
241
  }
241
242
  for (const entry of data.history) record(db, version, entry);
@@ -262,7 +263,7 @@ function importOnDb(db: Database, version: string): boolean {
262
263
  record(db, version, { at: new Date().toISOString(), by: item.updatedBy, id: item.id, field: 'reimport', from: null, to: item, released: data.released, dev: data.dev });
263
264
  continue;
264
265
  }
265
- const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate'] as const;
266
+ const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator'] as const;
266
267
  const baseEvidence = (db.query('SELECT evidence FROM assignments WHERE feature_id = ? AND version = ?').get(item.id, version) as { evidence: string | null }).evidence ?? undefined;
267
268
  const preserveRefs = item.evidence === baseEvidence || item.evidence === current.evidence;
268
269
  const differs = fields.some((field) => field === 'evidence'
@@ -10,7 +10,7 @@ import { move, releasedVersion } from './feature-store.js';
10
10
  import { listSchedules, type ReleaseSchedule } from './release-schedule.js';
11
11
 
12
12
  export type PlacementPriority = 'P0' | 'P1' | 'P2';
13
- export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string }
13
+ export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }
14
14
  export interface PlacementDecision { id: string; from: string | null; version: string; reason: string; displaced: Array<{ id: string; from: string; to: string; reason: string }> }
15
15
  export interface RebalanceResult {
16
16
  decisions: PlacementDecision[];
@@ -122,6 +122,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
122
122
  validateCeoLoad(input);
123
123
  const seat = parseOwner(input.owner).seat;
124
124
  if (!['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
125
+ if (input.accelerator !== undefined && input.accelerator !== true && input.accelerator !== false) throw new CliUserError('가속 등급은 true 이거나 생략한다');
125
126
  const now = (deps.now ?? new Date()).getTime();
126
127
  const schedules = [...(deps.schedules ?? listSchedules())].sort((a, b) => versionOrder(a.version, b.version));
127
128
  const released = deps.released ?? releasedVersion();
@@ -142,6 +143,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
142
143
  if (from && released && versionOrder(from, released) <= 0) throw new CliUserError(`이미 발행된 판의 칸은 배치하지 않는다: ${input.id}`);
143
144
  if (existing?.status === 'done') throw new CliUserError(`끝난 칸은 배치하지 않는다: ${input.id}`);
144
145
  if (existing && (existing.title !== input.title || existing.owner !== input.owner)) throw new CliUserError(`기존 칸 제목·담당 불일치: ${input.id}`);
146
+ const accelerator = input.accelerator === true || (input.accelerator === undefined && existing?.accelerator === true);
145
147
  const load = { id: input.id, ceoMinutes: input.ceoMinutes ?? existing?.ceoMinutes, ceoDate: input.ceoDate ?? existing?.ceoDate };
146
148
  const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
147
149
  const predecessors = input.predecessors.map((id) => {
@@ -170,6 +172,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
170
172
  let chosen: ReleaseSchedule | undefined;
171
173
  let loadIssue: string | null = null;
172
174
  const feasible = candidates.filter((row) => !dependencyViolation(snapshots, locations, new Map([[input.id, row.version]]), input));
175
+ // 판은 «마감 안 가장 이른 판»부터 본다(기존 불변식). 가속 등급은 그 판이 찼을 때 같은 우선순위 비가속 칸을 밀 수 있게만 한다.
173
176
  for (const row of feasible) {
174
177
  const placement = new Map([[input.id, row.version]]);
175
178
  const items = occupied(row.version);
@@ -180,11 +183,15 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
180
183
  chosen = row;
181
184
  break;
182
185
  }
183
- if (input.priority !== 'P1' || from === row.version) { loadIssue ??= directIssue; continue; }
186
+ const mayDisplace = input.priority === 'P1' ? from !== row.version : accelerator && from !== row.version;
187
+ if (!mayDisplace) { loadIssue ??= directIssue; continue; }
184
188
  const next = open.find((later) => versionOrder(later.version, row.version) > 0);
185
189
  if (!next || items.length - 1 >= capacity(row)) { loadIssue ??= directIssue; continue; }
190
+ const victimOk = (priority: PlacementPriority | undefined) => (input.priority === 'P1' && priority === 'P2')
191
+ || (accelerator && priority === input.priority);
186
192
  const p2 = [...items].reverse().find((item) => {
187
- if (item.priority !== 'P2' || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input)) return false;
193
+ if (item.accelerator === true) return false;
194
+ if (!victimOk(item.priority) || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input)) return false;
188
195
  if (item.deadlineVersion && versionOrder(next.version, item.deadlineVersion) > 0) return false;
189
196
  if (items.length < capacity(row) && !(seatItems.length >= seatLimit && ownerMatches(item.owner, seat))) return false;
190
197
  if (!item.owner) return false;
@@ -202,22 +209,24 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
202
209
  return true;
203
210
  });
204
211
  if (p2) {
205
- displaced.push({ id: p2.id, from: row.version, to: next.version, reason: `P1 ${input.id} 마감 판 ${row.version} 용량 확보를 위해 P2 이월` });
212
+ displaced.push({ id: p2.id, from: row.version, to: next.version, reason: accelerator
213
+ ? `가속 등급 ${input.id} 를 같은 우선순위 안에서 먼저 두기 위해 ${p2.priority ?? 'P2'} ${p2.id} 이월`
214
+ : `P1 ${input.id} 마감 판 ${row.version} 용량 확보를 위해 P2 이월` });
206
215
  chosen = row;
207
216
  break;
208
217
  }
209
218
  loadIssue ??= directIssue;
210
219
  }
211
220
  if (!chosen) throw new CliUserError(loadIssue ?? `${input.id} 배치할 판이 없다 — 용량·선행·마감·동결을 확인하라`);
212
- const reason = `${input.priority} ${input.priority === 'P0' ? '사고·회귀·발행 막음: 다음 판' : input.priority === 'P1' ? `지시·행사 마감 판 ${deadlineVersion}` : '착지 마감 내 가장 이른 여유 판'} · PR/24h ${rate} · 용량 ${capacity(chosen)} · 자리 ${seatLimit}`;
221
+ const reason = `${accelerator ? '가속 등급 · ' : ''}${input.priority} ${input.priority === 'P0' ? '사고·회귀·발행 막음: 다음 판' : input.priority === 'P1' ? `지시·행사 마감 판 ${deadlineVersion}` : '착지 마감 내 가장 이른 여유 판'}${accelerator ? ' · 같은 우선순위 안에서 가속 등급을 먼저' : ''} · PR/24h ${rate} · 용량 ${capacity(chosen)} · 자리 ${seatLimit}`;
213
222
  const decision = { id: input.id, from, version: chosen.version, reason, displaced };
214
223
  if (!deps.dryRun) {
215
224
  const by = deps.by ?? 'OP';
216
225
  for (const shifted of displaced) move(shifted.id, shifted.from, shifted.to, by, undefined, undefined, shifted.reason);
217
226
  if (from && from !== chosen.version) move(input.id, from, chosen.version, by, undefined, undefined, reason);
218
- if (from && (existing?.priority !== input.priority || JSON.stringify(existing.predecessors ?? []) !== JSON.stringify(input.predecessors) || (input.deadlineVersion !== undefined && existing.deadlineVersion !== input.deadlineVersion) || (input.ceoMinutes !== undefined && existing.ceoMinutes !== input.ceoMinutes) || (input.ceoDate !== undefined && existing.ceoDate !== input.ceoDate)))
219
- setItem(chosen.version, input.id, { priority: input.priority, predecessors: input.predecessors, ...(input.deadlineVersion ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}) }, by);
220
- if (!from) addItem(chosen.version, { id: input.id, title: input.title, owner: input.owner, priority: input.priority, predecessors: input.predecessors, deadlineVersion, ...(load.ceoMinutes !== undefined ? { ceoMinutes: load.ceoMinutes } : {}), ...(load.ceoDate !== undefined ? { ceoDate: load.ceoDate } : {}) });
227
+ if (from && (existing?.priority !== input.priority || JSON.stringify(existing.predecessors ?? []) !== JSON.stringify(input.predecessors) || (input.deadlineVersion !== undefined && existing.deadlineVersion !== input.deadlineVersion) || (input.ceoMinutes !== undefined && existing.ceoMinutes !== input.ceoMinutes) || (input.ceoDate !== undefined && existing.ceoDate !== input.ceoDate) || (input.accelerator !== undefined && (existing?.accelerator === true) !== (input.accelerator === true))))
228
+ setItem(chosen.version, input.id, { priority: input.priority, predecessors: input.predecessors, ...(input.deadlineVersion ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator !== undefined ? { accelerator: input.accelerator === true ? true : null } : {}) }, by);
229
+ if (!from) addItem(chosen.version, { id: input.id, title: input.title, owner: input.owner, priority: input.priority, predecessors: input.predecessors, deadlineVersion, ...(load.ceoMinutes !== undefined ? { ceoMinutes: load.ceoMinutes } : {}), ...(load.ceoDate !== undefined ? { ceoDate: load.ceoDate } : {}), ...(accelerator ? { accelerator: true } : {}) });
221
230
  }
222
231
  return decision;
223
232
  }
@@ -20,7 +20,7 @@ function createTable(store: MsgStore): void {
20
20
  store.db.exec(`CREATE TABLE IF NOT EXISTS seat_asks (
21
21
  id TEXT PRIMARY KEY, seat TEXT NOT NULL, origin TEXT NOT NULL,
22
22
  deadline INTEGER NOT NULL, timeout_minutes INTEGER NOT NULL DEFAULT 120, status TEXT NOT NULL DEFAULT 'pending',
23
- answer TEXT, answered_at INTEGER
23
+ answer TEXT, answered_at INTEGER, asked_at INTEGER
24
24
  ); CREATE TABLE IF NOT EXISTS seat_ask_outbox (
25
25
  id TEXT PRIMARY KEY, origin TEXT NOT NULL, text TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
26
26
  claim_until INTEGER NOT NULL DEFAULT 0, claim_token TEXT
@@ -28,6 +28,7 @@ function createTable(store: MsgStore): void {
28
28
  const askColumns = store.db.query('PRAGMA table_info(seat_asks)').all() as Array<{ name: string }>;
29
29
  if (!askColumns.some(({ name }) => name === 'answer')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answer TEXT');
30
30
  if (!askColumns.some(({ name }) => name === 'answered_at')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answered_at INTEGER');
31
+ if (!askColumns.some(({ name }) => name === 'asked_at')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN asked_at INTEGER');
31
32
  const columns = store.db.query('PRAGMA table_info(seat_ask_outbox)').all() as Array<{ name: string }>;
32
33
  if (!columns.some(({ name }) => name === 'claim_until')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_until INTEGER NOT NULL DEFAULT 0');
33
34
  if (!columns.some(({ name }) => name === 'claim_token')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_token TEXT');
@@ -87,8 +88,9 @@ export async function askSeat(text: string, origin: AskOrigin, command: CeoComma
87
88
  const store = (deps.open ?? openMsgStore)();
88
89
  try {
89
90
  createTable(store);
90
- store.db.query('INSERT INTO seat_asks (id, seat, origin, deadline, timeout_minutes, status) VALUES (?, ?, ?, ?, ?, ?)')
91
- .run(id, 'TC', JSON.stringify(origin), (deps.now ?? Date.now)() + timeout, minutes, 'pending');
91
+ const askedAt = (deps.now ?? Date.now)();
92
+ store.db.query('INSERT INTO seat_asks (id, seat, origin, deadline, timeout_minutes, status, asked_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
93
+ .run(id, 'TC', JSON.stringify(origin), askedAt + timeout, minutes, 'pending', askedAt);
92
94
  } finally { store.close(); }
93
95
  try {
94
96
  const result = await (deps.dispatch ?? dispatchCeoTask)('TC', `질문: ${body}\n답장 요청: ${id}\n회신: elanous seat answer ${id} "<답>"`, command,
@@ -119,12 +121,25 @@ export function answerSeatAsk(id: string, text: string, deps: Pick<SeatAskDeps,
119
121
  } finally { store.close(); }
120
122
  }
121
123
 
124
+ /** Return only the recorded reply's return address and measured ask time. */
125
+ export function seatAskReplyInfo(id: string, open: () => MsgStore = openMsgStore): { origin: AskOrigin; askedAt: number | null; answeredAt: number } {
126
+ const store = open();
127
+ try {
128
+ createTable(store);
129
+ const row = store.db.query('SELECT origin, asked_at, answered_at FROM seat_asks WHERE id = ? AND answer IS NOT NULL')
130
+ .get(id) as { origin: string; asked_at: number | null; answered_at: number } | null;
131
+ if (!row) throw new Error(`seat ask answer not recorded: ${id}`);
132
+ return { origin: JSON.parse(row.origin) as AskOrigin, askedAt: row.asked_at, answeredAt: row.answered_at };
133
+ } finally { store.close(); }
134
+ }
135
+
122
136
  /** Deliver recorded seat answers or expiration notices via the original surface. */
123
- export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
137
+ export async function deliverSeatAnswers(deps: SeatAskDeps & { askId?: string }): Promise<void> {
124
138
  const store = (deps.open ?? openMsgStore)();
125
139
  try {
126
140
  createTable(store);
127
- const pending = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE status = 'pending'").all() as AskRow[];
141
+ const pending = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE status = 'pending' AND (? IS NULL OR id = ?)")
142
+ .all(deps.askId ?? null, deps.askId ?? null) as AskRow[];
128
143
  const decide = store.db.transaction((id: string, now: number): 'answered' | 'expired' | null => {
129
144
  const ask = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE id = ? AND status = 'pending'").get(id) as AskRow | null;
130
145
  if (!ask) return null;
@@ -150,7 +165,8 @@ export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
150
165
  const status = decide(ask.id, (deps.now ?? Date.now)());
151
166
  if (status) debug.log('seat.ask', status, { seat: ask.seat, id: ask.id, via: origin.channel });
152
167
  }
153
- const outbox = store.db.query("SELECT id, origin, text FROM seat_ask_outbox WHERE status = 'pending'").all() as Array<{ id: string; origin: string; text: string }>;
168
+ const outbox = store.db.query("SELECT id, origin, text FROM seat_ask_outbox WHERE status = 'pending' AND (? IS NULL OR id = ?)")
169
+ .all(deps.askId ?? null, deps.askId ?? null) as Array<{ id: string; origin: string; text: string }>;
154
170
  for (const item of outbox) {
155
171
  const origin = JSON.parse(item.origin) as AskOrigin;
156
172
  if (deps.channel && origin.channel !== deps.channel) continue;
@@ -177,6 +193,17 @@ export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
177
193
  } finally { store.close(); }
178
194
  }
179
195
 
196
+ /** A pending outbox with a live claim is being delivered by another worker. */
197
+ export function seatAskDeliveryInProgress(id: string, open: () => MsgStore = openMsgStore): boolean {
198
+ const store = open();
199
+ try {
200
+ createTable(store);
201
+ const row = store.db.query("SELECT claim_until FROM seat_ask_outbox WHERE id = ? AND status = 'pending' AND claim_token IS NOT NULL")
202
+ .get(id) as { claim_until: number } | null;
203
+ return row !== null && row.claim_until > Date.now();
204
+ } finally { store.close(); }
205
+ }
206
+
180
207
  /** Confirm only this browser's displayed answers; polling alone never removes them. */
181
208
  export function acknowledgeSeatAnswers(clientId: string, ids: string[], open: () => MsgStore = openMsgStore): void {
182
209
  const store = open();
@@ -43,7 +43,7 @@ export type TcCandidate =
43
43
  | { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
44
44
  | { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
45
45
  export type TcPullRequest = { number: number; title: string; body?: string; state: 'OPEN' | 'MERGED' | 'CLOSED'; isDraft: boolean; createdAt: string; updatedAt?: string; mergedAt?: string | null; readyAt?: string; approvalWaitAt?: string; paths?: string[]; reviewDecision?: string };
46
- export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
46
+ export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'held' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
47
47
  export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off'; modeDowngradeReason?: string };
48
48
 
49
49
  export function seatLoopTickLine(result: SeatLoopResult): string {
@@ -53,6 +53,8 @@ export function seatLoopTickLine(result: SeatLoopResult): string {
53
53
  }
54
54
  export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null; action?: 'decision' | 'harness' | 'wait'; what?: string };
55
55
  export type SeatDeps = {
56
+ /** Version stamp for decision cards the seat loop raises. Default = the ledger's resolver (`git reflog` on the repo, up to 10 s) — tests inject a fixed one. */
57
+ resolveDecisionVersion?: (at: string) => { released: string | null; dev: string | null; codename: string | null };
56
58
  personaConfig?: PersonaLoopConfig;
57
59
  personaRegistry?: PersonaRegistry;
58
60
  personaDir?: string;
@@ -256,6 +258,59 @@ function queueKey(seat: string, item: SeatItem): string {
256
258
  return `seat-loop:${seat}:${createHash('sha256').update(handledItemKey(item)).digest('hex')}`;
257
259
  }
258
260
 
261
+ /** A decision ledger that raises cards — forwards the injected version resolver so tests never run the repo's `git reflog`. */
262
+ function cardLedger(root: string, deps: SeatDeps, now?: Date): DecisionLedger {
263
+ return new DecisionLedger({ stateDir: root, ...(now ? { now: () => now } : {}), ...(deps.resolveDecisionVersion ? { resolveVersion: deps.resolveDecisionVersion } : {}) });
264
+ }
265
+
266
+ const DEFAULT_REPEAT_STOP = 3;
267
+
268
+ function repeatStopLimit(config: SeatLoopConfig): number {
269
+ const value = config.repeatStop;
270
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : DEFAULT_REPEAT_STOP;
271
+ }
272
+
273
+ /** A finished attempt that never landed (no merged-PR evidence). `attempting` is the intent row of the same launch. A `refused` row whose reason is the AUTOQ retry note is the reconcile of a queued row, not a second try. */
274
+ function unlandedAttempt(entry: SeatEntry): boolean {
275
+ if (!entry.item || entry.action === 'seat-question' || entry.action === 'seat-answer' || entry.inquiry) return false;
276
+ if (entry.status === 'launched' || entry.status === 'hitl' || entry.status === 'held' || entry.status === 'attempting') return false;
277
+ if (entry.status === 'refused' && entry.reason === 'AUTOQ failed or cancelled; eligible for retry') return false;
278
+ return entry.status === 'queued' || entry.status === 'refused' || entry.status === 'outcome-unknown';
279
+ }
280
+
281
+ function repeatStopReason(attempts: number): string {
282
+ return `같은 칸 ${attempts}회 착지 0 — 사람 판단`;
283
+ }
284
+
285
+ function personReopened(ledger: readonly SeatEntry[], seat: string, item: SeatItem, root: string): boolean {
286
+ const key = handledItemKey(item);
287
+ const lastHold = [...ledger].reverse().find((entry) => entry.seat === seat && entry.item && handledItemKey(entry.item) === key && entry.status === 'held');
288
+ if (!lastHold) return false;
289
+ const heldAt = Date.parse(lastHold.at || lastHold.ts || '');
290
+ if (!Number.isFinite(heldAt)) return false;
291
+ const ref = decisionRef(seat, item);
292
+ return new DecisionLedger({ stateDir: root }).list({ status: 'all' }).some((card) => {
293
+ if (!card.refs?.includes(ref) || card.status !== 'decided' || !card.decidedAt) return false;
294
+ const decidedAt = Date.parse(card.decidedAt);
295
+ return Number.isFinite(decidedAt) && decidedAt > heldAt;
296
+ });
297
+ }
298
+
299
+ /** Prior attempts of this seat and handled item that finished without a landing. A queued row whose queue is still retryable has not finished. Queue rows with the same key count when the ledger has no receipt. */
300
+ function unlandedAttemptCount(seat: string, item: SeatItem, ledger: readonly SeatEntry[], root: string, deps: SeatDeps): number {
301
+ const key = handledItemKey(item);
302
+ const idem = queueKey(seat, item);
303
+ const outcome = deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot }));
304
+ const finished = (entry: SeatEntry) => unlandedAttempt(entry) && !(entry.status === 'queued' && entry.queueId && outcome(entry.queueId, root) === 'retryable');
305
+ const fromLedger = ledger.filter((entry) => entry.seat === seat && entry.item && handledItemKey(entry.item) === key && finished(entry));
306
+ const seen = new Set(fromLedger.flatMap((entry) => entry.queueId ? [entry.queueId] : []));
307
+ let rows: ReturnType<typeof listHarnessQueue> = [];
308
+ try { rows = (deps.queueItems ?? ((stateRoot: string) => listHarnessQueue({ root: stateRoot })))(root); }
309
+ catch (error) { observe('repeat-stop-queue-unreadable', { seat, item: item.id, error: String(error).slice(0, 200) }); }
310
+ const fromQueue = rows.filter((row) => row.seat === seat && row.idempotencyKey === idem && row.status === 'finished' && !seen.has(row.id));
311
+ return fromLedger.length + fromQueue.length;
312
+ }
313
+
259
314
  /** A crash (or a failed write) between AUTOQ enqueue and the `queued` row leaves an `attempting`/`outcome-unknown` row with no queue id. Find its queue row by the
260
315
  * seat-loop idempotency key so the outcome below (retry · launched · still queued) applies to it too (LOOP-LIVE1 review must-fix). */
261
316
  function recoverAttemptingQueueIds(ledger: readonly SeatEntry[], root: string, deps: SeatDeps): SeatEntry[] {
@@ -338,7 +393,7 @@ function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: strin
338
393
  || (entry.status === 'shadow' && entry.candidate?.kind === 'other-seat-cell-defect')
339
394
  || (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
340
395
  || (entry.inquiry?.to === 'CEO' && !!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))
341
- || (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown'))
396
+ || (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown' || (entry.status === 'held' && !personReopened(ledger, entry.seat, entry.item!, root ?? ''))))
342
397
  || (!inquiryRow(entry) && entry.status === 'attempting' && !(entry.item?.source === 'checklist' && ledger.slice(index + 1).some((later) =>
343
398
  later.seat === entry.seat && later.status === 'launched' && later.item?.source === 'checklist'
344
399
  && later.item.version === entry.item?.version && later.item.id === entry.item?.id
@@ -881,7 +936,7 @@ function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps
881
936
  if (judgment.state === 'absent') {
882
937
  if (config.mode === 'live-safe' && judgment.action?.action === 'escalate' && isRescueAllowedAction('decision-card')) {
883
938
  const ref = `seat-loop:neighbor:${seat}:${neighbor.id}:${lastSeenAt}`;
884
- new DecisionLedger({ stateDir: root, now: () => now }).raiseOnce({ title: `${seat}: 이웃 ${neighbor.id} 결측`, category: 'other',
939
+ cardLedger(root, deps, now).raiseOnce({ title: `${seat}: 이웃 ${neighbor.id} 결측`, category: 'other',
885
940
  scqa: { s: `${seat} 이웃 ${neighbor.id}의 마지막 맥락 버스 사건: ${lastSeenAt}.`,
886
941
  c: `설정한 heartbeat ${neighbor.heartbeat.everyMinutes}분 × ${neighbor.heartbeat.missedTicks}회 동안 새 사건이 없다.`,
887
942
  q: '결측 이웃의 업무를 어떻게 처리할까?' },
@@ -1403,6 +1458,38 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1403
1458
  return entry;
1404
1459
  }
1405
1460
  if (config.mode === 'live-safe') {
1461
+ const limit = repeatStopLimit(config);
1462
+ const attempts = unlandedAttemptCount(seat, item, ledger, stateRoot, deps);
1463
+ if (attempts >= limit && !personReopened(ledger, seat, item, stateRoot)) {
1464
+ const reason = repeatStopReason(attempts);
1465
+ entry.status = 'held';
1466
+ entry.reason = reason;
1467
+ const ref = decisionRef(seat, item);
1468
+ const prior = new DecisionLedger({ stateDir: stateRoot }).list({ status: 'all' }).find((card) => card.refs?.includes(ref) && card.status === 'open');
1469
+ if (!prior) {
1470
+ cardLedger(stateRoot, deps, now).raiseOnce({
1471
+ title: `${seat}: ${item.id} 반복 착지 0`,
1472
+ category: 'other',
1473
+ scqa: {
1474
+ s: `${seat} 칸 ${item.id} 가 착지 없이 ${attempts}회 끝났다.`,
1475
+ c: '같은 칸을 다시 발사하지 않고 사람 판단을 기다린다.',
1476
+ q: '이 칸을 계속할까, 멈출까?',
1477
+ },
1478
+ options: [
1479
+ { key: 'a', label: '다시 연다', consequence: '결정 뒤에 자리 루프가 한 번 더 시도할 수 있다' },
1480
+ { key: 'b', label: '멈춘다', consequence: '같은 칸은 다시 발사하지 않는다' },
1481
+ ],
1482
+ recommendation: { skipped: true, reason: '착지 없는 반복만으로 계속할지 자동 결정하지 않는다' },
1483
+ raisedBy: { agent: 'seat-loop' },
1484
+ refs: [ref],
1485
+ raisedAt: now.toISOString(),
1486
+ crossCheckSkipped: '반복 정지는 사람 판단',
1487
+ }, ref);
1488
+ }
1489
+ append(path, entry);
1490
+ observe('repeat-stopped', { seat, item: item.id, attempts, lastQueueId: [...ledger].reverse().find((row) => row.seat === seat && row.item && handledItemKey(row.item) === handledItemKey(item) && row.queueId)?.queueId ?? null });
1491
+ return entry;
1492
+ }
1406
1493
  let queued: { id: string };
1407
1494
  try {
1408
1495
  queued = await (deps.enqueue ?? ((assigned, text, root, idempotencyKey) => addHarnessQueue({ seat: assigned, say: text, idempotencyKey }, { root })))(seat, action.text, stateRoot, queueKey(seat, item));
@@ -11,6 +11,7 @@
11
11
  // ⛔ 종료(`process.exit`)는 여기가 «안» 한다 — 호출자가 자기 표면에 맞게 끝낸다.
12
12
  // 그래서 결과를 «세 값»으로 낸다: 발사 / 저작 전 중단 / 전제 검사 중단.
13
13
  import { linesOutsideFencedCode } from '../self-implement/goal-author.js';
14
+ import { recordAuthoredGoal } from './authored-goal-trace.js';
14
15
  import { retiredEntranceNotice, type EntranceDeclaration } from './entrance-registry.js';
15
16
  import {
16
17
  classifyConcurrentAuthoring,
@@ -58,7 +59,7 @@ function resolveSelfResolveClarifications(explicit: boolean | undefined): Resolv
58
59
  /** 저작기 산출 — `runGoalAuthorCli` 의 반환에서 이 흐름이 «실제로 쓰는» 것만. */
59
60
  export interface AskAuthoredGoal {
60
61
  readonly path: string;
61
- readonly authored?: { readonly document?: string; readonly grounded?: boolean | null } | null;
62
+ readonly authored?: { readonly document?: string; readonly grounded?: boolean | null; readonly authorRunId?: string } | null;
62
63
  }
63
64
 
64
65
  export type LaunchingTreeProbe =
@@ -158,6 +159,8 @@ export interface AskLaunchFlowDeps {
158
159
  /** ⛔ 못 얻으면 `null` — 같은 이유. */
159
160
  recentAuthoringSamples(): RecentAuthoringSample[] | null;
160
161
  authorGoal(args: readonly string[], options: Record<string, unknown>): Promise<AskAuthoredGoal>;
162
+ /** AUTHOR-TRACE seam (tests). */
163
+ recordAuthoredGoal?: typeof recordAuthoredGoal;
161
164
  /** Pod launches reserve a host slot before goal authoring begins. */
162
165
  beforeAuthoring?(): Promise<void>;
163
166
  /** 발사 전 권고용 분해 seam. 생략하면 전용 관측명을 준 기존 분해기를 쓴다. */
@@ -861,6 +864,24 @@ export async function runAskLaunchFlow(
861
864
  selfResolveClarifications: selfResolveClarifications.value,
862
865
  });
863
866
  deps.print(`[ask] 저작됨: ${authored.path}`);
867
+ // AUTHOR-TRACE: the authored goal file can be cleaned up later — keep path ⊕ sha256 ⊕ size in the run ledger and
868
+ // archive the body, so authoring length is measurable after the fact (GOAL-BENCH · AUTHOR-LITE).
869
+ const traceIds = {
870
+ ...(input.runId === undefined ? {} : { runId: input.runId }),
871
+ ...(authored.authored?.authorRunId === undefined ? {} : { authorRunId: authored.authored.authorRunId }),
872
+ };
873
+ if (authored.authored?.document !== undefined && (traceIds.runId !== undefined || traceIds.authorRunId !== undefined)) {
874
+ try {
875
+ const trace = (deps.recordAuthoredGoal ?? recordAuthoredGoal)(traceIds, authored.path, authored.authored.document);
876
+ deps.log('goal-authored-trace', { ...traceIds, ...trace }, 'info');
877
+ } catch (error) {
878
+ deps.log('goal-authored-trace-failed', { ...traceIds, error: error instanceof Error ? error.message : String(error) }, 'warn');
879
+ }
880
+ } else {
881
+ deps.log('goal-authored-trace-skipped', {
882
+ reason: authored.authored?.document === undefined ? 'no-authored-document' : 'no-run-or-author-id', path: authored.path,
883
+ }, 'warn');
884
+ }
864
885
  const markerCounts = authored.authored?.document === undefined
865
886
  ? null
866
887
  : countMissingAuthoredConstraintMarkers(input.askText, authored.authored.document);
@@ -0,0 +1,64 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { appendRunLedgerEntry, runLedgerDir } from '../self-implement/run-ledger.js';
5
+ import { debug } from '../debug/log.js';
6
+
7
+ /** AUTHOR-TRACE: what `harness say`/`ask` authored, kept with the run — the goal file itself may be cleaned up later,
8
+ * so the ledger carries path ⊕ sha256 ⊕ size and the body is archived next to the ledger. */
9
+ export interface AuthoredGoalTrace {
10
+ path: string;
11
+ sha256: string;
12
+ chars: number;
13
+ sections: number;
14
+ archivedAt: string;
15
+ }
16
+
17
+ /** `## ` headings outside fenced code (a fence closes only on the same marker, at least as long). */
18
+ function countSections(document: string): number {
19
+ let fence: { char: string; length: number } | null = null;
20
+ let count = 0;
21
+ for (const line of document.split('\n')) {
22
+ const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line);
23
+ if (fence) {
24
+ if (marker && marker[1]![0] === fence.char && marker[1]!.length >= fence.length && line.trim() === marker[1]) fence = null;
25
+ continue;
26
+ }
27
+ if (marker) { fence = { char: marker[1]![0]!, length: marker[1]!.length }; continue; }
28
+ if (/^ {0,3}##\s/.test(line)) count += 1;
29
+ }
30
+ return count;
31
+ }
32
+
33
+ export function authoredGoalTrace(path: string, document: string, archivedAt: string): AuthoredGoalTrace {
34
+ return {
35
+ path,
36
+ sha256: createHash('sha256').update(document).digest('hex'),
37
+ chars: [...document].length,
38
+ sections: countSections(document),
39
+ archivedAt,
40
+ };
41
+ }
42
+
43
+ /** Archive the body, emit one `goal-author`/`goal-authored` observation and — when the run id is already known —
44
+ * append a `goal-authored` run-ledger event. At `harness say` authoring time the run id usually does not exist yet;
45
+ * the authoring run id (`authorRunId`, joined to the goal by `goal-id-assigned`) keys the record then. */
46
+ export function recordAuthoredGoal(
47
+ ids: { runId?: string; authorRunId?: string },
48
+ path: string,
49
+ document: string,
50
+ options: { ledgerDir?: string; archiveDir?: string } = {},
51
+ ): AuthoredGoalTrace {
52
+ const key = ids.runId ?? ids.authorRunId;
53
+ if (!key) throw new Error('recordAuthoredGoal needs a runId or an authorRunId');
54
+ const ledgerDir = options.ledgerDir ?? runLedgerDir();
55
+ const archiveDir = options.archiveDir ?? join(dirname(ledgerDir), 'authored-goals');
56
+ const digest = createHash('sha256').update(document).digest('hex');
57
+ const archived = join(archiveDir, `${key}-${digest.slice(0, 12)}.md`);
58
+ if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
59
+ writeFileSync(archived, document, 'utf8');
60
+ const trace = authoredGoalTrace(path, document, archived);
61
+ debug.log('goal-author', 'goal-authored', { ...ids, ...trace });
62
+ if (ids.runId) appendRunLedgerEntry({ runId: ids.runId, event: 'goal-authored', data: { ...trace, ...(ids.authorRunId ? { authorRunId: ids.authorRunId } : {}) } }, ledgerDir);
63
+ return trace;
64
+ }
@@ -43,6 +43,7 @@ import { observeDeliverables, type DeliverableObservationResult, type Deliverabl
43
43
  import { debug } from '../debug/log.js';
44
44
  import { getUserConfig, type UserConfig } from '../user-config.js';
45
45
  import { resolveChildLlmPreference, type ChildLlmPreferenceInput, type ResolvedChildLlmPreference } from '../self-implement/child-llm-preference.js';
46
+ import { applyLaunchOverloadFailover, codexProviderNamed } from '../oauth/overload-failover-launch.js';
46
47
  import { readCachedGrokQuota } from '../oauth/codex-account-store.js';
47
48
  import { queryAbandonedDraftPrs, type AbandonedDraftPr } from '../cli/logs-abandoned-draft-prs.js';
48
49
  import { branchLineageSlug } from '../cli/pr-lineage.js';
@@ -1631,6 +1632,18 @@ function resolveChildLlmFromParts(
1631
1632
  return undefined;
1632
1633
  }
1633
1634
 
1635
+ /** Pinned `tools.selfImplement.childLlm` naming codex. Fallback-chain codex is not pinned. */
1636
+ function childLlmCodexPinned(flagProvider: string | undefined): boolean {
1637
+ if (flagProvider !== undefined) return false;
1638
+ try {
1639
+ const preference = resolveChildLlmPreference(getUserConfig());
1640
+ const provider = preference.chain[0]?.provider;
1641
+ return preference.mode === 'pinned' && preference.source.chain === 'pinned' && codexProviderNamed(provider);
1642
+ } catch {
1643
+ return false;
1644
+ }
1645
+ }
1646
+
1634
1647
  export function buildChildLlmSelection(
1635
1648
  opts: Pick<DevCliOpts, 'childLlmProvider' | 'childLlmModel' | 'childLlmEffort'> & {
1636
1649
  /** 발사 인자가 있을 때 관측용 선호를 읽는다. 없으면 getUserConfig. 기존 두 인자 시그니처는 그대로다. */
@@ -1967,7 +1980,12 @@ export function buildDevCliSpec(
1967
1980
  }
1968
1981
 
1969
1982
  if (path === 'self-mission') {
1970
- const childLlm = buildChildLlmSelection({ ...opts, readPreference: () => getUserConfig() });
1983
+ const builtChild = buildChildLlmSelection({ ...opts, readPreference: () => getUserConfig() });
1984
+ const childLlm = applyLaunchOverloadFailover(builtChild, {
1985
+ provider: opts.childLlmProvider,
1986
+ model: opts.childLlmModel,
1987
+ codexPinned: childLlmCodexPinned(opts.childLlmProvider),
1988
+ });
1971
1989
  // 잔량 조회(약 3초)는 한 발사에 한 번만 — 자식·부모가 같은 값을 쓴다.
1972
1990
  let grokQuotaOnce: ReturnType<typeof readCachedGrokQuota> | undefined;
1973
1991
  const readGrokQuotaOnce = () => (grokQuotaOnce ??= readGrokQuota());
@@ -1863,9 +1863,9 @@ async function observeDevTaskDispatch(spec: DevPipelineSpec, plan: ResolvedDevPl
1863
1863
  }
1864
1864
  }
1865
1865
 
1866
- async function inspectReleasePathBeforeMerge(seams: SelfImplementSeams, number: number, cwd: string): Promise<string | undefined> {
1866
+ async function inspectReleasePathBeforeMerge(seams: SelfImplementSeams, number: number, cwd: string, base?: string): Promise<string | undefined> {
1867
1867
  if (!seams.readPrFiles) throw new Error('release-path inspection unavailable — automatic merge refused');
1868
- const files = await seams.readPrFiles({ number, cwd });
1868
+ const files = await seams.readPrFiles({ number, cwd, ...(base ? { base } : {}) });
1869
1869
  if (!Array.isArray(files) || !files.every((path) => typeof path === 'string')) throw new Error('invalid PR file list — automatic merge refused');
1870
1870
  const path = releasePathHold(files);
1871
1871
  if (!path) return undefined;
@@ -2107,10 +2107,11 @@ async function runDevPipelineDispatch(
2107
2107
  ...assembledSeams,
2108
2108
  ...(assembledSeams.mergePr ? { mergePr: async (input) => {
2109
2109
  let path: string | undefined;
2110
- try { path = await inspectReleasePathBeforeMerge(assembledSeams, input.number, input.cwd); }
2110
+ try { path = await inspectReleasePathBeforeMerge(assembledSeams, input.number, input.cwd, plan.base); }
2111
2111
  catch (error) {
2112
- debug.log('self-dev.merge', 'release-path-inspection-failed', { number: input.number, error: String(error) });
2113
- return { merged: false, detail: `automatic merge refused: ${String(error)}` };
2112
+ const rateLimited = error instanceof Error && (error as { rateLimited?: boolean }).rateLimited === true;
2113
+ debug.log('self-dev.merge', 'release-path-inspection-failed', { number: input.number, error: String(error), ...(rateLimited ? { rateLimit: true } : {}) });
2114
+ return { merged: false, detail: rateLimited ? `automatic merge refused: rate-limit: ${String(error)}` : `automatic merge refused: ${String(error)}` };
2114
2115
  }
2115
2116
  if (path) return { merged: false, detail: `OP approval required: ${path}` };
2116
2117
  return assembledSeams.mergePr!(input);