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
@@ -10,6 +10,7 @@ import type { Command } from 'commander';
10
10
  import { collectUnifiedUsage, formatUnifiedUsage } from '../budget/unified-usage.js';
11
11
  import { consumeCodexResetCredits, listCodexResetCredits } from '../budget/codex-reset-credits.js';
12
12
  import { rollupRunUsage, type RunUsageInput } from '../budget/run-usage-rollup.js';
13
+ import { formatUsageOutcomes, outcomeTableHasCodexGrokUnknown, queryUsageOutcomes, type UsageOutcomesDeps, type UsageOutcomesReport } from '../budget/usage-outcomes.js';
13
14
  import { LogStore, logsDbPath, type LogQuery, type LogStoreRow } from '../mss/logging/log-store.js';
14
15
  import { LOGS_SINCE_OPTION, parseSince } from './logs-cli.js';
15
16
  import { formatCodexCreditPlan, type CodexCreditPlan } from '../budget/codex-credit-plan.js';
@@ -28,6 +29,9 @@ export interface UsageCliDeps {
28
29
  /** 계정 이름 → CODEX_HOME. ⛔ 여기 없는 이름은 «거부»한다(모르는 곳에 쓰지 않는다). */
29
30
  readonly homes?: Readonly<Record<string, string>>;
30
31
  readonly exit?: (code: number) => never;
32
+ /** 런 원장 결과 표. 시험은 디렉터리를 주입한다. */
33
+ readonly outcomes?: (sinceDays: number) => UsageOutcomesReport;
34
+ readonly outcomeDeps?: UsageOutcomesDeps;
31
35
  }
32
36
 
33
37
  /** 계정 이름 → CODEX_HOME 기본 표.
@@ -134,6 +138,28 @@ export function registerUsageCommand(program: Command, deps: UsageCliDeps = {}):
134
138
  // 🩸 그래서 ⑴ `--yes` 없이는 «안 쓴다» ⑵ 쓰기 «전»에 남은 수를 보여 준다
135
139
  // ⑶ 쓴 «뒤»에도 다시 조회해 「줄었나」를 값으로 낸다.
136
140
  // 📏 2026-09-24 현황 예: default 100%·team 100%·third 46% ⇒ third 에 쓰면 54% 가 버려진다.
141
+ const outcomes = deps.outcomes ?? ((sinceDays: number) => queryUsageOutcomes(sinceDays, deps.outcomeDeps));
142
+ usage
143
+ .command('outcomes')
144
+ .description('프로바이더별 무인 착지율 · 라운드 수 · 막힘 사유 분포 (런 원장 childProvider · 미기록은 불명)')
145
+ .option('--days <n>', '최근 N일', '2')
146
+ .option('--json', 'JSON 출력')
147
+ .action((opts: { days?: string; json?: boolean }, command: Command) => {
148
+ const json = opts.json || command.optsWithGlobals<{ json?: boolean }>().json;
149
+ const days = Number(opts.days);
150
+ if (!Number.isFinite(days) || days <= 0) {
151
+ out.log(`⛔ --days 는 양의 수: '${opts.days}'`);
152
+ exit(2);
153
+ return;
154
+ }
155
+ const report = outcomes(days);
156
+ if (json) {
157
+ out.log(JSON.stringify({ ...report, hasCodexGrokUnknown: outcomeTableHasCodexGrokUnknown(report) }, null, 2));
158
+ return;
159
+ }
160
+ out.log(formatUsageOutcomes(report));
161
+ });
162
+
137
163
  const list = deps.list ?? listCodexResetCredits;
138
164
  const consume = deps.consume ?? consumeCodexResetCredits;
139
165
  const homes = deps.homes ?? defaultHomes();
@@ -66,6 +66,7 @@ import { localOcrText } from './telegram-card-followup.js';
66
66
  import type { runGraph } from './graph-runner/runner.js';
67
67
  import { readSlashContextNow, renderTelegramNow } from './context-bus/context-now-surfaces.js';
68
68
  import type { ContextNowDeps } from './context-bus/context-now.js';
69
+ import { botUnavailableSlashReply, deriveTuiSlashAvailability, type TuiSlashAvailability } from './maturity/tui-slash-availability.js';
69
70
 
70
71
  const SLASH_FOCUS_TURNS_DEFAULT = 8;
71
72
 
@@ -142,6 +143,8 @@ export interface DiscordSelfMessageDeps {
142
143
  log?: (msg: string) => void;
143
144
  cardFollowupDeps?: { ocrText?: (path: string) => Promise<string | null>; runGraph?: typeof runGraph; rootDir?: () => string };
144
145
  nowDeps?: ContextNowDeps;
146
+ /** Override the TUI slash projection for isolated message-path tests. */
147
+ tuiSlashAvailability?: readonly TuiSlashAvailability[];
145
148
  }
146
149
 
147
150
  /** Compose the full discord self+interweave onMessage handler. */
@@ -399,6 +402,12 @@ export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessa
399
402
  log(`session cmd: ${ctx.text.trim().slice(0, 40)}`);
