elanous 0.2.25 → 0.2.27

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 (244) 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/{1454-37fd16f479f5554a.js → 1454-a9c338f5bb556f42.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-9360b1285b2a346e.js → page-a1ce306364f08da9.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-07110da72c45e68f.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/board/page-d2d76d4a7eae91bd.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/bots/page-e5a9302c105725a3.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/consult/{page-44eaad486bc98a45.js → page-20662900d6b4b455.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/control/{page-97fa6cf3d34c7eb9.js → page-f707a509515ac50c.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-305cd294e754906e.js → page-6a16a48d1aa6e91f.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/exec/{page-45889e7070d11126.js → page-39f82acf619cca23.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/field/{page-16a2e893f83ae4d0.js → page-7c2c8f81b185f9c0.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/inside/page-ae7aff07e32676bb.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/intake/{page-56888bc744e08dac.js → page-482c60c1bc540d0a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/layout-7cb996c1aa08b6f5.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/live/{page-952c6ceec6d9de38.js → page-623e08fe4477d606.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-6d0202af568f4c63.js → page-db460cd8aecb8490.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/loops/page-ff27902d0b11ef4f.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/market/{page-74366ae2693a7d95.js → page-8397a5c6ca569bf4.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-4fb6fc34e20c07b7.js → page-1917ea87ea71ab6b.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/morning/{page-e9502921f4f01b1f.js → page-25a302273b054fd7.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/{not-found-b6169ea2f09cf287.js → not-found-670510eda8b9443f.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4bb7556e2e97a750.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-b9cc8a10bc6078b4.js → page-15c0a4095baddfba.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/{page-3d3fc73dd187ed52.js → page-4eb96fc7241c2456.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-9c04dac0831c2bbb.js → page-0d64ff212dc55d03.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/sessions/page-7f096248993fc2d4.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-04de9ca6d52a3893.js → page-e47fc72049149a38.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/{page-62527677bbc2fdc4.js → page-f7d808e31e005895.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-9a2e09c4a311764c.js → page-5c2b24b5496dad73.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/{page-bfeb35d69e8b24bb.js → page-76cdb531668b5191.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-b008aff96be56654.js → page-d376e0c83edc041a.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/today/{page-6445ac4cbb8dd869.js → page-dab163b338e3b966.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/trace/{page-852170303c6cd755.js → page-08019efef111ed2e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/vault/page-0260ffc7eda50db1.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-97c7361cc2a19319.js → page-980ce6fe36ad95eb.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-20162174b77f96cf.js → page-99c4e6c7345a9156.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-8c6901833f897496.js → page-0ab42e0f05823013.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +1 -0
  52. package/apps/pwa/out/approvals/index.html +1 -1
  53. package/apps/pwa/out/approvals/index.txt +13 -13
  54. package/apps/pwa/out/autopilot/index.html +1 -1
  55. package/apps/pwa/out/autopilot/index.txt +13 -13
  56. package/apps/pwa/out/board/index.html +1 -1
  57. package/apps/pwa/out/board/index.txt +13 -13
  58. package/apps/pwa/out/botlab/index.html +1 -1
  59. package/apps/pwa/out/botlab/index.txt +13 -13
  60. package/apps/pwa/out/bots/index.html +1 -1
  61. package/apps/pwa/out/bots/index.txt +13 -13
  62. package/apps/pwa/out/ceo/index.html +1 -1
  63. package/apps/pwa/out/ceo/index.txt +13 -13
  64. package/apps/pwa/out/chat/index.html +1 -1
  65. package/apps/pwa/out/chat/index.txt +13 -13
  66. package/apps/pwa/out/consult/index.html +1 -1
  67. package/apps/pwa/out/consult/index.txt +13 -13
  68. package/apps/pwa/out/control/index.html +1 -1
  69. package/apps/pwa/out/control/index.txt +13 -13
  70. package/apps/pwa/out/decisions/index.html +1 -1
  71. package/apps/pwa/out/decisions/index.txt +13 -13
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +13 -13
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +13 -13
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +13 -13
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +13 -13
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +13 -13
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +13 -13
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +13 -13
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +13 -13
  89. package/apps/pwa/out/live-run/index.html +1 -1
  90. package/apps/pwa/out/live-run/index.txt +13 -13
  91. package/apps/pwa/out/loops/index.html +1 -1
  92. package/apps/pwa/out/loops/index.txt +13 -13
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +13 -13
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +13 -13
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +13 -13
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +13 -13
  105. package/apps/pwa/out/ops/seats/index.html +1 -1
  106. package/apps/pwa/out/ops/seats/index.txt +13 -13
  107. package/apps/pwa/out/outputs/index.html +1 -1
  108. package/apps/pwa/out/outputs/index.txt +13 -13
  109. package/apps/pwa/out/reflection/index.html +1 -1
  110. package/apps/pwa/out/reflection/index.txt +14 -14
  111. package/apps/pwa/out/scheduler/index.html +1 -1
  112. package/apps/pwa/out/scheduler/index.txt +13 -13
  113. package/apps/pwa/out/sessions/index.html +1 -1
  114. package/apps/pwa/out/sessions/index.txt +13 -13
  115. package/apps/pwa/out/settings/devices/index.html +1 -1
  116. package/apps/pwa/out/settings/devices/index.txt +13 -13
  117. package/apps/pwa/out/settings/index.html +1 -1
  118. package/apps/pwa/out/settings/index.txt +13 -13
  119. package/apps/pwa/out/setup/done/index.html +1 -1
  120. package/apps/pwa/out/setup/done/index.txt +13 -13
  121. package/apps/pwa/out/setup/index.html +1 -1
  122. package/apps/pwa/out/setup/index.txt +13 -13
  123. package/apps/pwa/out/share/index.html +1 -1
  124. package/apps/pwa/out/share/index.txt +13 -13
  125. package/apps/pwa/out/showroom/index.html +1 -1
  126. package/apps/pwa/out/showroom/index.txt +13 -13
  127. package/apps/pwa/out/tasks/index.html +1 -1
  128. package/apps/pwa/out/tasks/index.txt +13 -13
  129. package/apps/pwa/out/term/index.html +1 -1
  130. package/apps/pwa/out/term/index.txt +13 -13
  131. package/apps/pwa/out/today/index.html +1 -1
  132. package/apps/pwa/out/today/index.txt +13 -13
  133. package/apps/pwa/out/trace/index.html +1 -1
  134. package/apps/pwa/out/trace/index.txt +13 -13
  135. package/apps/pwa/out/vault/index.html +1 -1
  136. package/apps/pwa/out/vault/index.txt +13 -13
  137. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  138. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  139. package/apps/pwa/out/workflows/index.html +1 -1
  140. package/apps/pwa/out/workflows/index.txt +13 -13
  141. package/apps/pwa/out/workspace/index.html +1 -1
  142. package/apps/pwa/out/workspace/index.txt +13 -13
  143. package/apps/pwa/out/worktrees/index.html +1 -1
  144. package/apps/pwa/out/worktrees/index.txt +13 -13
  145. package/package.json +1 -1
  146. package/scripts/ci-typecheck-changed.ts +81 -2
  147. package/scripts/coord-msg-bridge.ts +209 -0
  148. package/scripts/coord-post.sh +40 -7
  149. package/scripts/coord-requests-bridge.ts +10 -5
  150. package/scripts/expose-rubric-suggest.ts +1 -1
  151. package/scripts/measure-tsc-affected-roots.ts +10 -1
  152. package/scripts/release-loop/export-check-node.ts +8 -5
  153. package/scripts/release-loop/node-verdict.ts +7 -2
  154. package/src/agent/task-notification.ts +30 -1
  155. package/src/agent-substrate/review-observation.ts +9 -2
  156. package/src/agent-substrate/self-review-cli.ts +28 -6
  157. package/src/boot/daemon-tools/self-implement.ts +72 -4
  158. package/src/budget/llm-cost.ts +5 -1
  159. package/src/cli/control-cli.ts +29 -0
  160. package/src/cli/doctor-cli.ts +90 -8
  161. package/src/cli/msg-cli.ts +15 -5
  162. package/src/cli/pr-cli.ts +53 -0
  163. package/src/cli/pr-land-overlap-evidence.ts +18 -10
  164. package/src/cli/release-cli.ts +32 -5
  165. package/src/cli/seat-cli.ts +121 -0
  166. package/src/cli/self-utterances-cli.ts +140 -0
  167. package/src/cli/tasks-cli.ts +139 -9
  168. package/src/context-card/cell.ts +40 -0
  169. package/src/dashboard/boot-timing.ts +35 -0
  170. package/src/dashboard/index.ts +86 -24
  171. package/src/dashboard/input/chat-main-plain-turn-runtime.ts +26 -0
  172. package/src/dashboard/slash-runtime/dashboard-handlers.ts +6 -0
  173. package/src/debug/log.ts +1 -1
  174. package/src/domains/release-tool.ts +29 -12
  175. package/src/domains/surface-events.ts +16 -0
  176. package/src/elanous-config-dir.ts +2 -2
  177. package/src/fleet/host-doctor.ts +73 -2
  178. package/src/graph-runner/runner.ts +3 -1
  179. package/src/harness/harness-cli-command.ts +46 -4
  180. package/src/harness/harness-stop.ts +11 -7
  181. package/src/index.ts +35 -4
  182. package/src/instance/current.ts +2 -6
  183. package/src/instance/resolve.ts +5 -6
  184. package/src/llm/openai-sampling-fields.ts +89 -0
  185. package/src/llm/runtime-model-family.ts +61 -0
  186. package/src/llm/zai-coding-plan.ts +27 -1
  187. package/src/llm.ts +13 -89
  188. package/src/machines/machine-ledger.ts +31 -3
  189. package/src/maturity/cli-maturity.ts +13 -3
  190. package/src/msg/msg-store.ts +69 -7
  191. package/src/ops/overview-collect.ts +3 -2
  192. package/src/ops/overview-owner.ts +29 -0
  193. package/src/plugin-hooks/shell-hook.ts +17 -2
  194. package/src/plugin-missions/evaluator-runner.ts +18 -2
  195. package/src/plugins/install/plugin-install.ts +11 -1
  196. package/src/prompt-enhance/enhance.ts +11 -0
  197. package/src/release-loop/checklist.ts +41 -4
  198. package/src/release-loop/feature-store.ts +10 -9
  199. package/src/seat-loop/seat-loop.ts +5 -1
  200. package/src/self-dev/draft-sweep.ts +23 -14
  201. package/src/self-dev/orchestrate.ts +6 -2
  202. package/src/self-dev/run-supervisor.ts +35 -1
  203. package/src/self-implement/goal-author.ts +3 -27
  204. package/src/self-implement/goal-command-execution.ts +26 -0
  205. package/src/self-implement/orchestrator.ts +2 -1
  206. package/src/self-implement/pwa-import-graph.ts +194 -7
  207. package/src/self-implement/self-implement-runtime.ts +126 -21
  208. package/src/session/index.ts +14 -4
  209. package/src/session-runtime/index.ts +55 -11
  210. package/src/task-agent/cover.ts +28 -9
  211. package/src/task-agent/live-moves.ts +14 -6
  212. package/src/task-agent/rejudge-on-head.ts +220 -23
  213. package/src/task-agent/review-result-capture.ts +64 -1
  214. package/src/task-agent/shadow.ts +137 -22
  215. package/src/task-agent/task-hand.ts +18 -2
  216. package/src/task-orchestrator/surfaces/self-implement-pod.ts +1 -0
  217. package/src/tool-runtime/tool-search-runtime.ts +25 -0
  218. package/src/tui.ts +20 -1
  219. package/src/user-config.ts +3 -59
  220. package/src/version/packed-revision.json +1 -1
  221. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f08f6529d09ffae1.js +0 -1
  222. package/apps/pwa/out/_next/static/chunks/app/board/page-f000d54d258f9ef6.js +0 -1
  223. package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12149c51f7d04b1.js +0 -1
  224. package/apps/pwa/out/_next/static/chunks/app/bots/page-22f674e7e4c2c9d5.js +0 -1
  225. package/apps/pwa/out/_next/static/chunks/app/ceo/page-8888733e40cea6d1.js +0 -1
  226. package/apps/pwa/out/_next/static/chunks/app/chat/page-9717ec6964b92276.js +0 -1
  227. package/apps/pwa/out/_next/static/chunks/app/design-check/page-dea4b795741126fd.js +0 -1
  228. package/apps/pwa/out/_next/static/chunks/app/editor/page-35e05d7450461a67.js +0 -1
  229. package/apps/pwa/out/_next/static/chunks/app/inside/page-b2638013c0bf7dfd.js +0 -1
  230. package/apps/pwa/out/_next/static/chunks/app/layout-c44f93a41167536f.js +0 -1
  231. package/apps/pwa/out/_next/static/chunks/app/loops/page-1b810474507f3702.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6a14333d1b1b3967.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-8c91a9a66c9ec90a.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4e634c5b98fd4e93.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ed3070d637850548.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/reflection/page-128458c8d86fbb5f.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/sessions/page-cf0c2b0183991d09.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/share/page-89cb33bced824e4a.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/tasks/page-814394dcbae789b1.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/term/page-6a87325c87f09dd2.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/vault/page-087ceeb975ef272b.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-05a4b2c9b91034aa.js +0 -1
  243. /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → Ev2jivDMTSoEc-5rIXdcy}/_buildManifest.js +0 -0
  244. /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → Ev2jivDMTSoEc-5rIXdcy}/_ssgManifest.js +0 -0
@@ -10,6 +10,16 @@ export interface MachineRow {
10
10
  locationStatus: string;
11
11
  role: string;
12
12
  duties: string;
13
+ /** Fleet intent only. Live membership comes from Nexus machine resources. */
14
+ fleet?: MachineFleetIntent;
15
+ }
16
+
17
+ export interface MachineFleetIntent {
18
+ sshName: string;
19
+ role: 'worker' | 'hq';
20
+ platform: 'darwin' | 'linux' | 'wsl';
21
+ podPool: string;
22
+ cap: number;
13
23
  }
14
24
 
15
25
  export interface DeviceRow {
@@ -59,18 +69,36 @@ export function validateMachineLedger(value: unknown): MachineLedger {
59
69
  keys(data, ['title', 'policy', 'machines', 'devices', 'rules', 'changes'], 'ledger');
60
70
  if (!Array.isArray(data.machines) || !Array.isArray(data.devices)) throw new Error('invalid machines or devices');
61
71
  const ids = new Set<string>();
62
- const rows = <T extends { id: string }>(entries: unknown[], fields: string[], label: string): T[] => entries.map((entry, index) => {
72
+ const rows = <T extends { id: string }>(entries: unknown[], fields: string[], label: string, extraFields: string[] = []): T[] => entries.map((entry, index) => {
63
73
  const row = record(entry, `${label}[${index}]`);
64
- keys(row, fields, label);
74
+ keys(row, [...fields, ...extraFields], label);
65
75
  const result = Object.fromEntries(fields.map((field) => [field, text(row[field], `${label}.${field}`)])) as T;
66
76
  if (!idPattern.test(result.id) || ids.has(result.id)) throw new Error(`empty, duplicate or invalid id: ${result.id}`);
67
77
  ids.add(result.id);
68
78
  return result;
69
79
  });
80
+ const machines = rows<MachineRow>(data.machines, ['id', 'name', 'specs', 'locationStatus', 'role', 'duties'], 'machines', ['fleet']);
81
+ const sshNames = new Set<string>();
82
+ for (let index = 0; index < machines.length; index++) {
83
+ const raw = record(data.machines[index], `machines[${index}]`);
84
+ if (raw.fleet === undefined) continue;
85
+ const fleet = record(raw.fleet, `machines[${index}].fleet`);
86
+ keys(fleet, ['sshName', 'role', 'platform', 'podPool', 'cap'], 'fleet');
87
+ const sshName = text(fleet.sshName, 'fleet.sshName');
88
+ const role = text(fleet.role, 'fleet.role');
89
+ const platform = text(fleet.platform, 'fleet.platform');
90
+ const podPool = text(fleet.podPool, 'fleet.podPool');
91
+ if (!idPattern.test(sshName) || sshNames.has(sshName)) throw new Error(`duplicate or invalid fleet.sshName: ${sshName}`);
92
+ if (role !== 'worker' && role !== 'hq') throw new Error(`invalid fleet.role: ${role}`);
93
+ if (platform !== 'darwin' && platform !== 'linux' && platform !== 'wsl') throw new Error(`invalid fleet.platform: ${platform}`);
94
+ if (!Number.isSafeInteger(fleet.cap) || (fleet.cap as number) < 0) throw new Error('invalid fleet.cap');
95
+ sshNames.add(sshName);
96
+ machines[index]!.fleet = { sshName, role, platform, podPool, cap: fleet.cap as number };
97
+ }
70
98
  return {
71
99
  title: text(data.title, 'title'),
72
100
  policy: list(data.policy, 'policy'),
73
- machines: rows<MachineRow>(data.machines, ['id', 'name', 'specs', 'locationStatus', 'role', 'duties'], 'machines'),
101
+ machines,
74
102
  devices: rows<DeviceRow>(data.devices, ['id', 'name', 'status', 'purpose'], 'devices'),
75
103
  rules: list(data.rules, 'rules'),
76
104
  changes: list(data.changes, 'changes'),
@@ -18,14 +18,24 @@ export function cliRootVisibleFor(name: string, role: Role, { showAll }: { showA
18
18
  return grade === 'stable' || (role === 'contributor' && (grade === 'beta' || grade === 'tool'));
19
19
  }
20
20
 
21
- /** Only help is filtered: Commander still dispatches explicitly typed commands. */
22
- export function filterCliRootHelp(commands: readonly Command[], role: Role, showAll: boolean): { shown: number; hidden: number } {
21
+ /** Root --help first screen (SETUP-HELP-SLIM): the three stable onboarding commands. */
22
+ export const FIRST_SCREEN_COMMANDS: readonly string[] = ['start', 'setup', 'ask'];
23
+
24
+ /**
25
+ * Only help is filtered: Commander still dispatches explicitly typed commands.
26
+ * firstScreen narrows the visible set to FIRST_SCREEN_COMMANDS and showAll wins over it;
27
+ * the caller opts in per role — the wired default (deferred slice) enables it for the
28
+ * general role only, so owner/contributor keep today's list.
29
+ */
30
+ export function filterCliRootHelp(commands: readonly Command[], role: Role, showAll: boolean, firstScreen: boolean = false): { shown: number; hidden: number } {
31
+ const firstScreenActive = firstScreen && !showAll;
23
32
  let shown = 0;
24
33
  let hidden = 0;
25
34
  for (const command of commands) {
26
35
  const grade = cliRootMaturity(command.name(), commands);
27
36
  if (!grade) throw new Error(`cliRoot 에 등급 없는 명령: ${command.name()}`);
28
- const visible = cliRootVisibleFor(command.name(), role, { showAll }, commands);
37
+ const visible = cliRootVisibleFor(command.name(), role, { showAll }, commands)
38
+ && (!firstScreenActive || FIRST_SCREEN_COMMANDS.includes(command.name()));
29
39
  (command as Command & { _hidden: boolean })._hidden = !visible;
30
40
  if (visible) shown++;
31
41
  else hidden++;
@@ -1,5 +1,5 @@
1
1
  import { Database } from 'bun:sqlite';
2
- import { mkdirSync } from 'node:fs';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { elanousStateRoot } from '../autopilot/state-paths.js';
5
5
 
@@ -20,7 +20,7 @@ export interface UnreadRecipient {
20
20
  count: number;
21
21
  }
22
22
 
23
- const SEAT_ALIASES: Readonly<Record<string, string>> = {
23
+ export const SEAT_ALIASES: Readonly<Record<string, string>> = {
24
24
  S: 'OP', OP: 'OP', COO: 'OP',
25
25
  T: 'MK', MK: 'MK', CMO: 'MK',
26
26
  O: 'TC', TC: 'TC', CTO: 'TC',
@@ -41,6 +41,7 @@ export function validateMessageEnvelope(input: unknown): MessageEnvelope {
41
41
  const record = input as Record<string, unknown>;
42
42
  const from = canonicalSeatId(record.from as string);
43
43
  const to = canonicalSeatId(record.to as string);
44
+ if (from === 'ALL') throw new Error('ALL cannot send messages');
44
45
  if (typeof record.body !== 'string' || !record.body.trim()) throw new Error('message body must be nonempty');
45
46
  if (record.kind !== undefined && (typeof record.kind !== 'string' || !/^[a-z][a-z0-9_-]{0,63}$/.test(record.kind))) {
46
47
  throw new Error('invalid message kind');
@@ -54,6 +55,13 @@ function validCursor(cursor: number): number {
54
55
  return cursor;
55
56
  }
56
57
 
58
+ function unreadCountOn(db: Database, to: string): number {
59
+ return (db.query(`SELECT COUNT(*) AS count FROM msg_messages m
60
+ WHERE m.id > COALESCE((SELECT last_id FROM msg_cursors WHERE recipient = ?), 0)
61
+ AND (m.recipient = ? OR (m.recipient = 'ALL' AND m.sender <> ?))`)
62
+ .get(to, to, to) as { count: number }).count;
63
+ }
64
+
57
65
  interface MessageRow {
58
66
  id: number;
59
67
  sender: string;
@@ -88,10 +96,37 @@ export class MsgStore {
88
96
  CREATE TABLE IF NOT EXISTS msg_cursors (
89
97
  recipient TEXT PRIMARY KEY,
90
98
  last_id INTEGER NOT NULL DEFAULT 0 CHECK(last_id >= 0)
99
+ );
100
+ CREATE TABLE IF NOT EXISTS msg_provenance (
101
+ provenance_key TEXT PRIMARY KEY,
102
+ message_id INTEGER NOT NULL REFERENCES msg_messages(id)
91
103
  );`);
92
104
  }
93
105
 
94
106
  append(input: MessageEnvelope): Message {
107
+ return this.insertMessage(input);
108
+ }
109
+
110
+ /** Append once for a provenance key such as coord:<commentId>:<recipient>. */
111
+ appendWithProvenance(input: MessageEnvelope, provenanceKey: string): Message {
112
+ if (typeof provenanceKey !== 'string' || !/^coord:[^:]+:[^:]+$/.test(provenanceKey)) {
113
+ throw new Error('invalid message provenance key');
114
+ }
115
+ const msg = validateMessageEnvelope(input);
116
+ if (provenanceKey.split(':')[2] !== msg.to) throw new Error('provenance recipient does not match message recipient');
117
+ return this.db.transaction(() => {
118
+ const existing = this.db.query(`SELECT m.id, m.sender, m.recipient, m.body, m.kind, m.created_at
119
+ FROM msg_provenance p JOIN msg_messages m ON m.id = p.message_id WHERE p.provenance_key = ?`)
120
+ .get(provenanceKey) as MessageRow | null;
121
+ if (existing) return fromRow(existing);
122
+ const message = this.insertMessage(msg);
123
+ this.db.query('INSERT INTO msg_provenance(provenance_key, message_id) VALUES (?, ?)')
124
+ .run(provenanceKey, message.id);
125
+ return message;
126
+ }).immediate();
127
+ }
128
+
129
+ private insertMessage(input: MessageEnvelope): Message {
95
130
  const msg = validateMessageEnvelope(input);
96
131
  const createdAt = new Date().toISOString();
97
132
  const result = this.db.query(`INSERT INTO msg_messages(sender, recipient, body, kind, created_at)
@@ -104,10 +139,12 @@ export class MsgStore {
104
139
  /** Messages strictly after the supplied cursor, ordered by durable insertion ID. */
105
140
  listByRecipient(recipient: string, after: number = 0, limit: number = 100): Message[] {
106
141
  const to = canonicalSeatId(recipient);
142
+ if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
107
143
  validCursor(after);
108
144
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000) throw new Error('limit must be between 1 and 1000');
109
145
  return (this.db.query(`SELECT id, sender, recipient, body, kind, created_at FROM msg_messages
110
- WHERE recipient = ? AND id > ? ORDER BY id ASC LIMIT ?`).all(to, after, limit) as MessageRow[]).map(fromRow);
146
+ WHERE id > ? AND (recipient = ? OR (recipient = 'ALL' AND sender <> ?))
147
+ ORDER BY id ASC LIMIT ?`).all(after, to, to, limit) as MessageRow[]).map(fromRow);
111
148
  }
112
149
 
113
150
  list(recipient: string, after: number = 0, limit: number = 100): Message[] {
@@ -116,12 +153,14 @@ export class MsgStore {
116
153
 
117
154
  getCursor(recipient: string): number {
118
155
  const to = canonicalSeatId(recipient);
156
+ if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
119
157
  return (this.db.query('SELECT last_id FROM msg_cursors WHERE recipient = ?').get(to) as { last_id: number } | null)?.last_id ?? 0;
120
158
  }
121
159
 
122
160
  /** Atomically reject future IDs and keep concurrent or stale acknowledgements from rewinding. */
123
161
  advanceCursor(recipient: string, cursor: number): number {
124
162
  const to = canonicalSeatId(recipient);
163
+ if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
125
164
  validCursor(cursor);
126
165
  const result = this.db.query(`INSERT INTO msg_cursors(recipient, last_id)
127
166
  SELECT ?, ? WHERE ? <= (SELECT COALESCE(MAX(id), 0) FROM msg_messages)
@@ -133,11 +172,24 @@ export class MsgStore {
133
172
 
134
173
  ack(recipient: string, cursor: number): number { return this.advanceCursor(recipient, cursor); }
135
174
 
175
+ unreadCount(recipient: string): number {
176
+ const to = canonicalSeatId(recipient);
177
+ if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
178
+ return unreadCountOn(this.db, to);
179
+ }
180
+
136
181
  unreadRecipients(): UnreadRecipient[] {
137
- return this.db.query(`SELECT m.recipient AS recipient, COUNT(*) AS count
138
- FROM msg_messages m LEFT JOIN msg_cursors c ON c.recipient = m.recipient
139
- WHERE m.id > COALESCE(c.last_id, 0)
140
- GROUP BY m.recipient ORDER BY m.recipient ASC`).all() as UnreadRecipient[];
182
+ return this.db.query(`WITH recipients(recipient) AS (
183
+ SELECT 'OP' UNION SELECT 'MK' UNION SELECT 'TC' UNION SELECT 'UX'
184
+ UNION SELECT recipient FROM msg_messages WHERE recipient <> 'ALL'
185
+ UNION SELECT recipient FROM msg_cursors WHERE recipient <> 'ALL'
186
+ )
187
+ SELECT r.recipient AS recipient, COUNT(m.id) AS count
188
+ FROM recipients r
189
+ LEFT JOIN msg_cursors c ON c.recipient = r.recipient
190
+ JOIN msg_messages m ON m.id > COALESCE(c.last_id, 0)
191
+ AND (m.recipient = r.recipient OR (m.recipient = 'ALL' AND m.sender <> r.recipient))
192
+ GROUP BY r.recipient ORDER BY r.recipient ASC`).all() as UnreadRecipient[];
141
193
  }
142
194
 
143
195
  unread(): UnreadRecipient[] { return this.unreadRecipients(); }
@@ -147,3 +199,13 @@ export class MsgStore {
147
199
 
148
200
  export function defaultMsgStorePath(): string { return join(elanousStateRoot(), 'msg', 'messages.db'); }
149
201
  export function openMsgStore(path?: string): MsgStore { return new MsgStore(path); }
202
+
203
+ /** Observe one recipient without creating a store or advancing its acknowledgement. */
204
+ export function readUnreadCount(recipient: string, path: string = defaultMsgStorePath()): number | null {
205
+ const to = canonicalSeatId(recipient);
206
+ if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
207
+ if (!existsSync(path)) return null;
208
+ const db = new Database(path, { readonly: true });
209
+ try { return unreadCountOn(db, to); }
210
+ finally { db.close(); }
211
+ }
@@ -16,6 +16,7 @@ import {
16
16
  type OverviewSnapshot, type OwnerTaskRow, type ParentsInfo, type PodMemberRow, type ReleaseRunInfo, type ScheduleInfo,
17
17
  } from './overview.js';
18
18
  import { SEAT_ERA_SOURCE, readDispatcherBeats, readDispatcherCapacity, readSeatEraHanded, seatEraFeederDir } from './overview-seat-era.js';
19
+ import { matchesOverviewOwner, normalizeOverviewOwner } from './overview-owner.js';
19
20
 
20
21
  const GB = 1024 ** 3;
21
22
  /** 하트비트 주기 30 s × 2 (RFC §A2 «낡음»). */
@@ -77,8 +78,8 @@ export function ownerRows(cards: readonly TaskCard[], sinceIso: string, ownerFil
77
78
  const rows: Record<string, OwnerTaskRow> = {};
78
79
  for (const card of cards) {
79
80
  if (!card || typeof card.createdAt !== 'string' || card.createdAt < sinceIso) continue;
80
- const owner = card.seat ? `seat:${card.seat}` : 'unassigned';
81
- if (ownerFilter && ownerFilter !== 'all' && owner !== ownerFilter && owner !== `seat:${ownerFilter}`) continue;
81
+ const owner = normalizeOverviewOwner(card.seat);
82
+ if (ownerFilter !== undefined && !matchesOverviewOwner(owner, ownerFilter)) continue;
82
83
  const row = rows[owner] ??= { handed: 0, launched: 0, prBound: 0, failed: 0, launchFailed: 0 };
83
84
  if (card.status === 'handed') row.handed++;
84
85
  else if (card.status === 'launched') row.launched++;
@@ -0,0 +1,29 @@
1
+ /** Owner labels used by ops overview task rows and the --owner filter. */
2
+ type OverviewOwnerKind = 'seat' | 'steward' | 'orchestrator' | 'external';
3
+ type OverviewOwner = `${OverviewOwnerKind}:${string}` | 'unassigned';
4
+
5
+ const OWNER_KINDS = new Set<OverviewOwnerKind>(['seat', 'steward', 'orchestrator', 'external']);
6
+
7
+ /** Normalize a task-card owner into a stable kind-prefixed overview key. */
8
+ export function normalizeOverviewOwner(value: unknown): OverviewOwner {
9
+ if (typeof value !== 'string' || value.trim() === '') return 'unassigned';
10
+ const normalized = value.trim();
11
+ const separator = normalized.indexOf(':');
12
+ if (separator >= 0) {
13
+ const kind = normalized.slice(0, separator).toLowerCase() as OverviewOwnerKind;
14
+ const name = normalized.slice(separator + 1).trim();
15
+ if (OWNER_KINDS.has(kind) && name) return `${kind}:${name}`;
16
+ }
17
+ const kind = normalized.toLowerCase();
18
+ if (kind === 'steward' || kind === 'orchestrator' || kind === 'external') return `${kind}:default`;
19
+ return `seat:${normalized}`;
20
+ }
21
+
22
+ /** Whether a normalized owner key matches a CLI filter, accepting either the full key or its kind. */
23
+ export function matchesOverviewOwner(owner: OverviewOwner, filter?: string): boolean {
24
+ if (!filter || filter === 'all') return true;
25
+ const normalizedFilter = filter.trim();
26
+ if (!normalizedFilter) return true;
27
+ if (normalizedFilter.toLowerCase() === 'unassigned') return owner === 'unassigned';
28
+ return owner === normalizeOverviewOwner(normalizedFilter) || owner.startsWith(`${normalizedFilter.toLowerCase()}:`);
29
+ }
@@ -118,6 +118,7 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
118
118
  let stdout = '';
119
119
  let stderrBuffer = '';
120
120
  let killed = false;
121
+ let stdinError: string | undefined;
121
122
  const onAbort = () => {
122
123
  if (!killed) {
123
124
  killed = true;
@@ -132,6 +133,14 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
132
133
  opts.abortSignal.addEventListener('abort', onAbort, { once: true });
133
134
 
134
135
  child.stdout?.on('data', (chunk) => { stdout += String(chunk); });
136
+ // A short-lived command can close its read end before end() finishes.
137
+ // EPIPE says only that stdin was unread; stdout and exit status still
138
+ // determine the hook result. Other pipe errors are real I/O failures.
139
+ child.stdin?.on('error', (err: NodeJS.ErrnoException) => {
140
+ if (err.code === 'EPIPE') return;
141
+ stdinError = `stdin error: ${err.message}`;
142
+ opts.onStderr(stdinError);
143
+ });
135
144
  child.stderr?.on('data', (chunk) => {
136
145
  stderrBuffer += String(chunk);
137
146
  let idx;
@@ -145,13 +154,19 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
145
154
  clearTimeout(timer);
146
155
  if (stderrBuffer.trim()) opts.onStderr(stderrBuffer.trim());
147
156
  // killed => treat as a non-zero exit so dispatcher abort.
148
- resolve({ stdout, exitCode: killed ? 124 : (code ?? 1) });
157
+ resolve({ stdout, exitCode: killed ? 124 : stdinError ? 127 : (code ?? 1), error: stdinError });
149
158
  });
150
159
  child.on('error', (err) => {
151
160
  clearTimeout(timer);
152
161
  opts.onStderr(`spawn error: ${err.message}`);
153
162
  resolve({ stdout, exitCode: 127 });
154
163
  });
155
- try { child.stdin?.end(opts.stdin); } catch { /* pipe may have closed */ }
164
+ try { child.stdin?.end(opts.stdin); }
165
+ catch (err) {
166
+ if ((err as NodeJS.ErrnoException).code !== 'EPIPE') {
167
+ stdinError = `stdin error: ${(err as Error).message}`;
168
+ opts.onStderr(stdinError);
169
+ }
170
+ }
156
171
  });
157
172
  }
@@ -137,6 +137,7 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
137
137
  let stderrBuf = '';
138
138
  let killed = false;
139
139
  let timedOut = false;
140
+ let stdinError: string | undefined;
140
141
  const kill = (timeout: boolean) => {
141
142
  if (killed) return;
142
143
  killed = true;
@@ -150,6 +151,14 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
150
151
  opts.abortSignal.addEventListener('abort', () => kill(false), { once: true });
151
152
 
152
153
  child.stdout?.on('data', (chunk) => { stdout += String(chunk); });
154
+ // Early exit may close stdin before end() finishes. A broken pipe must
155
+ // not replace the child's stdout/exit result; other write errors must
156
+ // surface as evaluator errors.
157
+ child.stdin?.on('error', (err: NodeJS.ErrnoException) => {
158
+ if (err.code === 'EPIPE') return;
159
+ stdinError = `stdin error: ${err.message}`;
160
+ opts.onStderr(stdinError);
161
+ });
153
162
  child.stderr?.on('data', (chunk) => {
154
163
  stderrBuf += String(chunk);
155
164
  let idx;
@@ -164,8 +173,9 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
164
173
  if (stderrBuf.trim()) opts.onStderr(stderrBuf.trim());
165
174
  resolve({
166
175
  stdout,
167
- exitCode: timedOut ? 124 : (code ?? 1),
176
+ exitCode: timedOut ? 124 : stdinError ? 127 : (code ?? 1),
168
177
  timedOut,
178
+ error: stdinError,
169
179
  });
170
180
  });
171
181
  child.on('error', (err) => {
@@ -173,6 +183,12 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
173
183
  opts.onStderr(`spawn error: ${err.message}`);
174
184
  resolve({ stdout, exitCode: 127, timedOut: false });
175
185
  });
176
- try { child.stdin?.end(opts.stdin); } catch { /* pipe may have closed */ }
186
+ try { child.stdin?.end(opts.stdin); }
187
+ catch (err) {
188
+ if ((err as NodeJS.ErrnoException).code !== 'EPIPE') {
189
+ stdinError = `stdin error: ${(err as Error).message}`;
190
+ opts.onStderr(stdinError);
191
+ }
192
+ }
177
193
  });
178
194
  }
@@ -438,6 +438,13 @@ function declaresMain(packageRoot: string): boolean {
438
438
  return !existsSync(join(packageRoot, 'plugin.json')) && !existsSync(join(packageRoot, '.codex-plugin', 'plugin.json'));
439
439
  }
440
440
 
441
+ /** [wiring] Caller: installPlugin. The PluginManifest normalized by
442
+ * loadPluginManifestFromDir is handed into the install path here — the install
443
+ * destination follows the manifest identity, never the source spec/dir name. */
444
+ export function pluginInstallPath(root: string, market: string, manifest: PluginManifest): string {
445
+ return join(root, 'plugins', market, manifest.id, manifest.version);
446
+ }
447
+
441
448
  export async function installPlugin(spec: string, opts: InstallOptions = {}): Promise<InstalledPlugin> {
442
449
  const root = resolve(opts.root ?? elanousStateRoot());
443
450
  const stageRoot = join(root, 'plugins', '.staging');
@@ -466,6 +473,10 @@ export async function installPlugin(spec: string, opts: InstallOptions = {}): Pr
466
473
  const manifest: PluginManifest = loadPluginManifestFromDir(join(stage, 'package'), { id: source.expectedName ?? basename(spec) }).manifest;
467
474
  if (!NAME.test(manifest.id) || !VERSION.test(manifest.version)) fail('io', 'invalid plugin name or version');
468
475
  if (source.expectedName && (manifest.id !== source.expectedName || manifest.version !== source.expectedVersion)) fail('conflict', 'market name or version differs from manifest');
476
+ // [wiring] installPlugin is the caller: the PluginManifest normalized above by
477
+ // loadPluginManifestFromDir (present manifest or the loader's manifest-absent
478
+ // fallback) is handed into the install path here, past the NAME/VERSION gates.
479
+ const destination = pluginInstallPath(root, source.market, manifest);
469
480
  const packageRoot = join(stage, 'package');
470
481
  const mainPath = inside(packageRoot, manifest.main);
471
482
  // A skills/graphs-only pack (official `elanous-basics`) declares no `main` — the default `./plugin.ts` is not a promise.
@@ -480,7 +491,6 @@ export async function installPlugin(spec: string, opts: InstallOptions = {}): Pr
480
491
  const path = inside(packageRoot, asset);
481
492
  if (!existsSync(path) || !realpathSync(path).startsWith(`${realpathSync(packageRoot)}${sep}`)) fail('io', `plugin asset not found or unsafe: ${asset}`);
482
493
  }
483
- const destination = join(root, 'plugins', source.market, name, version);
484
494
  const emit = (event: InstallEvent) => opts.onEvent?.(event);
485
495
  emit({ event: 'resolve', plugin: name, version, sha256: source.sha256 });
486
496
  let security: ReturnType<typeof inspectPluginSecurity>;
@@ -393,6 +393,8 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
393
393
  let complication: string | undefined;
394
394
  let decisionSignal: EnhanceResult['decisionSignal'];
395
395
  let enhancedBy: 'llm' | 'fallback' = 'llm';
396
+ let fallbackReason: 'llm-error' | 'empty-reply' | 'unparsed' | 'none' = 'none';
397
+ let replyChars = 0;
396
398
  const groundedFacts = opts.groundedFacts ?? [];
397
399
  const wantsScqa = groundedFacts.length > 0;
398
400
 
@@ -439,6 +441,10 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
439
441
  try { opts.onUsage?.(usage); } catch { /* usage observation must not change enhancement */ }
440
442
  },
441
443
  });
444
+ replyChars = rawOut.length;
445
+ if (rawOut.trim().length === 0) {
446
+ fallbackReason = 'empty-reply';
447
+ }
442
448
  const parsed = parseEnhanceJson(rawOut);
443
449
  if (parsed) {
444
450
  goal = parsed.goal;
@@ -449,9 +455,11 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
449
455
  complication = parsed.complication;
450
456
  decisionSignal = parsed.decisionSignal;
451
457
  } else {
458
+ fallbackReason = fallbackReason === 'empty-reply' ? 'empty-reply' : 'unparsed';
452
459
  enhancedBy = 'fallback';
453
460
  }
454
461
  } catch (e) {
462
+ fallbackReason = 'llm-error';
455
463
  debug.log('prompt-enhance', 'llm-error', { error: String((e as { message?: string })?.message ?? e).slice(0, 160) }, { level: 'error' });
456
464
  enhancedBy = 'fallback';
457
465
  }
@@ -461,6 +469,7 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
461
469
  checklist = extractChecklistFallback(original);
462
470
  checklistProvenance = checklist.map(() => 'unknown');
463
471
  enhancedBy = 'fallback';
472
+ if (fallbackReason === 'none') fallbackReason = 'unparsed';
464
473
  }
465
474
  if (!constraints.length) {
466
475
  constraints = [
@@ -515,6 +524,8 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
515
524
  enhancedChars: enhanced.length,
516
525
  checklist: checklist.length,
517
526
  enhancedBy,
527
+ fallbackReason,
528
+ replyChars,
518
529
  model,
519
530
  verbatimPreserved,
520
531
  // ⭐ 관측 — 「요약을 요구했나 / 받았나 / 옮겨 적어 버렸나」를 «다른 값»으로 남긴다.
@@ -10,6 +10,8 @@ export type ChecklistDisposition = 'move' | 'known-issue' | 'block';
10
10
  /** `screen` = a five-surface feature cell — its evidence carries a `짝:` line (MANUAL-five-surface-parity §A). `postpub` = measured after publication, outside the pre-publication gate. */
11
11
  export type ChecklistKind = 'screen' | 'postpub';
12
12
  const CHECKLIST_KINDS: readonly ChecklistKind[] = ['screen', 'postpub'];
13
+ export type ChecklistDispatchKind = 'code' | 'ops' | 'measure' | 'decision' | 'umbrella';
14
+ const CHECKLIST_DISPATCH_KINDS: readonly ChecklistDispatchKind[] = ['code', 'ops', 'measure', 'decision', 'umbrella'];
13
15
  export interface ChecklistItem {
14
16
  id: string;
15
17
  title: string;
@@ -27,6 +29,8 @@ export interface ChecklistItem {
27
29
  evidence?: string;
28
30
  disposition?: ChecklistDisposition;
29
31
  kind?: ChecklistKind;
32
+ /** Whether the seat dispatcher should launch a coding task. Missing in old rows means code. */
33
+ dispatchKind?: ChecklistDispatchKind;
30
34
  updatedAt: string;
31
35
  updatedBy: string;
32
36
  }
@@ -122,6 +126,37 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
122
126
 
123
127
  const REPO_ROOT = join(import.meta.dir, '..', '..');
124
128
 
129
+ const PATH_TOKEN = /(?:^|[^A-Za-z0-9_.\/-])((?:(?:[A-Za-z0-9_.-]+\/)+)?[A-Za-z0-9_.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|yaml|yml|sh|py|css|html|toml|sql|rs|swift))(?![A-Za-z0-9_.\/-])/g;
130
+ function mentionedPaths(text: string): string[] {
131
+ return [...text.matchAll(PATH_TOKEN)].map((match) => match[1]!).filter((path) => !path.split('/').includes('..'));
132
+ }
133
+
134
+ /** Shadow check at checklist authorship. Callers supply the origin/main file probe and merged-PR file reader. */
135
+ export function inspectChecklistTargetPaths(input: {
136
+ title: string; evidence?: string; refs?: readonly string[]; dispatchKind?: ChecklistDispatchKind;
137
+ }, deps: { existsAtMain: (path: string) => boolean; mergedPrFiles?: (number: number) => readonly string[] }): {
138
+ targets: string[]; warnings: string[]; candidates: string[];
139
+ } {
140
+ const lines = input.title.split(/\r?\n/);
141
+ const first = /^대상 경로:\s*(.*)$/.exec(lines[0] ?? '');
142
+ const targets = first ? [...new Set(mentionedPaths(first[1]!))] : [];
143
+ const newFileDeclared = /^신설:\s*/.test(lines[1] ?? '');
144
+ const warnings: string[] = [];
145
+ if ((input.dispatchKind ?? 'code') === 'code') {
146
+ if (!first || !first[1]!.trim() || targets.length === 0) warnings.push('첫 줄 대상 경로가 없다');
147
+ for (const path of targets) if (!deps.existsAtMain(path) && !newFileDeclared) warnings.push(`${path}: origin/main에 없는 파일 · 신설: 둘째 줄 선언 필요`);
148
+ }
149
+ if (!warnings.length) return { targets, warnings, candidates: [] };
150
+ const sources = [input.title, input.evidence ?? '', ...(input.refs ?? [])].join('\n');
151
+ const candidates = [...new Set(mentionedPaths(sources).filter(deps.existsAtMain))];
152
+ for (const match of sources.matchAll(/(?:^|[^\d])#(\d{2,8})\b/g)) {
153
+ for (const path of deps.mergedPrFiles?.(Number(match[1])) ?? []) {
154
+ if (deps.existsAtMain(path) && !candidates.includes(path)) candidates.push(path);
155
+ }
156
+ }
157
+ return { targets, warnings, candidates };
158
+ }
159
+
125
160
  /** `--ref` 문서를 찾는 뿌리들 — 이 코드의 트리 ⊕ 지금 작업 디렉토리의 git 뿌리.
126
161
  * ⭐ 설치본(`~/.local/share/elanous/current`)에는 `docs/` 가 없다 — 저장소 안에서 부른 설치본도 저장소 문서를 인용할 수 있게. */
127
162
  export function refRoots(cwd = process.cwd()): string[] {
@@ -233,10 +268,11 @@ export function validateCeoLoad(input: { ceoMinutes?: number; ceoDate?: string }
233
268
  if (input.ceoDate !== undefined && (!/^\d{4}-\d{2}-\d{2}$/.test(input.ceoDate) || !Number.isFinite(Date.parse(`${input.ceoDate}T00:00:00Z`)) || new Date(`${input.ceoDate}T00:00:00Z`).toISOString().slice(0, 10) !== input.ceoDate)) throw new CliUserError('대표 손 날짜는 YYYY-MM-DD 이어야 한다');
234
269
  }
235
270
 
236
- export function addItem(v: string, input: { id: string; title: string; refs?: string[]; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }, options: { allowDuplicateId?: boolean } = {}): Checklist {
271
+ export function addItem(v: string, input: { id: string; title: string; refs?: string[]; owner?: string; kind?: ChecklistKind; dispatchKind?: ChecklistDispatchKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }, options: { allowDuplicateId?: boolean } = {}): Checklist {
237
272
  if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
238
273
  if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
239
274
  if (input.kind !== undefined && !CHECKLIST_KINDS.includes(input.kind)) throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen|postpub');
275
+ if (input.dispatchKind !== undefined && !CHECKLIST_DISPATCH_KINDS.includes(input.dispatchKind)) throw new CliUserError(`잘못된 발사 종류: ${input.dispatchKind}`, 'code|ops|measure|decision|umbrella');
240
276
  if (input.owner !== undefined) parseOwner(input.owner);
241
277
  if (input.priority !== undefined && !['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
242
278
  if (input.predecessors !== undefined && (!Array.isArray(input.predecessors) || input.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === input.id))) throw new CliUserError('잘못된 선행 칸');
@@ -255,20 +291,21 @@ export function addItem(v: string, input: { id: string; title: string; refs?: st
255
291
  assertPredecessorsExist(input.predecessors, data, otherItems);
256
292
  const by = process.env.ELANOUS_TRACK || 'cli';
257
293
  const at = new Date().toISOString();
258
- const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator === true ? { accelerator: true as const } : {}), ...(refs ? { refs } : {}), updatedAt: at, updatedBy: by };
294
+ const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.dispatchKind !== undefined ? { dispatchKind: input.dispatchKind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator === true ? { accelerator: true as const } : {}), ...(refs ? { refs } : {}), updatedAt: at, updatedBy: by };
259
295
  data.items.push(item);
260
296
  change(data, item.id, 'add', null, item, by, at);
261
297
  return true;
262
298
  });
263
299
  }
264
300
 
265
- export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; refs?: string[]; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean | null }, by: string): Checklist {
301
+ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; refs?: string[]; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; dispatchKind?: ChecklistDispatchKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean | null }, by: string): Checklist {
266
302
  return mutate(v, (data, otherItems) => {
267
303
  const item = data.items.find((i) => i.id === id);
268
304
  if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
269
305
  if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new CliUserError(`잘못된 상태: ${patch.status}`);
270
306
  if (patch.disposition !== undefined && !['move', 'known-issue', 'block'].includes(patch.disposition)) throw new CliUserError(`잘못된 처분: ${patch.disposition}`);
271
307
  if (patch.kind !== undefined && !CHECKLIST_KINDS.includes(patch.kind)) throw new CliUserError(`잘못된 종류: ${patch.kind}`, 'screen|postpub');
308
+ if (patch.dispatchKind !== undefined && !CHECKLIST_DISPATCH_KINDS.includes(patch.dispatchKind)) throw new CliUserError(`잘못된 발사 종류: ${patch.dispatchKind}`, 'code|ops|measure|decision|umbrella');
272
309
  if (patch.priority !== undefined && !['P0', 'P1', 'P2'].includes(patch.priority)) throw new CliUserError(`잘못된 우선순위: ${patch.priority}`);
273
310
  if (patch.predecessors !== undefined && (!Array.isArray(patch.predecessors) || patch.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === id))) throw new CliUserError('잘못된 선행 칸');
274
311
  assertPredecessorsExist(patch.predecessors, data, otherItems);
@@ -283,7 +320,7 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
283
320
  throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
284
321
  }
285
322
  }
286
- const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field] ?? null));
323
+ const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'dispatchKind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field] ?? null));
287
324
  if (fields.length === 0) return false;
288
325
  const at = new Date().toISOString();
289
326
  for (const field of fields) {