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
@@ -456,11 +456,36 @@ function parseGoals(req: SelfImplementRuntimeRequest): string[] | undefined {
456
456
  return goals;
457
457
  }
458
458
 
459
- function formatOrchestrationResults(results: readonly SelfDevJobResult[]): string {
459
+ interface SelfImplementGoalResult {
460
+ goal: string;
461
+ status: SelfDevJobResult['status'];
462
+ runId?: string;
463
+ stage?: string;
464
+ reason?: string;
465
+ parentPrNumber?: number;
466
+ }
467
+
468
+ function observedGoalResults(results: readonly SelfDevJobResult[]): SelfImplementGoalResult[] {
469
+ return results.map((result, index) => ({
470
+ goal: result.feature || `골 ${index + 1}`,
471
+ status: result.status,
472
+ ...(result.runId ? { runId: result.runId } : {}),
473
+ ...(result.stage ? { stage: result.stage } : {}),
474
+ ...(result.error?.message || result.stopReason || result.blockReason
475
+ ? { reason: result.error?.message || result.stopReason || (result.blockReason === 'parent-unlanded'
476
+ ? result.parentPrNumber ? `선행 PR #${result.parentPrNumber} 미착지` : '선행 골 미착지'
477
+ : result.blockReason) }
478
+ : {}),
479
+ ...(result.parentPrNumber ? { parentPrNumber: result.parentPrNumber } : {}),
480
+ }));
481
+ }
482
+
483
+ function formatOrchestrationResults(results: readonly SelfDevJobResult[], goals: readonly SelfImplementGoalResult[]): string {
460
484
  const done = results.filter(result => result.status === 'done').length;
461
485
  const merged = results.filter(result => result.merged).length;
462
486
  const failed = results.filter(result => result.status === 'failed').length;
463
- return `[self-implement] parallel ${results.length} goal · done ${done}${merged ? ` (병합 ${merged})` : ''}${failed ? ` · 실패 ${failed}` : ''}`;
487
+ const summary = `[self-implement] parallel ${results.length} goal · done ${done}${merged ? ` (병합 ${merged})` : ''}${failed ? ` · 실패 ${failed}` : ''}`;
488
+ return [summary, ...goals.map(goal => `- ${goal.goal}: ${goal.status} · 런 ${goal.runId ?? '미관측'}${goal.stage ? ` · 단계 ${goal.stage}` : ''}${goal.reason ? ` · 이유 ${goal.reason}` : ''}`)].join('\n');
464
489
  }
465
490
 
466
491
  export function resolveDocumentReferences(paths: readonly string[], repositoryRoot = process.cwd()): DocumentReferenceStatus[] {
@@ -476,7 +501,15 @@ function documentReferenceStatusLines(references: readonly DocumentReferenceStat
476
501
 
477
502
  function formatResult(feature: string, r: SelfImplementResult, references: readonly DocumentReferenceStatus[] = []): string {
478
503
  const head = `[self-implement] ${r.stage}`;
479
- const lines: string[] = [head, ...documentReferenceStatusLines(references)];
504
+ const next = r.runId ? `eln self run ${r.runId}` : r.ok ? 'eln self running-runs' : '실패 이유를 확인한 뒤 다시 요청';
505
+ const lines: string[] = [
506
+ r.ok
507
+ ? r.runId
508
+ ? `하니스가 맡았습니다 · 런 ${r.runId} · 노드 ${r.node || '미관측'} · 다음 조회: ${next} · 이 턴에서 파일을 직접 편집하지 마세요`
509
+ : `하니스 응답을 받았지만 런 식별자는 아직 미관측입니다 · 진행 중 런 조회: ${next} · 일치하는 런이 없으면 요청을 다시 보내세요 · 이 턴에서 파일을 직접 편집하지 마세요`
510
+ : `하니스가 발사되지 않았거나 실패했습니다: ${r.detail ?? r.stage} · 다음: ${next} · 이 턴에서 파일을 직접 편집하지 마세요`,
511
+ head, ...documentReferenceStatusLines(references),
512
+ ];
480
513
  if (r.branch) lines.push(`branch: ${r.branch}`);
481
514
  if (r.worktreePath) lines.push(`worktree: ${r.worktreePath}`);
482
515
  if (r.stage === 'pr-opened' && r.prUrl) {
@@ -494,6 +527,12 @@ function formatResult(feature: string, r: SelfImplementResult, references: reado
494
527
  export interface SelfImplementRunResult {
495
528
  output: string;
496
529
  ok: boolean;
530
+ kind: 'launched' | 'observed-only' | 'failed' | 'pending';
531
+ next?: string;
532
+ reason?: string;
533
+ missing?: string[];
534
+ /** 병렬 골마다 실제로 관측된 종료 상태·식별자·이유. */
535
+ goalResults?: SelfImplementGoalResult[];
497
536
  // ⛔ 관측 전용 반환에는 **단계가 없다** — 아무 노드도 돌지 않았다. 없는 것을 있는 값으로 적으면
498
537
  // «안 돌았다» 와 «어느 단계에서 끝났다» 가 같은 값이 된다(부재와 미지를 같은 값으로 적지 않는다).
499
538
  stage?: SelfImplementResult['stage'];
@@ -506,8 +545,10 @@ export interface SelfImplementRunResult {
506
545
  observed?: true;
507
546
  /** 명시적 Fabric 분해의 상태·기존 3결말·미산출 사유. */
508
547
  fabricDecomposition?: FabricDecompositionOutcome;
509
- /** 즉시 반환 모드에서 할당한 런 식별자. 블로킹 기본 경로에는 없어 기존 반환 모양을 보존한다. */
548
+ /** 실제 런 결과에서 확인한 식별자. 선할당된 비동기 ID는 반환하지 않는다. */
510
549
  runId?: string;
550
+ /** 비동기 요청 조회용 상관 ID. 발사된 런의 증거가 아니다. */
551
+ requestId?: string;
511
552
  /** 호출이 블로킹인지 비블로킹인지. 기본 경로에는 없어 기존 반환 모양을 보존한다. */
512
553
  invocationMode?: 'blocking' | 'non-blocking';
513
554
  }
@@ -516,6 +557,7 @@ export const selfImplementRuntime: ToolRuntime<SelfImplementRuntimeRequest, Self
516
557
  id: 'self_implement',
517
558
  spec: buildSelfImplementSpec(),
518
559
  async run(req, ctx: ToolRuntimeContext): Promise<SelfImplementRunResult> {
560
+ try {
519
561
  const hasFeature = req.feature !== undefined;
520
562
  const hasGoals = req.goals !== undefined;
521
563
  if (hasFeature === hasGoals) {
@@ -604,7 +646,7 @@ export const selfImplementRuntime: ToolRuntime<SelfImplementRuntimeRequest, Self
604
646
  if (observeOnly.enabled) {
605
647
  const requestSummary = feature ?? goals!.join(' · ');
606
648
  debug.log('self-implement', 'runtime.observed', { surface: ctx.surface, ...(feature ? { feature: feature.slice(0, 120) } : { goals: goals!.length }), observeOnly: observeOnly.enabled, observeOnlySource: observeOnly.source });
607
- return { output: `SelfImplement 관측 전용 측정 모드 — 호출이 접수되어 기록되었습니다. 이 세션에서는 구현을 진행하지 않습니다. 같은 요청을 이 턴에서 직접 구현하지 말고 응답을 마무리하세요: ${requestSummary.slice(0, 120)}${documentReferenceStatusLines(documentReferences).length ? `\n${documentReferenceStatusLines(documentReferences).join('\n')}` : ''}`, ok: true, observed: true };
649
+ return { output: `관측 전용이라 런을 발사하지 않았습니다 · 코드도 안 바꿨습니다 · 이 결과를 구현 실패로 읽고 직접 Write/Edit 하지 마세요. 실제 발사를 원하면 관측 전용 모드를 끈 뒤 다시 요청하세요: ${requestSummary.slice(0, 120)}${documentReferenceStatusLines(documentReferences).length ? `\n${documentReferenceStatusLines(documentReferences).join('\n')}` : ''}`, ok: true, kind: 'observed-only', observed: true, missing: ['runId', 'node'], next: '관측 전용 모드를 끈 뒤 다시 요청' };
608
650
  }
609
651
 
610
652
  const allocatedRunId = nonBlocking ? mintRunId() : undefined;
@@ -637,9 +679,14 @@ export const selfImplementRuntime: ToolRuntime<SelfImplementRuntimeRequest, Self
637
679
  fabricDecomposition = fabricDecompositionOutcome(fabricResult);
638
680
  observeFabricDecomposition(fabricDecomposition, ctx.surface, fabricResult.status === 'decomposed' ? fabricResult.goals.length : undefined);
639
681
  if (fabricResult.status !== 'decomposed') {
682
+ const observedOnly = fabricDecomposition.outcome !== 'llm-failed';
640
683
  return {
641
- output: formatFabricDecompositionOutcome(fabricDecomposition),
642
- ok: fabricDecomposition.outcome !== 'llm-failed',
684
+ output: `${formatFabricDecompositionOutcome(fabricDecomposition)}${observedOnly ? '\n관측 전용이라 런을 발사하지 않았습니다 · 코드도 안 바꿨습니다 · 이 결과를 구현 실패로 읽고 직접 Write/Edit 하지 마세요.' : '\n이 턴에서 파일을 직접 편집하지 마세요.'}`,
685
+ ok: observedOnly,
686
+ kind: observedOnly ? 'observed-only' : 'failed',
687
+ next: observedOnly ? '분해 결과를 확인하고 구현 골을 다시 요청' : '분해 실패 이유를 확인한 뒤 다시 요청',
688
+ missing: ['runId', 'node'],
689
+ ...(observedOnly ? { observed: true as const } : { reason: formatFabricDecompositionOutcome(fabricDecomposition) }),
643
690
  fabricDecomposition,
644
691
  };
645
692
  }
@@ -674,10 +721,40 @@ export const selfImplementRuntime: ToolRuntime<SelfImplementRuntimeRequest, Self
674
721
  ...(forwardedSupervise === undefined ? {} : { supervise: forwardedSupervise }),
675
722
  ...(deliverable === undefined ? {} : { deliverable }),
676
723
  });
677
- const failed = results.filter(result => result.status === 'failed').length;
724
+ const goalResults = observedGoalResults(results);
725
+ const failedGoals = goalResults.filter(result => result.status === 'failed' || result.status === 'cancelled');
726
+ const blockedGoals = goalResults.filter(result => result.status === 'blocked');
727
+ const allRunsIdentified = goalResults.length > 0 && goalResults.every(result => result.runId);
728
+ const kind = goalResults.length === 0 || failedGoals.length > 0 || blockedGoals.length > 0 ? 'failed' : allRunsIdentified ? 'launched' : 'pending';
729
+ const parentBlockedPrNumbers = [...new Set(results.flatMap(result => result.status === 'blocked' && result.blockReason === 'parent-unlanded' && result.parentPrNumber ? [result.parentPrNumber] : []))];
730
+ const parentBlockedWithoutPr = results.some(result => result.status === 'blocked' && result.blockReason === 'parent-unlanded' && !result.parentPrNumber && !result.runId);
731
+ const otherFailureWithoutId = results.some(result => !result.runId && ['failed', 'cancelled', 'blocked'].includes(result.status) && result.blockReason !== 'parent-unlanded');
732
+ const runQueries = [...new Set(goalResults.flatMap(result => result.runId ? [`eln self run ${result.runId}`] : []))];
733
+ const availableQueries = [
734
+ ...parentBlockedPrNumbers.map(pr => `eln gh pr view ${pr}`),
735
+ ...runQueries,
736
+ ];
737
+ const pendingWithoutId = goalResults.some(result => !result.runId && !['failed', 'cancelled', 'blocked'].includes(result.status));
738
+ const next = [
739
+ ...availableQueries,
740
+ ...(pendingWithoutId ? ['eln self running-runs', '일치하는 런이 없으면 ID 미확정 골을 다시 요청'] : []),
741
+ ...(parentBlockedPrNumbers.length ? ['선행 PR 착지 뒤 차단된 골을 재실행하세요'] : []),
742
+ ...(parentBlockedWithoutPr ? ['선행 골 상태를 확인한 뒤 해당 골을 재실행하세요'] : []),
743
+ ...(otherFailureWithoutId ? ['실패·차단 이유를 확인한 뒤 해당 골을 다시 요청'] : []),
744
+ ].join('\n') || '골 결과가 0건이라 상태를 확인한 뒤 다시 요청';
745
+ const reason = goalResults.length === 0
746
+ ? '골 결과가 0건이라 상태 미관측'
747
+ : [...failedGoals, ...blockedGoals].map(result => `${result.goal}: ${result.reason ?? '이유 미관측'}`).join('; ');
678
748
  return {
679
- output: formatOrchestrationResults(results),
680
- ok: failed === 0,
749
+ output: formatOrchestrationResults(results, goalResults)
750
+ + (kind === 'failed' ? `\n실패/차단: ${reason} · 다음: ${next}` : `\n다음 조회: ${next}`)
751
+ + '\n이 턴에서 파일을 직접 편집하지 마세요',
752
+ ok: kind !== 'failed',
753
+ kind,
754
+ next,
755
+ missing: [ ...(goalResults.length === 0 || goalResults.some(result => !result.runId) ? ['runId'] : []), 'node' ],
756
+ goalResults,
757
+ ...(kind === 'failed' ? { reason } : {}),
681
758
  ...(fabricDecomposition ? { fabricDecomposition } : {}),
682
759
  };
683
760
  }
@@ -751,12 +828,12 @@ export const selfImplementRuntime: ToolRuntime<SelfImplementRuntimeRequest, Self
751
828
  ...(runId ? { runId } : {}),
752
829
  }, { pipelineDeps });
753
830
  if (!outcome.ok) {
754
- return { output: withGoalAuthorFailure(`[self-implement] failed: ${outcome.message}`), ok: false };
831
+ return { output: withGoalAuthorFailure(`하니스가 발사되지 않았거나 실패했습니다: ${outcome.message} · 다음: 실패 이유를 확인한 뒤 다시 요청하세요 · 이 턴에서 파일을 직접 편집하지 마세요`), ok: false, kind: 'failed', reason: outcome.message, next: '실패 이유를 확인한 뒤 다시 요청', missing: ['runId', 'node'] };
755
832
  }
756
833
  if (outcome.kind !== 'self') {
757
834
  return {
758
- output: withGoalAuthorFailure('[self-implement] observed by central CLI'),
759
- ok: true,
835
+ output: withGoalAuthorFailure('관측 전용이라 런을 발사하지 않았습니다 · 코드도 안 바꿨습니다 · 이 결과를 구현 실패로 읽고 직접 Write/Edit 하지 마세요'),
836
+ ok: true, kind: 'observed-only', observed: true, missing: ['runId', 'node'], next: '관측 전용 모드를 끈 뒤 다시 요청',
760
837
  };
761
838
  }
762
839
 
@@ -764,6 +841,11 @@ export const selfImplementRuntime: ToolRuntime<SelfImplementRuntimeRequest, Self
764
841
  return {
765
842
  output: withGoalAuthorFailure(formatResult(feature, result, documentReferences)),
766
843
  ok: result.ok,
844
+ kind: result.ok ? (result.runId ? 'launched' : 'pending') : 'failed',
845
+ next: result.runId ? `eln self run ${result.runId}` : result.ok ? 'eln self running-runs' : '실패 이유를 확인한 뒤 다시 요청',
846
+ missing: [...(result.runId ? [] : ['runId']), ...(result.node ? [] : ['node'])],
847
+ ...(!result.ok ? { reason: result.detail ?? result.stage } : {}),
848
+ runId: result.runId,
767
849
  stage: result.stage,
768
850
  node: result.node,
769
851
  ...(result.prUrl ? { prUrl: result.prUrl } : {}),
@@ -775,28 +857,51 @@ export const selfImplementRuntime: ToolRuntime<SelfImplementRuntimeRequest, Self
775
857
 
776
858
  if (nonBlocking) {
777
859
  const runId = allocatedRunId ?? mintRunId();
860
+ const requestId = runId;
861
+ const lookup = `eln logs --category self-implement --event runtime.non-blocking-completed,runtime.non-blocking-failed --grep ${requestId} --since 3h --all --include-test`;
778
862
  void drive(runId).then(
779
863
  (result) => {
780
- debug.log('self-implement', 'runtime.non-blocking-completed', {
781
- runId,
864
+ debug.log('self-implement', result.kind === 'failed' ? 'runtime.non-blocking-failed' : 'runtime.non-blocking-completed', {
865
+ requestId,
866
+ kind: result.kind,
867
+ ...(result.runId ? { runId: result.runId } : {}),
868
+ ...(result.reason ? { reason: result.reason } : {}),
869
+ next: result.next ?? '요청 결과를 확인한 뒤 다시 요청',
782
870
  ok: result.ok,
783
871
  ...(result.stage === undefined ? {} : { stage: result.stage }),
784
- });
872
+ }, result.kind === 'failed' ? { level: 'warn' } : undefined);
785
873
  },
786
874
  (error) => {
875
+ const reason = error instanceof Error ? error.message : String(error);
787
876
  debug.log('self-implement', 'runtime.non-blocking-failed', {
788
- runId,
789
- error: error instanceof Error ? error.message : String(error),
877
+ requestId,
878
+ kind: 'failed',
879
+ reason,
880
+ next: '요청과 실패 이유를 확인한 뒤 다시 요청',
790
881
  }, { level: 'warn' });
791
882
  },
792
883
  );
793
884
  return {
794
885
  ok: true,
795
- runId,
886
+ kind: 'pending',
796
887
  invocationMode: 'non-blocking',
797
- output: `[self-implement] non-blocking accepted\nrunId: ${runId}`,
888
+ requestId,
889
+ next: lookup,
890
+ missing: ['runId', 'node'],
891
+ output: `하니스 요청을 접수했습니다 · 런과 노드는 아직 미관측 · 요청 ID ${requestId}(발사 증거 아님) · 다음 조회: ${lookup} · 이 턴에서 파일을 직접 편집하지 마세요\n[self-implement] non-blocking pending`,
892
+ };
893
+ }
894
+ return await drive();
895
+ } catch (error) {
896
+ const reason = error instanceof Error ? error.message : String(error);
897
+ return {
898
+ ok: false,
899
+ kind: 'failed',
900
+ reason,
901
+ next: '요청과 실패 이유를 확인한 뒤 다시 요청',
902
+ missing: ['runId', 'node'],
903
+ output: `SelfImplement 실패: ${reason} · 다음: 요청과 실패 이유를 확인한 뒤 다시 요청 · 이 턴에서 파일을 직접 편집하지 마세요`,
798
904
  };
799
905
  }
800
- return drive();
801
906
  },
802
907
  };
@@ -142,10 +142,20 @@ function migrateLegacySessions(): void {
142
142
  }
143
143
 
144
144
  function stateRoot(): string {
145
- const base = process.env.XDG_STATE_HOME && process.env.XDG_STATE_HOME.trim()
146
- ? process.env.XDG_STATE_HOME
147
- : join(homedir(), '.local', 'state');
148
- return join(base, 'elanous');
145
+ const universe = process.env.ELANOUS_STATE_DIR?.trim();
146
+ const root = universe
147
+ ? join(universe, 'elanous')
148
+ : join(
149
+ process.env.XDG_STATE_HOME && process.env.XDG_STATE_HOME.trim()
150
+ ? process.env.XDG_STATE_HOME
151
+ : join(homedir(), '.local', 'state'),
152
+ 'elanous',
153
+ );
154
+ try {
155
+ const { debug } = require('../debug/log.js') as typeof import('../debug/log.js');
156
+ debug.log('session.active', 'pointer-root', { root, universe });
157
+ } catch { /* 관측 실패가 포인터 경로 결정을 막지 않는다 */ }
158
+ return root;
149
159
  }
150
160
 
151
161
  function indexPath(root: string = sessionRoot()): string { return join(root, 'index.json'); }
@@ -4,6 +4,7 @@ import { getUserConfig } from '../user-config.js';
4
4
  import { resolve } from 'node:path';
5
5
  import type { ModelFamily } from '../models/prompts.js';
6
6
  import type { InputSourceKind, InputSourceRef } from '../input/input-source-kind.js';
7
+ import type { ControlTurnIntent, SubmitTurnIntent } from '../input/input-intent.js';
7
8
  import { getSessionCwd } from '../session/working-dir.js';
8
9
  import {
9
10
  armQuickControlOnce,
@@ -22,6 +23,7 @@ import {
22
23
  import type { LLMMessage, LLMToolSpec } from '../llm.js';
23
24
  import type { LLMToolDef } from '../plugins/core/types.js';
24
25
  import type { ToolRunResult, ToolRuntime } from '../tool-runtime/types.js';
26
+ import { blockWriteToolSearchHydration, type ToolSearchRunResult } from '../tool-runtime/tool-search-runtime.js';
25
27
  import { findNativeTool, isNativeToolModelExposed } from '../native-tool-catalog.js';
26
28
  import { hasAstGrep } from '../skills/tools/ast-grep.js';
27
29
  import { buildOmniSearchTool, omniSearchAvailable } from '../skills/tools/omni-search.js';
@@ -1731,6 +1733,8 @@ const ESSENTIAL_NATIVE_RULE_IDS = [
1731
1733
  'agent', 'agent-output', 'agent-reply', 'agent-stop', 'agent-list', 'self-implement', 'self-orchestrate', 'dev-harness',
1732
1734
  ] as const;
1733
1735
  const ESSENTIAL_HOST_FAMILY_IDS = ['self-ops', 'release-read'] as const;
1736
+ // Keep this TUI essential addition out of the shared control/agent self-ops family.
1737
+ const ESSENTIAL_HOST_EXTRA_NAMES = ['logs_query'] as const;
1734
1738
  const ESSENTIAL_RUNTIME_FAMILY_IDS = ['bash', 'run-shell', 'pty-shell', 'run-tests', 'elanous_skills_list', 'skill_exec', 'code-grounding'] as const;
1735
1739
  const ESSENTIAL_OPTIONAL_TOOL_NAMES = [
1736
1740
  'GetDashboardState',
@@ -1764,7 +1768,11 @@ function buildEssentialSessionRuntimeToolSpecs(opts: {
1764
1768
  .filter((tool): tool is LLMToolDef => !!tool)
1765
1769
  .map(toSpec)
1766
1770
  : [];
1767
- });
1771
+ }).concat(ESSENTIAL_HOST_EXTRA_NAMES
1772
+ .filter(name => isToolAvailable(name, opts.toolAvailability))
1773
+ .map(name => hostByName.get(name))
1774
+ .filter((tool): tool is LLMToolDef => !!tool)
1775
+ .map(toSpec));
1768
1776
  const runtimeById = new Map(opts.runtimeTools.map(tool => [tool.id, tool] as const));
1769
1777
  const runtimeSpecs = ESSENTIAL_RUNTIME_FAMILY_IDS.flatMap(id => {
1770
1778
  const rule = RUNTIME_TOOL_FAMILY_RULE_MAP.get(id);
@@ -1833,12 +1841,23 @@ export function buildSessionRuntimeToolSpecs(opts: {
1833
1841
  // toolset (incident 2026-05-04 — Opus replied "no file tools in this
1834
1842
  // session" while Bash was actually exposed at position 7+).
1835
1843
  const finalSpecs: LLMToolSpec[] = opts.rich === false
1836
- ? buildEssentialSessionRuntimeToolSpecs({
1837
- hostTools: opts.hostTools,
1838
- runtimeTools: opts.runtimeTools ?? [],
1839
- optionalTools: opts.optionalTools ?? [],
1840
- toolAvailability: opts.toolAvailability,
1841
- })
1844
+ ? (() => {
1845
+ const essentialSpecs = buildEssentialSessionRuntimeToolSpecs({
1846
+ hostTools: opts.hostTools,
1847
+ runtimeTools: opts.runtimeTools ?? [],
1848
+ optionalTools: opts.optionalTools ?? [],
1849
+ toolAvailability: opts.toolAvailability,
1850
+ });
1851
+ if (!hasDevHarnessIntent(opts.userText)) return essentialSpecs;
1852
+ const droppedNames = new Set(['Edit', 'Write']);
1853
+ const filteredSpecs = essentialSpecs.filter(spec => !droppedNames.has(spec.name));
1854
+ if (filteredSpecs.length !== essentialSpecs.length) {
1855
+ debug.log('session-runtime.tools', 'harness-phrase-write-tools-dropped', {
1856
+ dropped: ['Edit', 'Write'],
1857
+ });
1858
+ }
1859
+ return filteredSpecs;
1860
+ })()
1842
1861
  : [
1843
1862
  ...dynamicNativeTools,
1844
1863
  ...hostSpecs,
@@ -2084,6 +2103,10 @@ export async function dispatchSessionRuntimeTool(
2084
2103
  args: Record<string, unknown>,
2085
2104
  deps: SessionRuntimeDispatchDeps,
2086
2105
  ): Promise<unknown> {
2106
+ const finishRuntimeResult = (result: unknown): unknown =>
2107
+ name === 'ToolSearch' && isDevHarnessIntent(deps.userText ?? '')
2108
+ ? blockWriteToolSearchHydration(result as ToolSearchRunResult)
2109
+ : result;
2087
2110
  // Fix W-bis (2026-04-25): codex family always enables scoped-analysis
2088
2111
  // mode regardless of userText vocabulary. Rationale (see docs/RESEARCH-
2089
2112
  // codex-reread-pathology-2refs-2026-04-25.md): codex's search-heavy
@@ -2178,8 +2201,9 @@ export async function dispatchSessionRuntimeTool(
2178
2201
  const nativeRuntime = nativeRule?.preferRuntime ? deps.getToolRuntime(name) : undefined;
2179
2202
  if (nativeRuntime) {
2180
2203
  const result = await deps.dispatchToolRuntime(name, normalizedArgs, deps.toolCallId);
2181
- rememberCandidateListingResult(name, normalizedArgs, result, scopedAnalysis, deps.searchPlannerState);
2182
- return result;
2204
+ const safeResult = finishRuntimeResult(result);
2205
+ rememberCandidateListingResult(name, normalizedArgs, safeResult, scopedAnalysis, deps.searchPlannerState);
2206
+ return safeResult;
2183
2207
  }
2184
2208
  if (nativeRule?.dispatch) {
2185
2209
  let result: unknown;
@@ -2213,8 +2237,9 @@ export async function dispatchSessionRuntimeTool(
2213
2237
  const isPtyTool = name.startsWith('PtyShell') || name.startsWith('pty_shell');
2214
2238
  if (!isPtyTool || deps.ptyDashboardOn) {
2215
2239
  const result = await deps.dispatchToolRuntime(name, normalizedArgs, deps.toolCallId);
2216
- rememberCandidateListingResult(name, normalizedArgs, result, scopedAnalysis, deps.searchPlannerState);
2217
- return result;
2240
+ const safeResult = finishRuntimeResult(result);
2241
+ rememberCandidateListingResult(name, normalizedArgs, safeResult, scopedAnalysis, deps.searchPlannerState);
2242
+ return safeResult;
2218
2243
  }
2219
2244
  }
2220
2245
 
@@ -2699,6 +2724,25 @@ function hasDevHarnessIntent(text: string): boolean {
2699
2724
  return hasAny(text, DEV_HARNESS_INTENT_TERMS);
2700
2725
  }
2701
2726
 
2727
+ export function isDevHarnessIntent(text: string): boolean {
2728
+ return hasDevHarnessIntent(text);
2729
+ }
2730
+
2731
+ /** The dashboard calls this immediately after submitIntent resolution. A
2732
+ * fixed plain turn becomes the same control turn used by `/harness ask`, so
2733
+ * the model dispatch branch is never reached for that turn. */
2734
+ export function routeDevHarnessSubmitIntent(
2735
+ intent: SubmitTurnIntent | ControlTurnIntent,
2736
+ ): SubmitTurnIntent | ControlTurnIntent {
2737
+ if (intent.kind !== 'submit-turn' || intent.route !== 'plain' || !isDevHarnessIntent(intent.text)) return intent;
2738
+ return {
2739
+ kind: 'control-turn',
2740
+ source: intent.source,
2741
+ command: 'slash-command',
2742
+ commandText: `/harness ask ${intent.text}`,
2743
+ };
2744
+ }
2745
+
2702
2746
  export function shouldExposeDevHarnessSessionTool(text: string): boolean {
2703
2747
  return isDevHarnessModelSurfaceEnabled() && hasDevHarnessIntent(text);
2704
2748
  }
@@ -32,7 +32,10 @@ export interface TaskAgentCoverInput {
32
32
  /** null means the pending-landing age source could not be read; [] means no pending landings. */
33
33
  pendingLandingAgeSeconds?: readonly number[] | null;
34
34
  /** A source reached the query safety limit; its aggregate is not known to be complete. */
35
- truncated?: { taskAgentActions: boolean; allLands: boolean };
35
+ truncated?: { taskAgentActions: boolean; allLands: boolean; goalAsks?: boolean; goalRuns?: boolean };
36
+ /** Completed authoring provenance and terminal goal records. undefined = not wired; null = unreadable. */
37
+ goalAsks?: readonly { authorRunId: string; goalId: string }[] | null;
38
+ goalRuns?: readonly { goalId: string; outcome: string; stage?: string }[] | null;
36
39
  }
37
40
 
38
41
  export type LandingFailureReason = 'load' | 'oom' | 'time-limit' | 'conflict' | 'review' | 'other';
@@ -161,22 +164,22 @@ export function formatTaskAgentCover(row: CoverRow): string {
161
164
  /** 7동사의 이름 — 리뷰·착지·수리·검증·저작·구현·정리 (decision·deliver 는 7동사 밖이라 세지 않는다). */
162
165
  export type SevenVerbName = '리뷰' | '착지' | '수리' | '검증' | '저작' | '구현' | '정리';
163
166
 
164
- /** no-source 는 출처 배선이 아직 없는 동사 · unreadable 은 TASK-AGENT 행동 원장을 못 읽은 것이다. */
167
+ /** no-source 는 출처 배선이 없는 동사 · unreadable 은 해당 동사의 원천을 완전히 못 읽은 것이다. */
165
168
  export type SevenVerbState = 'live' | 'shadow-only' | 'none' | 'no-source' | 'unreadable';
166
169
 
167
170
  /** 7동사 출처 표의 한 칸 — source 는 그 동사를 관측하는 TASK-AGENT action kind, null 이면 출처가 아직 없다. */
168
171
  export interface SevenVerbSource {
169
172
  verb: SevenVerbName;
170
- source: Exclude<NextActionKind, 'decision' | 'deliver'> | null;
173
+ source: Exclude<NextActionKind, 'decision' | 'deliver'> | 'goal-ask+goal-run' | null;
171
174
  }
172
175
 
173
- /** TA-THEME-VERBS-LIVE — 7동사와 출처 표(코드로 고정). 저작·구현·정리의 출처 배선은 다음 조각. */
176
+ /** TA-THEME-VERBS-LIVE — 저작은 완료 골 조인; 구현·정리는 아직 출처가 없다. */
174
177
  export const SEVEN_VERB_SOURCES: readonly SevenVerbSource[] = [
175
178
  { verb: '리뷰', source: 'review' },
176
179
  { verb: '착지', source: 'land' },
177
180
  { verb: '수리', source: 'retry' },
178
181
  { verb: '검증', source: 'green' },
179
- { verb: '저작', source: null },
182
+ { verb: '저작', source: 'goal-ask+goal-run' },
180
183
  { verb: '구현', source: null },
181
184
  { verb: '정리', source: null },
182
185
  ];
@@ -197,12 +200,21 @@ export interface SevenVerbLive {
197
200
  measuredVerbs: number;
198
201
  }
199
202
 
200
- /** live = 창 안에 result done 인 non-shadow 사건 ≥1. 원장이 null 이거나 절단되었으면 전부 unreadable. */
203
+ /** 행동 동사는 창 안의 done, 저작은 완료한 저작 원장과 착지 골의 조인. 각 출처의 null·절단은 해당 동사만 unreadable. */
201
204
  export function measureSevenVerbLive(input: TaskAgentCoverInput): SevenVerbLive {
202
205
  const unreadable = input.taskAgentActions === null || input.truncated?.taskAgentActions === true;
203
206
  const verbs = SEVEN_VERB_SOURCES.map(({ verb, source }): SevenVerbLiveRow => {
204
- if (unreadable) return { verb, source, liveDone: null, shadow: null, state: 'unreadable' };
205
207
  if (source === null) return { verb, source: null, liveDone: null, shadow: null, state: 'no-source' };
208
+ if (source !== 'goal-ask+goal-run' && unreadable) return { verb, source, liveDone: null, shadow: null, state: 'unreadable' };
209
+ if (source === 'goal-ask+goal-run') {
210
+ if (input.goalAsks == null || input.goalRuns == null || input.truncated?.goalAsks || input.truncated?.goalRuns) {
211
+ return { verb, source, liveDone: null, shadow: null, state: 'unreadable' };
212
+ }
213
+ const landed = new Set(input.goalRuns.filter((run) =>
214
+ run.stage === 'merged' && (run.outcome === 'completed' || run.outcome === 'merged')).map((run) => run.goalId));
215
+ const liveDone = new Set(input.goalAsks.filter((ask) => ask.authorRunId && landed.has(ask.goalId)).map((ask) => ask.goalId)).size;
216
+ return { verb, source, liveDone, shadow: 0, state: liveDone > 0 ? 'live' : 'none' };
217
+ }
206
218
  const actions = input.taskAgentActions!.filter((action) => action.kind === source);
207
219
  const liveDone = actions.filter((action) => action.result === 'done').length;
208
220
  const shadow = actions.filter((action) => action.result === 'shadow').length;
@@ -210,7 +222,7 @@ export function measureSevenVerbLive(input: TaskAgentCoverInput): SevenVerbLive
210
222
  });
211
223
  return {
212
224
  verbs,
213
- liveVerbs: unreadable ? null : verbs.filter((row) => row.state === 'live').length,
225
+ liveVerbs: verbs.some((row) => row.state === 'unreadable' && row.source !== null) ? null : verbs.filter((row) => row.state === 'live').length,
214
226
  measuredVerbs: SEVEN_VERB_SOURCES.filter((row) => row.source !== null).length,
215
227
  };
216
228
  }
@@ -218,7 +230,14 @@ export function measureSevenVerbLive(input: TaskAgentCoverInput): SevenVerbLive
218
230
  /** 한 줄 요약 — 못 읽었으면 n 자리를 `-` 로 적는다(0 으로 읽히지 않게). */
219
231
  export function formatSevenVerbLive(measurement: SevenVerbLive): string {
220
232
  const total = SEVEN_VERB_SOURCES.length;
221
- if (measurement.liveVerbs === null) return `${total}동사 live -/${total} · 못 잼(행동 원장 null·절단)`;
233
+ if (measurement.liveVerbs === null) {
234
+ const actionUnreadable = measurement.verbs.some((row) => row.source === 'review' && row.state === 'unreadable');
235
+ const authorUnreadable = measurement.verbs.some((row) => row.source === 'goal-ask+goal-run' && row.state === 'unreadable');
236
+ const reason = actionUnreadable && authorUnreadable ? '행동·저작·골 원장 null·절단'
237
+ : actionUnreadable ? '행동 원장 null·절단' : '저작·골 원장 null·절단';
238
+ const noSource = measurement.verbs.filter((row) => row.state === 'no-source').map((row) => row.verb).join('·');
239
+ return `${total}동사 live -/${total} · 못 잼(${reason})${noSource ? ` · 출처 없음 ${noSource}` : ''}`;
240
+ }
222
241
  const noSource = measurement.verbs.filter((row) => row.state === 'no-source').map((row) => row.verb).join('·');
223
242
  return `${total}동사 live ${measurement.liveVerbs}/${total}${noSource ? ` · 출처 없음 ${noSource}` : ''}`;
224
243
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * - `review`: 수확 가능 런의 PR 에 elanous 리뷰를 «한 번» 요청한다 — PR 머리(sha)마다 한 번(카드 `reviewRequests` 이력).
5
5
  * 요청만 한다: 결과로 착지·재발사로 잇지 않는다(실행부 `executeNextAction` 의 review→land 사슬을 타지 않는다).
6
- * TA-REVIEW-RESULT-CAPTURE — 자식 stdout(`--json`)은 결과 파일로 받고, 회수(`review-result-capture.ts`)는 «다음» 판단 틱이 한다 —
6
+ * TA-REVIEW-RESULT-CAPTURE — 자식 stdout(`--json`)은 결과 파일로 받고, 자식 완료 뒤 해당 카드만 회수·재판정한다.
7
7
  * 회수한 pass 는 그 틱의 판단부·land 관문 입력(`selfReview`)이 될 뿐 land 판정 로직은 그대로다.
8
8
  * - `propose-green`: 카드에 green «제안»(`greenProposal`)만 적는다 — 체크리스트는 손대지 않는다.
9
9
  * - `propose-land`: 카드·현재 머리의 self review 통과(pass «또는» warn ⊕ must-fix 0 · TA-LAND-WARN-MUSTFIX0)·non-draft 를 확인하고 기존 `pr land` 를 머리 고정으로 한 번 부른다.
@@ -25,7 +25,7 @@ import type { DetachedSpawn } from '../cli/tasks-cli.js';
25
25
  import { LIVE_MOVE_HISTORY_EVENT, taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
26
26
  import { LIVE_RETRY_MOVES } from './judge.js';
27
27
  import { decisionCardGrounds, decisionRaiseArgs, type DecisionCardEvidence, type NextAction } from './actions.js';
28
- import { reviewResultPath } from './review-result-capture.js';
28
+ import { reviewResultPath, TA_REVIEW_COMPLETION_CARD_ENV, TA_REVIEW_COMPLETION_RESULT_ENV, TA_REVIEW_COMPLETION_STATE_ENV } from './review-result-capture.js';
29
29
  import { buildPriorFixContext, PRIOR_FIX_SCOPE_LINE, priorFixBlock, priorFixObservation, type PriorDiffReader, type PriorFixContext } from './review-prior-fix.js';
30
30
  import { createLandWorktree, localHead, observeLandWorktree, type LandWorktree, type LandWorktreeFailure, type LandWorktreeInput, type LandWorktreeRemoval } from './land-worktree.js';
31
31
 
@@ -208,13 +208,18 @@ export function liveReviewArgs(entry: string, configDir: string, pr: number, int
208
208
  * 파이프가 아니라 파일 — 부모가 먼저 끝나도 자식이 EPIPE 로 죽지 않는다). 없으면 종전과 같다(stdio ignore).
209
209
  * `opts` 는 시험 몫(spawn·진입점·우주) — 기본은 실물.
210
210
  */
211
- export async function defaultRequestReview(pr: number, intent: string, cwd?: string, opts: { spawn?: DetachedSpawn; entry?: string; configDir?: string; resultPath?: string } = {}): Promise<void> {
211
+ export async function defaultRequestReview(pr: number, intent: string, cwd?: string, opts: { spawn?: DetachedSpawn; entry?: string; configDir?: string; resultPath?: string; cardId?: string; statePath?: string } = {}): Promise<void> {
212
212
  const { spawnDetachedConfirmed, ELANOUS_CLI_ENTRY } = await import('../cli/tasks-cli.js');
213
213
  const { getElanousConfigDirOverride } = await import('../elanous-config-dir.js');
214
214
  const { effectiveInstanceRoot } = await import('../instance/resolve.js');
215
215
  const configDir = opts.configDir ?? getElanousConfigDirOverride() ?? effectiveInstanceRoot();
216
216
  const base = opts.spawn ?? (spawn as unknown as DetachedSpawn);
217
217
  const args = liveReviewArgs(opts.entry ?? ELANOUS_CLI_ENTRY, configDir, pr, intent);
218
+ const completionEnv = opts.resultPath && opts.cardId && opts.statePath ? {
219
+ [TA_REVIEW_COMPLETION_CARD_ENV]: opts.cardId,
220
+ [TA_REVIEW_COMPLETION_STATE_ENV]: opts.statePath,
221
+ [TA_REVIEW_COMPLETION_RESULT_ENV]: opts.resultPath,
222
+ } : undefined;
218
223
  if (!opts.resultPath) { await spawnDetachedConfirmed(base, process.execPath, args, undefined, cwd); return; }
219
224
  let fd: number;
220
225
  try {
@@ -229,7 +234,7 @@ export async function defaultRequestReview(pr: number, intent: string, cwd?: str
229
234
  const toFile: DetachedSpawn = (command, spawnArgs, options) =>
230
235
  (base as unknown as (c: string, a: string[], o: unknown) => ReturnType<DetachedSpawn>)(command, spawnArgs, { ...options, stdio: ['ignore', fd, 'ignore'] });
231
236
  try {
232
- await spawnDetachedConfirmed(toFile, process.execPath, args, undefined, cwd);
237
+ await spawnDetachedConfirmed(toFile, process.execPath, args, undefined, cwd, completionEnv);
233
238
  } finally {
234
239
  try { closeSync(fd); } catch { /* the child keeps its own copy */ }
235
240
  }
@@ -360,7 +365,7 @@ export async function executeLiveReview(
360
365
  try { (deps.log ?? ((c, e, d) => debug.log(c, e, d)))('task-agent', 'review-prior-fix-filled', priorFixObservation(card.id, pr, prior)); } catch { /* fail-soft */ }
361
366
  }
362
367
  const intent = liveReviewIntent(card, ctx.runId, ctx.stopReason, prior);
363
- await (deps.requestReview ? deps.requestReview(pr, intent, cwd, { resultPath }) : defaultRequestReview(pr, intent, cwd, { resultPath }));
368
+ await (deps.requestReview ? deps.requestReview(pr, intent, cwd, { resultPath }) : defaultRequestReview(pr, intent, cwd, { resultPath, cardId: card.id, statePath: path }));
364
369
  } catch (error) {
365
370
  const reason = error instanceof Error ? error.message : String(error);
366
371
  updateTaskCard(path, card.id, (current) => current?.reviewRequests?.some((entry) => entry.pr === pr && entry.head === head!.head && entry.at === at)
@@ -560,11 +565,14 @@ export async function executeLiveLand(
560
565
  const path = deps.statePath ?? taskAgentStatePath();
561
566
  const at = (deps.now ?? (() => new Date()))().toISOString();
562
567
  const local = (deps.landWorktree?.localHead ?? localHead)(cwd);
568
+ const landKey = `land:${card.id}:${view.head.slice(0, 12)}`;
563
569
  const needsLandWorktree = !!local && local.toLowerCase() !== view.head.toLowerCase();
564
570
  const createCap = landWorktreeCreateCap();
565
571
  let claimed = false;
566
572
  let missing = false;
567
573
  let cappedReason: string | undefined;
574
+ // This state-file lock is the cross-process card/head land claim. Both completion workers
575
+ // must pass it; an unfinished or successful attempt makes the second worker skip.
568
576
  // TA-LAND-RETRY-ON-INFRA — 같은 머리의 앞 시도들(잠금 안에서 읽은 «지금» 카드) · 재시도면 그 근거.
569
577
  let prior: LandAttempt[] = [];
570
578
  try {
@@ -590,7 +598,7 @@ export async function executeLiveLand(
590
598
  if (!claimed) {
591
599
  const last = prior.at(-1);
592
600
  const why = last ? ` (attempts ${prior.length} · last ${last.ok === false ? `class ${last.class ?? 'unclassified'}${last.reason ? ` ${last.reason}` : ''}` : last.ok === true ? 'ok' : 'unfinished'})` : '';
593
- return observe(deps, withRepo({ kind, card: card.id, ok: true, executed: false, detail: `already attempted land for #${pr} head ${view.head.slice(0, 12)}${why}` }));
601
+ return observe(deps, withRepo({ kind, card: card.id, ok: true, executed: false, detail: `already attempted land for #${pr} head ${view.head.slice(0, 12)} · ${landKey} claimed — skip${why}` }));
594
602
  }
595
603
  const attempt = prior.length + 1;
596
604
  if (attempt > 1) {