400
403
  return sessionReply;
401
404
  }
405
+ const slashName = /^\/([a-z0-9_-]{1,32})(?:\s|$)/i.exec(ctx.text.trim())?.[1]?.toLowerCase();
406
+ if (slashName) {
407
+ const entry = (deps.tuiSlashAvailability ?? deriveTuiSlashAvailability()).find(({ name }) => name === slashName);
408
+ const unavailable = botUnavailableSlashReply(entry, 'discord');
409
+ if (unavailable) return unavailable;
410
+ }
402
411
  log(`◀ ${ctx.userName ?? ctx.userId}: ${ctx.text.slice(0, 80)}${ctx.attachments.length ? ` (+첨부 ${ctx.attachments.length})` : ''}`);
403
412
  debug.log('discord.self.turn', 'inbound', { channelId: ctx.channelId, chars: ctx.text.length, attachments: ctx.attachments.length });
404
413
  // C2+ — 첨부 기억(사진만 먼저 올리는 패턴 지원). 명령 여부와 무관.
@@ -0,0 +1,24 @@
1
+ import type { TaskCard } from '../task-cards/card-store.js';
2
+
3
+ export interface SplitCell { id: string; title: string; seat?: string; host?: string }
4
+
5
+ /** FLOW1: only explicitly assigned work may enter a seat's queue. The same proposal is used in shadow and live. */
6
+ export function splitCard(card: TaskCard, _opts: { shadow?: boolean } = {}): SplitCell[] {
7
+ const intake = [...card.sections].reverse().find(section => section.key.startsWith('intake:wish:'));
8
+ let text = card.title;
9
+ if (intake) {
10
+ let body: unknown;
11
+ try { body = JSON.parse(intake.content); } catch { body = intake.content; }
12
+ if (typeof body === 'string') text = body;
13
+ else if (body && typeof body === 'object' && typeof (body as { text?: unknown }).text === 'string') text = (body as { text: string }).text;
14
+ }
15
+ const lines = text.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
16
+ const cells: SplitCell[] = lines.map((line, index) => {
17
+ const match = /^(?:[-*]|\d+[.)])?\s*\[(OP|MK|TC|UX)\]\s*(.+)$/u.exec(line);
18
+ if (match) return { id: `${card.id}-${index + 1}`, title: match[2]!.trim(), seat: match[1]! };
19
+ // 담당 없는 행도 칸으로 남긴다 — seat 가 없으니 대기열엔 못 들어가고 tick 이 unplaced-no-owner 로 센다.
20
+ return { id: `${card.id}-${index + 1}`, title: line.replace(/^(?:[-*]|\d+[.)])\s*/u, '') };
21
+ });
22
+ // 의뢰 본문이 있으면 담당이 하나도 없어도 행마다 칸으로 남긴다. 본문 없이 제목뿐일 때만 카드 한 칸.
23
+ return intake || cells.some(cell => cell.seat) ? cells : [{ id: card.id, title: card.title }];
24
+ }
@@ -23,6 +23,7 @@ import { queryRunningRuns } from '../self-implement/running-runs.js';
23
23
  import { DevPipelineError } from '../self-dev/dev-pipeline.js';
24
24
  import { runDraftSweep, sweepFailureReason, type DraftSweepAdapters, type DraftSweepResult, type SweepDraft, type SweepMergedPr, type SweepReviewGate } from '../self-dev/draft-sweep.js';
25
25
  import { debug } from '../debug/log.js';
26
+ import { decideNestedElanousLaunch, readNestedElanousDepth } from './nested-elanous-policy.js';
26
27
  import { dispatchTask, type DispatchTaskInput, type DispatchTaskDeps } from '../execution-loop/dispatch-task.js';
27
28
  import { launchRequestId, preLaunchGate, type PreLaunchGateDeps } from '../execution-loop/launch-gate.js';
28
29
  import { PR_LABELS } from '../github/pr-labels.js';
@@ -110,6 +111,8 @@ export interface HarnessAskSayOptions {
110
111
  supervisorSource?: HarnessSupervisorSource;
111
112
  goalType?: GoalType;
112
113
  correlation?: string;
114
+ /** Depth 0 only. At depth >= 1 this flag is ignored and the launch stays refused. */
115
+ nestedElanous?: 'allow';
113
116
  }
114
117
 
115
118
  export interface ResolvedHarnessSupervisor {
@@ -174,7 +177,8 @@ function registerHarnessCommonOptions(command: Command): Command {
174
177
  .option('--merge-by-host', 'Pod: merge-ready 까지 실행하고 병합은 발사 호스트가 재게이트')
175
178
  .option('--observe-only', 'elanous: child boot부터 SelfImplement 호출을 기록만 한다')
176
179
  .addOption(new Option('--no-supervise', 'self: supervisor 재개를 끔').hideHelp())
177
- .option('--dry-run', '변경 없이 발사 계획만 출력');
180
+ .option('--dry-run', '변경 없이 발사 계획만 출력')
181
+ .addOption(new Option('--nested-elanous <policy>', '깊이 0만: 중첩 elanous 발사를 허용(allow). 깊이 1 이상은 무시되고 거부가 유지된다').choices(['allow']));
178
182
  }
179
183
 
180
184
  type HarnessDryRunOpts = { dryRun?: boolean };
@@ -299,9 +303,33 @@ function normalizeHarnessCommonOptions(opts: HarnessAskSayOptions): HarnessAskSa
299
303
  ...(opts.observeOnly ? { observeOnly: true } : {}),
300
304
  ...(opts.goalType !== undefined ? { goalType: opts.goalType } : {}),
301
305
  ...resolveHarnessSupervisor(opts),
306
+ ...resolveNestedElanousOption(opts),
302
307
  };
303
308
  }
304
309
 
310
+ export class NestedElanousRefused extends Error {
311
+ constructor(readonly depth: number) {
312
+ super(`nested elanous refused (allow-ignored, depth ${depth})`);
313
+ this.name = 'NestedElanousRefused';
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Depth 0 may keep `--nested-elanous allow`.
319
+ * Depth >= 1 cannot flip the refusal: the flag is ignored, logged, and the launch stops.
320
+ * No flag means an ordinary launch — unset depth is 0 and nothing is refused here.
321
+ */
322
+ export function resolveNestedElanousOption(
323
+ opts: Pick<HarnessAskSayOptions, 'nestedElanous'>,
324
+ env: Record<string, string | undefined> = process.env,
325
+ ): { nestedElanous?: 'allow' } {
326
+ if (opts.nestedElanous !== 'allow') return {};
327
+ const decision = decideNestedElanousLaunch({ depth: readNestedElanousDepth(env), allow: true });
328
+ if (decision.reason === 'allow-ignored') throw new NestedElanousRefused(decision.depth);
329
+ if (!decision.allowed) return {};
330
+ return { nestedElanous: 'allow' };
331
+ }
332
+
305
333
  function normalizeHarnessAskSayOptions(opts: HarnessAskSayChildLlmOptions): HarnessAskSayChildLlmOptions {
306
334
  return {
307
335
  ...normalizeHarnessCommonOptions(opts),
@@ -704,7 +732,13 @@ async function dispatchHarnessAskSay(
704
732
  const podSeat = resolved.substrate === 'pod' && ['OP', 'TC', 'MK', 'UX'].includes(inheritedSeat ?? '')
705
733
  ? inheritedSeat as QueueSeat : undefined;
706
734
  const stampedSeat = resolved.substrate === 'pod' ? opts.seat ?? queueSeat ?? podSeat ?? assigned : assigned;
707
- const stampedOpts = stampedSeat ? { ...opts, seat: stampedSeat } : opts;
735
+ // Depth >= 1 cannot re-allow. Drop the flag here so the launch sees the refusal, not the raw argv.
736
+ const nested = resolveNestedElanousOption(opts);
737
+ const stampedOpts = {
738
+ ...opts,
739
+ ...(stampedSeat ? { seat: stampedSeat } : {}),
740
+ ...(nested.nestedElanous ? { nestedElanous: nested.nestedElanous } : { nestedElanous: undefined }),
741
+ };
708
742
  const launch = async () => {
709
743
  const previous = process.env.ELANOUS_HARNESS_SEAT;
710
744
  if (stampedSeat) process.env.ELANOUS_HARNESS_SEAT = stampedSeat;
@@ -7,10 +7,14 @@
7
7
  * 매니페스트가 실은 런 계약(`ELANOUS_RUN_CONTRACT`)으로 자기가 Pod 인 줄 안다(graph-run-contract.ts).
8
8
  */
9
9
  import { spawn } from 'node:child_process';
10
+ import { createHash, randomUUID } from 'node:crypto';
11
+ import { hostname } from 'node:os';
10
12
  import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
11
13
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
12
14
  import { selfDevRunsDir } from '../self-dev/run-store.js';
13
15
  import { findGitDir } from '../git-fs/locate.js';
16
+ import { logsDbPath } from '../mss/logging/log-store.js';
17
+ import { POD_REEMIT_LOGS_DB_ENV } from '../task-orchestrator/surfaces/pod-ledger-prod-sink.js';
14
18
  import { debug } from '../debug/log.js';
15
19
  import { ELANOUS_ENTRY_SCRIPT } from '../self-implement/seams.js';
16
20
  import { parsePodSourceSpec } from '../task-orchestrator/surfaces/pod-source-spec.js';
@@ -18,6 +22,7 @@ import { declaredGoalType, type GoalType } from '../self-implement/goal-author.j
18
22
  import { appendRunLedgerEntry } from '../self-implement/run-ledger.js';
19
23
  import { HARNESS_RUN_ID_ENV } from './harness-space.js';
20
24
  import { podMemoryLimitFor, type PodMemoryTier } from '../task-orchestrator/surfaces/self-implement-pod.js';
25
+ import { oldDoorInternalEnv } from '../self-dev/old-door.js';
21
26
  import { selectPodMemoryTier } from './pod-memory-policy.js';
22
27
 
23
28
  export interface HarnessPodDispatchInput {
@@ -46,44 +51,6 @@ export interface HarnessPodDispatchInput {
46
51
  readonly authorOnPod?: boolean;
47
52
  }
48
53
 
49
- /** 골 본문에 넣을 문장 — 코드 펜스가 문장을 삼키지 않게 백틱 런을 늘린다. */
50
- export function fenceVerbatimSentence(sentence: string): { fence: string; body: string } {
51
- const body = sentence.replace(/\r\n/g, '\n');
52
- const longest = body.match(/`+/g)?.reduce((n, run) => Math.max(n, run.length), 0) ?? 0;
53
- return { fence: '`'.repeat(Math.max(3, longest + 1)), body };
54
- }
55
-
56
- /** Pod 가 문장 그대로 `harness say` 를 돌리게 하는 골. 자격·풀은 기존 Pod 주입을 그대로 쓴다. */
57
- export function authorOnPodGoal(sentence: string): string {
58
- const { fence, body } = fenceVerbatimSentence(sentence);
59
- return [
60
- '# 골: Pod 안에서 harness say 저작',
61
- '',
62
- '- GoalType: implement',
63
- '- 대상 경로: src/harness/harness-pod-dispatch.ts',
64
- '',
65
- '## 할 일',
66
- '',
67
- '이 Pod 안에서, 그리고 이 Pod 안에서만, 아래 펜스의 문장을 한 글자도 바꾸지 않고',
68
- '`elanous harness say --substrate local` 의 인자로 실행한다.',
69
- '그 한 발이 골 저작·리뷰·게이트·구현·PR 까지 이 Pod 안에서 끝낸다.',
70
- '호스트에는 발사 영수증과 원장만 남는다. 호스트에서 저작·리뷰·게이트를 지휘하지 않는다.',
71
- '저작 자격은 이 Job 에 이미 주입된 Pod 자격 규칙을 그대로 쓴다.',
72
- '',
73
- '## 문장 (verbatim)',
74
- '',
75
- fence,
76
- body,
77
- fence,
78
- '',
79
- '## 수용 기준',
80
- '',
81
- '- 위 펜스 문장만으로 `elanous harness say --substrate local` 한 발이 이 Pod 에서 저작부터 PR 까지 간다.',
82
- '- 그 명령의 종료 코드가 0 이 아니면 이 골은 실패다.',
83
- '',
84
- ].join('\n');
85
- }
86
-
87
54
  export function podOrchestrateArgs(input: HarnessPodDispatchInput, goalFile: string): string[] {
88
55
  return [
89
56
  ELANOUS_ENTRY_SCRIPT, 'self', 'orchestrate', '--goal-file', goalFile,
@@ -116,7 +83,9 @@ export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDi
116
83
  return 2;
117
84
  }
118
85
  }
119
- const env = { ...process.env };
86
+ const env = { ...process.env, ...oldDoorInternalEnv('self-orchestrate') };
87
+ // POD-OBS(10-06): 자식 orchestrate 는 트리 파생 우주에서 돈다 — Pod 원장 재방출을 «발사한 집»의 logs.db 에도 남기게 경로를 넘긴다.
88
+ env[POD_REEMIT_LOGS_DB_ENV] = logsDbPath();
120
89
  let tempDir: string | undefined;
121
90
  let goalFile = '';
122
91
  let goalText: string;
@@ -124,21 +93,22 @@ export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDi
124
93
  goalFile = resolve(cwd, input.input);
125
94
  goalText = (deps.readFile ?? ((p) => readFileSync(p, 'utf8')))(goalFile);
126
95
  } else if (input.authorOnPod) {
127
- const goal = authorOnPodGoal(input.input);
128
- goalText = goal;
129
- const root = findGitDir(cwd)?.root ?? cwd;
130
- const runsDir = join(root, 'docs', 'goals');
96
+ // AUTHOR-POD2 (10-06) — the Pod runs `harness say` on the verbatim sentence itself (podJobManifest
97
+ // authorSentence branch). No wrapper goal and no host-side file under docs/goals: the 10-06 live run
98
+ // showed a wrapper goal is implemented as code by the Pod child instead of being executed.
99
+ goalText = input.input;
100
+ const runsDir = selfDevRunsDir();
131
101
  mkdirSync(runsDir, { recursive: true });
132
- tempDir = mkdtempSync(join(runsDir, '.pod-author-'));
133
- goalFile = join(tempDir, 'GOAL-pod-author-2026-10-05.md');
134
- writeFileSync(goalFile, goal, { mode: 0o600 });
135
- const runId = env[HARNESS_RUN_ID_ENV]?.trim();
136
- const receipt = { entrance: input.entrance, host: 'receipt-only', authorOnPod: true, goalFile };
102
+ tempDir = mkdtempSync(join(runsDir, 'pod-author-'));
103
+ goalFile = join(tempDir, 'sentence.txt');
104
+ writeFileSync(goalFile, input.input, { mode: 0o600 });
105
+ const runId = env[HARNESS_RUN_ID_ENV]?.trim() || `run-${randomUUID()}`;
106
+ env[HARNESS_RUN_ID_ENV] = runId;
107
+ const receipt = { entrance: input.entrance, host: hostname(), runId, authorOnPod: true,
108
+ sentenceChars: input.input.length, sentenceSha256: createHash('sha256').update(input.input).digest('hex') };
137
109
  debug.log('harness.substrate', 'author-on-pod-receipt', receipt);
138
- if (runId) {
139
- try { appendRunLedgerEntry({ runId, event: 'author-on-pod-receipt', data: receipt }); }
140
- catch (error) { debug.log('harness.substrate', 'author-on-pod-receipt-unwritten', { reason: error instanceof Error ? error.message : String(error) }); }
141
- }
110
+ try { appendRunLedgerEntry({ runId, event: 'author-on-pod-receipt', data: receipt }); }
111
+ catch (error) { debug.log('harness.substrate', 'author-on-pod-receipt-unwritten', { reason: error instanceof Error ? error.message : String(error) }); }
142
112
  } else {
143
113
  const goal = input.input;
144
114
  goalText = goal;
@@ -176,9 +146,9 @@ export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDi
176
146
  if (input.after) env.ELANOUS_POD_AFTER = input.after;
177
147
  if (input.authorOnPod) env.ELANOUS_POD_AUTHOR_ON_POD = '1';
178
148
  else delete env.ELANOUS_POD_AUTHOR_ON_POD;
179
- if (input.entrance === 'cli-harness-ask' || input.authorOnPod) {
149
+ if (input.entrance === 'cli-harness-ask') {
180
150
  const root = findGitDir(cwd)?.root;
181
- const path = input.authorOnPod ? goalFile : resolve(cwd, input.input);
151
+ const path = resolve(cwd, input.input);
182
152
  const relativePath = root ? relative(realpathSync(root), realpathSync(path)) : '';
183
153
  if (relativePath && relativePath !== '..' && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath)) {
184
154
  env.ELANOUS_POD_GOAL_DOC = relativePath;
@@ -208,7 +178,7 @@ export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDi
208
178
  child.once('error', () => { cleanup(); resolveStatus(1); });
209
179
  child.once('close', (code, signal) => { cleanup(); resolveStatus(code ?? ((signal ?? forwarded) === 'SIGINT' ? 130 : 143)); });
210
180
  }));
211
- const cleanup = () => { if (tempDir && !input.authorOnPod) rmSync(tempDir, { recursive: true, force: true }); };
181
+ const cleanup = () => { if (tempDir) rmSync(tempDir, { recursive: true, force: true }); };
212
182
  try {
213
183
  const status = run(process.execPath, args, env);
214
184
  if (typeof status === 'number' || status === null) {
@@ -727,10 +727,13 @@ export function buildHarnessSeams(deps: HarnessSeamsDeps): StagedHarnessSeams {
727
727
  let inspectionError: string | undefined;
728
728
  try {
729
729
  if (!deps.seams.readPrFiles) throw new Error('readPrFiles seam unavailable');
730
- const prFiles = await deps.seams.readPrFiles({ number: pr.number, cwd });
730
+ const prFiles = await deps.seams.readPrFiles({ number: pr.number, cwd, ...(deps.base ? { base: deps.base } : {}) });
731
731
  if (!Array.isArray(prFiles) || !prFiles.every((path) => typeof path === 'string')) throw new Error('invalid PR file list');
732
732
  holdPath = releasePathHold(prFiles);
733
- } catch (error) { inspectionError = String(error); }
733
+ } catch (error) {
734
+ const rateLimited = error instanceof Error && (error as { rateLimited?: boolean }).rateLimited === true;
735
+ inspectionError = rateLimited ? `rate-limit: ${String(error)}` : String(error);
736
+ }
734
737
  if (holdPath) {
735
738
  const protectedPath = holdPath;
736
739
  const prCwd = cwd;
@@ -745,7 +748,14 @@ export function buildHarnessSeams(deps: HarnessSeamsDeps): StagedHarnessSeams {
745
748
  observe('deployed-pr', { number: pr.number, url: pr.url, autoReview: false, mergeReason: 'release-path-hold' });
746
749
  } else if (inspectionError) {
747
750
  observe('release-path-inspection-failed', { number: pr.number, error: inspectionError });
748
- observe('deployed-pr', { number: pr.number, url: pr.url, autoReview: false, mergeReason: 'release-path-inspection-failed' });
751
+ observe('deployed-pr', {
752
+ number: pr.number,
753
+ url: pr.url,
754
+ autoReview: false,
755
+ mergeReason: inspectionError.startsWith('rate-limit:')
756
+ ? 'release-path-inspection-failed: rate-limit'
757
+ : 'release-path-inspection-failed',
758
+ });
749
759
  } else {
750
760
  let autoReviewLabeled = false;
751
761
  if (labels?.includes(AUTO_REVIEW_LABEL)) {
@@ -2,6 +2,9 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { debug } from '../debug/log.js';
5
+ import { releasePathHold, releasePrFilePaths } from '../self-dev/release-path-guard.js';
6
+ import { getUserConfig, type UserConfig } from '../user-config.js';
7
+ import type { QueueItem, QueueSeat } from './harness-queue.js';
5
8
  import type { RunGh, ScannedStoppedPr, StoppedPrCategory } from './helper-scan.js';
6
9
 
7
10
  export interface RepairGoalInput {
@@ -62,26 +65,154 @@ export function repairInputsFromBody(body: string): { originalRequest: string; m
62
65
 
63
66
  export type RepairShadowRow = { pr: number; category: StoppedPrCategory; goalHash: string; at: string };
64
67
 
65
- /** 그림자: 수리 골을 저작해 헬퍼 원장에만 남긴다(발사·라벨 0) · 같은 PR 은 한 번만. */
66
- export async function recordRepairShadows(rows: readonly ScannedStoppedPr[], deps: { runGh: RunGh; root: string; now?: Date }): Promise<RepairShadowRow[]> {
68
+ type RepairLedgerResult = 'shadow' | 'launched' | 'cap' | 'needs-human' | 'launch-failed';
69
+
70
+ type RepairLedgerLine = {
71
+ pr: number; category: StoppedPrCategory; goalHash: string; at: string;
72
+ mode: 'shadow' | 'live'; goal: string; result: RepairLedgerResult;
73
+ /** Harness queue item id — the run itself starts on a later queue tick. */ queueId?: string; reason?: string;
74
+ };
75
+
76
+ const RELEASE_FILE_PAGE = 100;
77
+
78
+ /** Changed files of the original PR. A failed read is a reason to hold, never a reason to launch. */
79
+ async function changedFiles(pr: number, runGh: RunGh): Promise<string[]> {
80
+ const pages: unknown[] = [];
81
+ for (let page = 1; ; page++) {
82
+ const raw = await runGh(['api', '--method', 'GET', '--raw-field', `per_page=${RELEASE_FILE_PAGE}`, '--raw-field', `page=${page}`, `repos/{owner}/{repo}/pulls/${pr}/files`]);
83
+ const parsed: unknown = JSON.parse(raw);
84
+ if (!Array.isArray(parsed)) throw new Error('gh api PR files returned an invalid file list');
85
+ pages.push(parsed);
86
+ if (parsed.length < RELEASE_FILE_PAGE) break;
87
+ }
88
+ return releasePrFilePaths(pages);
89
+ }
90
+
91
+ const QUEUE_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
92
+
93
+ /** 원 PR 의 자리 라벨(elanous:seat-<자리> 또는 자리 이름 라벨). 없거나 둘 이상이면 MK. */
94
+ function seatOf(labels: readonly { name?: string }[] | undefined): QueueSeat {
95
+ const found = new Set<QueueSeat>();
96
+ for (const label of labels ?? []) {
97
+ const name = label.name ?? '';
98
+ const prefixed = /^elanous:seat-(OP|TC|MK|UX)$/.exec(name)?.[1];
99
+ const bare = (QUEUE_SEATS as readonly string[]).includes(name) ? name : undefined;
100
+ const seat = prefixed ?? bare;
101
+ if (seat) found.add(seat as QueueSeat);
102
+ }
103
+ return found.size === 1 ? [...found][0]! : 'MK';
104
+ }
105
+
106
+ function helperSettings(config: UserConfig | undefined): { mode: 'shadow' | 'live'; perDay: number } {
107
+ const helper = config?.harness?.helper;
108
+ return {
109
+ mode: helper?.repair === 'live' ? 'live' : 'shadow',
110
+ perDay: typeof helper?.repairPerDay === 'number' && Number.isSafeInteger(helper.repairPerDay) && helper.repairPerDay >= 0
111
+ ? helper.repairPerDay : 3,
112
+ };
113
+ }
114
+
115
+ /** 그림자(기본): 수리 골을 원장에만 남긴다. live: 같은 함수로 대기열에 넣고 원 PR 을 superseded 로 표시한다. 같은 PR 은 평생 한 번. */
116
+ export async function recordRepairShadows(rows: readonly ScannedStoppedPr[], deps: {
117
+ runGh: RunGh; root: string; now?: Date;
118
+ config?: UserConfig;
119
+ /** 대기열 추가. 생략하면 addHarnessQueue — 실제 발사는 기존 대기열 틱이 한다. */
120
+ enqueue?: (input: { seat: QueueSeat; say: string; idempotencyKey: string }) => Promise<Pick<QueueItem, 'id'>>;
121
+ }): Promise<RepairShadowRow[]> {
67
122
  const ledger = join(deps.root, 'helper', 'repairs.jsonl');
68
- const seen = new Set<number>();
123
+ const prior: RepairLedgerLine[] = [];
69
124
  if (existsSync(ledger)) for (const line of readFileSync(ledger, 'utf8').split('\n')) {
70
125
  if (!line.trim()) continue;
71
- try { seen.add((JSON.parse(line) as { pr: number }).pr); } catch { /* malformed line is skipped */ }
126
+ try { prior.push(JSON.parse(line) as RepairLedgerLine); } catch { /* malformed line is skipped */ }
127
+ }
128
+ const { mode, perDay } = helperSettings(deps.config ?? getUserConfig());
129
+ const day = (deps.now ?? new Date()).toISOString().slice(0, 10);
130
+ // Terminal results end a PR for good. A shadow line blocks only while still in shadow (live must still launch it),
131
+ // and a cap or failed launch blocks only for the same UTC day so the PR is retried tomorrow (ACP must-fix).
132
+ // Ledger lines written before results existed have no `result`; they were shadow records.
133
+ for (const line of prior) if (!line.result) line.result = 'shadow';
134
+ const seen = new Set(prior.filter((line) => line.result === 'launched' || line.result === 'needs-human'
135
+ || (line.result === 'shadow' && mode === 'shadow')
136
+ || ((line.result === 'cap' || line.result === 'launch-failed') && line.at.slice(0, 10) === day)).map((line) => line.pr));
137
+ // Distinct PRs: a follow-up repair line for an already launched PR is not a second launch.
138
+ let launchedToday = new Set(prior.filter((line) => line.result === 'launched' && line.at.slice(0, 10) === day).map((line) => line.pr)).size;
139
+ // A launched line whose label/comment failed is repaired here — the follow-up only, never a second enqueue.
140
+ if (mode === 'live') {
141
+ const latest = new Map<number, RepairLedgerLine>();
142
+ for (const line of prior) latest.set(line.pr, line);
143
+ for (const line of latest.values()) {
144
+ if (line.result !== 'launched' || !line.reason?.startsWith('후속 실패') || !line.queueId) continue;
145
+ const followups: string[] = [];
146
+ if (line.reason.includes('label ·')) {
147
+ try { await deps.runGh(['pr', 'edit', String(line.pr), '--add-label', 'elanous:superseded']); }
148
+ catch (error) { followups.push(`label · ${error instanceof Error ? error.message : String(error)}`); }
149
+ }
150
+ if (line.reason.includes('comment ·')) {
151
+ try { await deps.runGh(['pr', 'comment', String(line.pr), '--body', `수리 대기열 항목 ${line.queueId} 로 대체(런은 대기열 틱에서 시작)`]); }
152
+ catch (error) { followups.push(`comment · ${error instanceof Error ? error.message : String(error)}`); }
153
+ }
154
+ const repaired: RepairLedgerLine = { ...line, at: (deps.now ?? new Date()).toISOString(), ...(followups.length ? { reason: `후속 실패 · ${followups.join(' · ')}` } : { reason: '후속 복구' }) };
155
+ appendFileSync(ledger, JSON.stringify(repaired) + '\n');
156
+ debug.log('harness.helper', 'repair-followup-retried', { pr: line.pr, queueId: line.queueId, remaining: followups.length });
157
+ }
72
158
  }
159
+ const enqueue = deps.enqueue ?? (async (input) => {
160
+ const { addHarnessQueue } = await import('./harness-queue.js');
161
+ return addHarnessQueue(input, { root: deps.root });
162
+ });
73
163
  const written: RepairShadowRow[] = [];
74
164
  for (const row of rows) {
75
165
  if ((row.category !== 'review-budget' && row.category !== 'review-oscillation') || seen.has(row.pr)) continue;
76
166
  const view = JSON.parse(await deps.runGh(['pr', 'view', String(row.pr), '--json', 'body'])) as { body: string };
167
+ // 자리 라벨은 본문 조회와 분리한다 — 기존 본문 조회 계약(--json body)을 그대로 둔다. 못 읽으면 MK.
168
+ let labels: { name?: string }[] | undefined;
169
+ if (mode === 'live') {
170
+ try { labels = (JSON.parse(await deps.runGh(['pr', 'view', String(row.pr), '--json', 'labels'])) as { labels?: { name?: string }[] }).labels; }
171
+ catch { labels = undefined; }
172
+ }
77
173
  const goal = authorRepairGoal({ pr: row.pr, category: row.category, ...repairInputsFromBody(view.body) });
78
174
  if (!goal) continue;
79
- const entry: RepairShadowRow = { pr: row.pr, category: row.category, goalHash: createHash('sha256').update(goal).digest('hex').slice(0, 16), at: (deps.now ?? new Date()).toISOString() };
175
+ const at = (deps.now ?? new Date()).toISOString();
176
+ const base = { pr: row.pr, category: row.category, goalHash: createHash('sha256').update(goal).digest('hex').slice(0, 16), at, goal };
177
+ let record: RepairLedgerLine = { ...base, mode: 'shadow', result: 'shadow' };
178
+ if (mode === 'live') {
179
+ let files: string[] | undefined;
180
+ try { files = await changedFiles(row.pr, deps.runGh); }
181
+ catch (error) { files = undefined; debug.log('harness.helper', 'repair-files-unreadable', { pr: row.pr, reason: String(error) }); }
182
+ const release = files && releasePathHold(files);
183
+ // 상한은 «발사한 수»의 상한이다. 사람 승인이 필요한 PR 은 그 수를 쓰지 않으므로 상한보다 먼저 가른다.
184
+ if (files && !release && launchedToday >= perDay) {
185
+ record = { ...base, mode: 'live', result: 'cap', reason: '상한' };
186
+ } else if (!files) {
187
+ // An unreadable file list is transient: hold today, retry on a later day (never a permanent needs-human) (ACP must-fix).
188
+ record = { ...base, mode: 'live', result: 'launch-failed', reason: '변경 파일 조회 실패 · 다음 날 재시도' };
189
+ } else if (release) {
190
+ record = { ...base, mode: 'live', result: 'needs-human', reason: '사람 승인 필요' };
191
+ } else {
192
+ let item: Pick<QueueItem, 'id'> | undefined;
193
+ try {
194
+ item = await enqueue({ seat: seatOf(labels), say: goal, idempotencyKey: `helper-repair-${row.pr}` });
195
+ // The queue item exists from here on: a later label/comment failure is recorded on the launched line,
196
+ // never as launch-failed (which would let the next scan enqueue a second repair) (ACP must-fix).
197
+ const followups: string[] = [];
198
+ try { await deps.runGh(['pr', 'edit', String(row.pr), '--add-label', 'elanous:superseded']); }
199
+ catch (error) { followups.push(`label · ${error instanceof Error ? error.message : String(error)}`); }
200
+ try { await deps.runGh(['pr', 'comment', String(row.pr), '--body', `수리 대기열 항목 ${item.id} 로 대체(런은 대기열 틱에서 시작)`]); }
201
+ catch (error) { followups.push(`comment · ${error instanceof Error ? error.message : String(error)}`); }
202
+ record = { ...base, mode: 'live', result: 'launched', queueId: item.id, ...(followups.length ? { reason: `후속 실패 · ${followups.join(' · ')}` } : {}) };
203
+ launchedToday += 1;
204
+ debug.log('harness.helper', 'repair-launched', { pr: row.pr, queueId: item.id, mode: 'live', followupFailures: followups.length });
205
+ } catch (error) {
206
+ record = { ...base, mode: 'live', result: 'launch-failed', reason: `발사 실패 · ${error instanceof Error ? error.message : String(error)}` };
207
+ debug.log('harness.helper', 'repair-launch-failed', { pr: row.pr, reason: record.reason });
208
+ }
209
+ }
210
+ }
80
211
  mkdirSync(dirname(ledger), { recursive: true });
81
- appendFileSync(ledger, JSON.stringify({ ...entry, mode: 'shadow', goal }) + '\n');
82
- debug.log('harness.helper', 'repair-authored', { pr: row.pr, category: row.category, mode: 'shadow' });
212
+ appendFileSync(ledger, JSON.stringify(record) + '\n');
213
+ if (record.result === 'shadow') debug.log('harness.helper', 'repair-authored', { pr: row.pr, category: row.category, mode: 'shadow' });
83
214
  seen.add(row.pr);
84
- written.push(entry);
215
+ written.push({ pr: base.pr, category: base.category, goalHash: base.goalHash, at: base.at });
85
216
  }
86
217
  return written;
87
218
  }
@@ -139,8 +139,19 @@ if (import.meta.main) {
139
139
  // 명시적 그림자 기록에서만 추가 PR 조회와 헬퍼 원장 쓰기를 수행한다(발사 0).
140
140
  const { recordRepairShadows } = await import('./helper-repair.js');
141
141
  const { effectiveInstanceRoot } = await import('../instance/resolve.js');
142
- const shadows = await recordRepairShadows(rows, { runGh, root: effectiveInstanceRoot() });
143
- if (shadows.length) console.log(`수리 골 그림자 ${shadows.length}건 → helper/repairs.jsonl`);
142
+ const { getUserConfig } = await import('../user-config.js');
143
+ const root = effectiveInstanceRoot();
144
+ const config = getUserConfig();
145
+ const live = config.harness?.helper?.repair === 'live';
146
+ const shadows = await recordRepairShadows(rows, {
147
+ runGh, root, config,
148
+ // live 만 대기열에 넣는다. 실제 발사는 기존 대기열 틱이 한다(새 발사 경로 0).
149
+ ...(live ? { enqueue: async (input) => {
150
+ const { addHarnessQueue } = await import('./harness-queue.js');
151
+ return addHarnessQueue(input, { root });
152
+ } } : {}),
153
+ });
154
+ if (shadows.length) console.log(live ? `수리 골 ${shadows.length}건 → helper/repairs.jsonl` : `수리 골 그림자 ${shadows.length}건 → helper/repairs.jsonl`);
144
155
  }
145
156
  if (json) console.log(JSON.stringify(rows, null, 2));
146
157
  else for (const row of rows) {