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
@@ -75,6 +75,66 @@ export function _setCreateGoalAuthorDecomposeStepsForTesting(fn?: CreateGoalAuth
75
75
  /** 진행 종료 스테이지(phase='end') — 그 외는 delta(start 제외). */
76
76
  const END_STAGES: readonly SelfImplementProgressStage[] = ['pr-opened', 'aborted', 'gate-failed', 'pr-declined'];
77
77
 
78
+ // ⭐ 결과 네 꼴(HARNESS-RESULT-CONTRACT · RFC-tui-tool-surface-on-graph-harness §4 · 0.2.26) —
79
+ // 종전 `{observed:true}` 한 비트는 모델이 «안 됐다 → 직접 편집»으로 읽었다(UX S1-02 · 47 도구).
80
+ // ⇒ launched / pending / observed-only / failed 네 꼴로 구조화해 «다음 행동»을 문면으로 고정한다.
81
+ // ⚠️ launched 꼴은 SelfImplementResult 필드를 **그대로 보존**한다 — runId 조인·원장 소비자 무손상.
82
+ export type SelfImplementLaunchedToolResult = SelfImplementResult & { runId: string; kind: 'launched'; message: string; next: string; missing: string[] };
83
+ export type SelfImplementPendingToolResult = Partial<SelfImplementResult> & { kind: 'pending'; message: string; next: string; missing: string[] };
84
+ export type SelfImplementObservedOnlyToolResult = { kind: 'observed-only'; observed: true; message: string; next: string; missing: ['runId', 'node'] };
85
+ export type SelfImplementFailedToolResult = Partial<SelfImplementResult> & { kind: 'failed'; reason: string; message: string; next: string; missing: string[] };
86
+ export type SelfImplementToolResult = SelfImplementLaunchedToolResult | SelfImplementPendingToolResult | SelfImplementObservedOnlyToolResult | SelfImplementFailedToolResult;
87
+
88
+ /** 하니스가 런을 맡았다 — 파일 편집은 하니스 몫이므로 모델의 직접 Write/Edit 을 문면으로 막는다. */
89
+ export function buildLaunchedToolResult(result: SelfImplementResult): SelfImplementLaunchedToolResult | SelfImplementPendingToolResult {
90
+ if (!result.runId) return buildPendingToolResult(result);
91
+ const runId = result.runId;
92
+ const node = result.node || null;
93
+ const missing = node ? [] : ['node'];
94
+ const query = `eln self run ${runId}`;
95
+ return {
96
+ ...result,
97
+ runId,
98
+ kind: 'launched',
99
+ next: query,
100
+ missing,
101
+ message: `하니스가 맡았습니다 · 런 ${runId} · 노드 ${node ?? '미관측'} · 다음 조회: ${query} · 이 턴에서 파일을 직접 편집하지 마세요`,
102
+ };
103
+ }
104
+
105
+ /** 완료 또는 접수는 관측했지만 실제 런 ID가 없어 발사를 단언할 수 없다. */
106
+ export function buildPendingToolResult(result: SelfImplementResult): SelfImplementPendingToolResult {
107
+ const { runId: _unconfirmedRunId, ...observed } = result;
108
+ return {
109
+ ...observed,
110
+ kind: 'pending',
111
+ next: 'eln self running-runs',
112
+ missing: ['runId', ...(result.node ? [] : ['node'])],
113
+ message: '하니스 응답을 받았지만 런 식별자는 아직 미관측입니다 · 진행 중 런 조회: eln self running-runs · 일치하는 런이 없으면 요청을 다시 보내세요 · 이 턴에서 파일을 직접 편집하지 마세요',
114
+ };
115
+ }
116
+
117
+ /** 관측 전용 — 런이 발사되지 «않았다»는 사실과 «구현 실패가 아니다»를 함께 말한다. */
118
+ export function buildObservedOnlyToolResult(): SelfImplementObservedOnlyToolResult {
119
+ return {
120
+ kind: 'observed-only',
121
+ observed: true,
122
+ missing: ['runId', 'node'],
123
+ next: '실제 발사를 원하면 관측 전용 모드를 끈 뒤 다시 요청하세요',
124
+ message: '관측 전용이라 런을 발사하지 않았습니다 · 코드도 안 바꿨습니다 · 이 결과를 구현 실패로 읽고 직접 Write/Edit 하지 마세요 · 실제 발사를 원하면 관측 전용 모드를 끈 뒤 다시 요청하세요',
125
+ };
126
+ }
127
+
128
+ /** 거부·실패 — 이유와 다음 행동을 구조화해 모델이 재시도·포기·인계를 판단하게 한다. */
129
+ export function buildFailedToolResult(reason: string, result?: SelfImplementResult): SelfImplementFailedToolResult {
130
+ const next = result?.runId ? `eln self run ${result.runId}` : '요청과 실패 이유를 확인한 뒤 다시 요청하세요';
131
+ return {
132
+ ...(result ?? {}), kind: 'failed', reason, next,
133
+ missing: [...(result?.runId ? [] : ['runId']), ...(result?.node ? [] : ['node'])],
134
+ message: `하니스가 발사되지 않았거나 실패했습니다: ${reason} · 다음: ${next} · 이 턴에서 파일을 직접 편집하지 마세요`,
135
+ };
136
+ }
137
+
78
138
  /**
79
139
  * ★ base seam 에 막(SurfaceUx) 배선을 얹는다(P0·테스트 가능). approvePr=ux.confirm(fail-closed)·
80
140
  * onProgress=ux.progress. 코어(runSelfImplement)·다른 seam(worktree/implement/gate/openPr)은 무손상.
@@ -180,9 +240,10 @@ export async function dispatchSelfImplement(
180
240
  authorGoal: SelfImplementGoalAuthor = writeAuthoredGoal,
181
241
  runCli: RunSelfImplementCliFn = runSelfImplementCliCommand,
182
242
  seamsFactory?: SelfImplementSeamsFactory,
183
- ): Promise<unknown> {
243
+ ): Promise<SelfImplementToolResult> {
184
244
  const feature = typeof args.feature === 'string' ? args.feature.trim() : '';
185
- if (!feature) return { error: 'SelfImplement: `feature` is required' };
245
+ if (!feature) return buildFailedToolResult('SelfImplement: `feature` is required');
246
+ try {
186
247
  const base = typeof args.base === 'string' && args.base.trim() ? args.base.trim() : undefined;
187
248
  const draft = typeof args.draft === 'boolean' ? args.draft : true;
188
249
  const entry = resolveDispatchEntry(ctx);
@@ -216,7 +277,7 @@ export async function dispatchSelfImplement(
216
277
  debug.log('daemon-tools.self-implement', 'runtime.observe-only-decision', { surface: ux.surface, observeOnly: observeOnly.enabled, observeOnlySource: observeOnly.source });
217
278
  if (observeOnly.enabled) {
218
279
  debug.log('daemon-tools.self-implement', 'runtime.observed', { surface: ux.surface, observeOnly: observeOnly.enabled, observeOnlySource: observeOnly.source });
219
- return { observed: true };
280
+ return buildObservedOnlyToolResult();
220
281
  }
221
282
 
222
283
  let goalFile: string | undefined;
@@ -298,5 +359,12 @@ export async function dispatchSelfImplement(
298
359
  // ⭐ runId 를 반드시 싣는다 — 관측이 전부 runId 로 키잉되므로(`self run <runId>`·`logs --grep`)
299
360
  // 이게 없으면 **이 경계에서 조인이 끊긴다**(N3 재측정 2026-07-28: 계약은 섰는데 로그가 못 따라갔다).
300
361
  debug.log('daemon-tools.self-implement', 'done', { runId: result.runId, stage: result.stage, ok: result.ok, pr: result.prUrl ?? null });
301
- return result;
362
+ return !result.ok
363
+ ? buildFailedToolResult(result.detail ?? result.stage, result)
364
+ : result.runId ? buildLaunchedToolResult(result) : buildPendingToolResult(result);
365
+ } catch (error) {
366
+ const reason = error instanceof Error ? error.message : String(error);
367
+ debug.log('daemon-tools.self-implement', 'failed', { reason, stage: 'dispatch' });
368
+ return buildFailedToolResult(reason);
369
+ }
302
370
  }
@@ -24,7 +24,11 @@ export interface LlmPricingUsd {
24
24
  longContext?: { thresholdTokens: number; inputPerM: number; outputPerM: number; cacheReadPerM?: number; cacheWritePerM?: number };
25
25
  }
26
26
 
27
- /** AWS Bedrock Global Standard prices, USD per million tokens (AWS pricing API, ap-northeast-2). */
27
+ /** AWS Bedrock Global Standard prices, USD per million tokens (AWS pricing API, ap-northeast-2).
28
+ * ⛔ BEDROCK-COST-LEDGER(0.2.26) — mantle-openai 비-Anthropic Bedrock id(`openai.gpt-6-sol`·`zai.glm-5` 등
29
+ * `src/llm/bedrock.ts` BEDROCK_MODELS 의 `route:'mantle-openai'` 행)는 여기에 행이 «없다»: 단가 수치 근거가
30
+ * 하나도 확보되지 않았다. 근거 없는 id는 `isBedrockPricingModel` 게이트(`claude-` 시작)를 지나지 않아
31
+ * `unknownCost()` 로 `kind:'unknown'` 에 머문다 — 0원으로 꾸미지 않는다. */
28
32
  export const BEDROCK_PRICING_USD: Readonly<Record<string, LlmPricingUsd>> = {
29
33
  'claude-haiku-5-5': { inputPerM: 0.1, outputPerM: 0.5, cacheWritePerM: 0.125, cacheReadPerM: 0.01 },
30
34
  'claude-sonnet-5-5': { inputPerM: 2, outputPerM: 10, cacheWritePerM: 2.5, cacheReadPerM: 0.1 },
@@ -31,6 +31,35 @@ function reasonFor(error: unknown, token?: string): string {
31
31
  return token ? message.replaceAll(token, '[redacted]') : message;
32
32
  }
33
33
 
34
+ /** Doctor's read-only live source. One malformed Nexus machine row invalidates the whole
35
+ * reading: doctor reports unmeasured rather than treating a partial registry as complete. */
36
+ export async function listMachineResources(options: { primaryUrl?: string; queryToken?: string; port?: string; root?: string; fetch?: (input: URL | RequestInfo, init?: RequestInit) => Promise<Response> } = {}): Promise<ResourceView[]> {
37
+ const portOverride = options.port ?? (process.env.ELANOUS_CONTROL_PORT?.trim() || undefined);
38
+ const primary = await resolvePrimary({
39
+ role: 'query',
40
+ ...(options.root ? { root: options.root } : {}),
41
+ config: { url: options.primaryUrl, tokens: options.queryToken ? { query: options.queryToken } : undefined },
42
+ ...(portOverride !== undefined ? { port: Number(portOverride) } : {}),
43
+ });
44
+ const token = primary.token;
45
+ if (!token) throw new Error('control query token unavailable');
46
+ const url = new URL('/v1/resources', primary.url);
47
+ url.searchParams.set('kind', 'machine');
48
+ const response = await (options.fetch ?? globalThis.fetch)(url, {
49
+ headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(5000),
50
+ });
51
+ if (!response.ok) throw new Error(`control HTTP ${response.status}`);
52
+ const body: unknown = await response.json();
53
+ if (!body || typeof body !== 'object' || !('resources' in body) || !Array.isArray(body.resources) ||
54
+ body.resources.some((row: unknown) => !row || typeof row !== 'object' ||
55
+ (row as ResourceView).kind !== 'machine' || typeof (row as ResourceView).machine !== 'string' ||
56
+ typeof (row as ResourceView).name !== 'string' || typeof (row as ResourceView).expired !== 'boolean' ||
57
+ !((row as ResourceView).attrs && typeof (row as ResourceView).attrs === 'object'))) {
58
+ throw new Error('invalid machine resource response');
59
+ }
60
+ return body.resources as ResourceView[];
61
+ }
62
+
34
63
  export async function runResourcesQuery(options: { where?: string; kind?: string; port?: string; primaryUrl?: string; queryToken?: string; json?: boolean }): Promise<number> {
35
64
  let base = '';
36
65
  let token: string | undefined;
@@ -27,7 +27,7 @@ import {
27
27
  type ReadinessReport,
28
28
  type SubstrateSignals,
29
29
  } from './doctor-readiness.js';
30
- import { debug } from '../debug/log.js';
30
+ import { debug, redactSecretText } from '../debug/log.js';
31
31
  import { resolveDaemonEndpoint, type ResolveDaemonEndpointOpts } from '../nexus/daemon-endpoint.js';
32
32
  import { applyDoctorFixes, applyGitInstall, applyServiceRestart, applySudoFixes, planDoctorFixes, type DoctorFixDeps, type DoctorFixItem } from './doctor-fix.js';
33
33
  import { ADVISABLE_DOCTOR_FIX_IDS, adviseDoctorFixes, type DoctorAdvice } from './doctor-llm-assist.js';
@@ -40,7 +40,10 @@ import type { LlmInventory } from '../llm/local-manager/types.js';
40
40
  import { buildDoctorBundle } from './doctor-bundle.js';
41
41
  import { uploadDoctorBundle } from './doctor-bundle-upload.js';
42
42
  import { checkNodeModules, formatNodeModulesCheck, nodeModulesTrees } from './doctor-node-modules.js';
43
- import { formatHostDoctorLines, runHostDoctor, type HostDoctorExpected, type HostDoctorRemote } from '../fleet/host-doctor.js';
43
+ import { formatHostDoctorLines, runHostDoctor, type HostDoctorExpected, type HostDoctorOptions, type HostDoctorRemote } from '../fleet/host-doctor.js';
44
+ import { listMachineResources } from './control-cli.js';
45
+ import type { ResourceView } from '../control-plane/ledger.js';
46
+ import { loadMachineLedger } from '../machines/machine-ledger.js';
44
47
 
45
48
  function defaultCheckPythonEnv(): { status: 'ok' | 'fixable' | 'manual'; evidence: string; remedy?: string } {
46
49
  const c = checkPythonEnv(false);
@@ -203,9 +206,14 @@ export interface DoctorCliDeps extends DoctorOptions, Pick<DoctorFixDeps, 'home'
203
206
  hostDoctorRemote?: HostDoctorRemote;
204
207
  /** `doctor host` 가 대조할 HQ 실측값(설치 판 · 이미지 핀) — 없으면 해당 점검은 unmeasured. */
205
208
  hostDoctorExpected?: HostDoctorExpected;
209
+ /** HQ membership readings; injected tests need no live Nexus. */
210
+ hostDoctorMembership?: () => Promise<NonNullable<HostDoctorOptions['membership']>>;
211
+ /** Nexus machine query seam; tests can exercise the default assembly without contacting a live service. */
212
+ hostDoctorMachineResources?: () => Promise<ResourceView[]>;
206
213
  }
207
214
 
208
215
  const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
216
+ const observationError = (error: unknown): string => redactSecretText(error instanceof Error ? error.message : String(error)).slice(0, 200);
209
217
  const tavilyNames = new Set(['TAVILY_API_KEY', 'TAVILY_KEY']);
210
218
 
211
219
  function defaultCodeRevision(): string | undefined {
@@ -1211,6 +1219,52 @@ function formatDoctorAdvice(advice: DoctorAdvice): string {
1211
1219
  ].join('\n');
1212
1220
  }
1213
1221
 
1222
+ function formatDoctorAdviceProposal(advice: DoctorAdvice, report: DoctorReport): { text: string; itemId: string; observedValue: string } | null {
1223
+ const proposal = advice.ok ? advice.order[0] : undefined;
1224
+ if (!proposal) return null;
1225
+ const observation = report.readiness?.items.find((item) => item.id === proposal.readinessId);
1226
+ if (!observation) return null;
1227
+ const observedValue = `${observation.status}: ${observation.evidence}`;
1228
+ return {
1229
+ itemId: proposal.readinessId,
1230
+ observedValue,
1231
+ text: [
1232
+ `LLM 제안: ${proposal.why}`,
1233
+ `운영 팩 항목 id: ${proposal.readinessId}`,
1234
+ `현재 관측값: ${observedValue}`,
1235
+ ].join('\n'),
1236
+ };
1237
+ }
1238
+
1239
+ async function recordDoctorAdviceResponse(
1240
+ proposal: { itemId: string; observedValue: string },
1241
+ accepted: boolean,
1242
+ ): Promise<void> {
1243
+ debug.log('doctor.advise', accepted ? 'accepted' : 'rejected', {
1244
+ itemId: proposal.itemId,
1245
+ observedValue: proposal.observedValue,
1246
+ applied: 0,
1247
+ });
1248
+ }
1249
+
1250
+ async function presentDoctorAdviceProposal(
1251
+ advice: DoctorAdvice,
1252
+ report: DoctorReport,
1253
+ deps: DoctorCliDeps,
1254
+ json: boolean,
1255
+ announce: (text: string) => void,
1256
+ ): Promise<{ proposal: { itemId: string; observedValue: string }; response: boolean | null; text: string } | null> {
1257
+ const formatted = formatDoctorAdviceProposal(advice, report);
1258
+ if (!formatted) return null;
1259
+ const proposal = { itemId: formatted.itemId, observedValue: formatted.observedValue };
1260
+ const interactive = deps.adviceInteractive ?? (process.stdin.isTTY === true && (json ? process.stderr.isTTY === true : process.stdout.isTTY === true));
1261
+ if (!interactive) return { proposal, response: null, text: formatted.text };
1262
+ announce(formatted.text);
1263
+ const accepted = await (deps.confirmAdvice ?? ((message) => confirmDoctorAdvice(message, json)))('이 제안을 수락할까요? [y/N] ');
1264
+ await recordDoctorAdviceResponse(proposal, accepted);
1265
+ return { proposal, response: accepted, text: `${formatted.text}\n응답: ${accepted ? '수락' : '거절'}` };
1266
+ }
1267
+
1214
1268
  async function confirmDoctorAdvice(message: string, json: boolean): Promise<boolean> {
1215
1269
  const prompt = createInterface({ input: process.stdin, output: json ? process.stderr : process.stdout });
1216
1270
  try { return /^(y|yes)$/i.test((await prompt.question(message)).trim()); }
@@ -1246,18 +1300,39 @@ export function registerDoctorCommand(program: Command, deps: DoctorCliDeps = {}
1246
1300
  if (checks.some((check) => check.status !== 'ok')) setExitCode(1);
1247
1301
  });
1248
1302
  doctor.command('host')
1249
- .description('Check whether a fleet host stands ready for its role (read-only · setup-status shape)')
1250
- .argument('<host>', "ssh name, or 'local' for this machine")
1303
+ .description('Check whether a fleet host stands ready (read-only; membership needs a repository checkout and a named remote worker)')
1304
+ .argument('<host>', "SSH name in fleet intent; an unknown name fails membership; 'local' checks readiness without membership")
1251
1305
  .option('--role <role>', 'host role: worker or hq', 'worker')
1252
1306
  .option('--json', 'structured output')
1253
- .action((host: string, opts: { role: string; json?: boolean }, command: Command) => {
1307
+ .action(async (host: string, opts: { role: string; json?: boolean }, command: Command) => {
1254
1308
  if (opts.role !== 'worker' && opts.role !== 'hq') {
1255
1309
  err.error('--role must be worker or hq');
1256
1310
  setExitCode(1);
1257
1311
  return;
1258
1312
  }
1259
1313
  const json = opts.json === true || command.parent?.opts().json === true;
1260
- const report = runHostDoctor({ host, role: opts.role, remote: deps.hostDoctorRemote, expected: deps.hostDoctorExpected });
1314
+ let membership: HostDoctorOptions['membership'];
1315
+ if (opts.role === 'worker' && host !== 'local') {
1316
+ if (deps.hostDoctorMembership) membership = await deps.hostDoctorMembership();
1317
+ else {
1318
+ let intent: NonNullable<HostDoctorOptions['membership']>['intent'] = null;
1319
+ let machines: NonNullable<HostDoctorOptions['membership']>['machines'] = null;
1320
+ let parentHosts: NonNullable<HostDoctorOptions['membership']>['parentHosts'] = null;
1321
+ let intentError: string | undefined;
1322
+ let machinesError: string | undefined;
1323
+ let parentHostsError: string | undefined;
1324
+ try { intent = loadMachineLedger(deps.repositoryRoot ?? repositoryRoot); } catch (error) { intentError = observationError(error); }
1325
+ try { machines = await (deps.hostDoctorMachineResources ?? listMachineResources)(); } catch (error) { machinesError = observationError(error); }
1326
+ try {
1327
+ // UserConfig permits one parent object or an array of parent objects, never a bare host string.
1328
+ const configured = (deps.userConfig ?? (deps.getUserConfig ?? getUserConfig)()).taskAgent?.parentHost;
1329
+ if (configured === null) parentHostsError = 'taskAgent.parentHost is null';
1330
+ else parentHosts = configured === undefined ? [] : Array.isArray(configured) ? configured : [configured];
1331
+ } catch (error) { parentHostsError = observationError(error); }
1332
+ membership = { intent, machines, parentHosts, intentError, machinesError, parentHostsError };
1333
+ }
1334
+ }
1335
+ const report = runHostDoctor({ host, role: opts.role, remote: deps.hostDoctorRemote, expected: deps.hostDoctorExpected, membership });
1261
1336
  out.log(json ? JSON.stringify(report, null, 2) : formatHostDoctorLines(report).join('\n'));
1262
1337
  if (!report.ok) setExitCode(1);
1263
1338
  });
@@ -1404,10 +1479,17 @@ export function registerDoctorCommand(program: Command, deps: DoctorCliDeps = {}
1404
1479
  // Planning never writes; existing repairs require --yes, while git alone can use affirmative TTY consent.
1405
1480
  const plan = planDoctorFixes(fixes);
1406
1481
  const advice = opts.advise ? await requestDoctorAdvice(report, plan, deps) : undefined;
1482
+ const proposal = advice && !opts.fix ? await presentDoctorAdviceProposal(advice, report, deps, opts.json === true, (text) => {
1483
+ if (opts.json) err.error(text);
1484
+ else out.log(text);
1485
+ }) : null;
1407
1486
  if (!opts.fix) {
1408
1487
  out.log(opts.json
1409
- ? JSON.stringify({ ...report, advise: advice }, null, 2)
1410
- : [formatDoctorReport(report, { credentials: opts.credentials }), formatDoctorAdvice(advice!)].join('\n'));
1488
+ ? JSON.stringify({ ...report, advise: advice, ...(proposal ? { adviceProposal: { itemId: proposal.proposal.itemId, observedValue: proposal.proposal.observedValue, response: proposal.response } } : {}) }, null, 2)
1489
+ : [
1490
+ formatDoctorReport(report, { credentials: opts.credentials }),
1491
+ proposal?.response === null ? proposal.text : proposal ? `응답: ${proposal.response ? '수락' : '거절'}` : formatDoctorAdvice(advice!),
1492
+ ].join('\n'));
1411
1493
  if (report.requiredMissing?.length) setExitCode(1);
1412
1494
  return;
1413
1495
  }
@@ -57,13 +57,17 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
57
57
  };
58
58
 
59
59
  msg.command('post [body]').description('Post a message; use - or omit body to read stdin')
60
- .option('--from <seat>').option('--to <seat>').option('--kind <kind>').option('--body <text>').option('--json')
61
- .action((body: string | undefined, opts: CommonOptions & { from?: string; to?: string; kind?: string; body?: string }) => run(async () => {
60
+ .option('--from <seat>').option('--to <seat>').option('--kind <kind>').option('--body <text>')
61
+ .option('--origin-key <key>', 'deduplicate a channel mirror by coord:<commentId>:<recipient>').option('--json')
62
+ .action((body: string | undefined, opts: CommonOptions & { from?: string; to?: string; kind?: string; body?: string; originKey?: string }) => run(async () => {
62
63
  const from = seat(opts.from, '--from');
63
64
  const to = seat(opts.to, '--to');
64
65
  if (body !== undefined && body !== '-' && opts.body !== undefined) throw new Error('provide body once (argument or --body)');
65
66
  const text = opts.body ?? (body !== undefined && body !== '-' ? body : await readStdin());
66
- const posted = withStore(store => store.post({ from, to, body: text, ...(opts.kind === undefined ? {} : { kind: opts.kind }) }));
67
+ const envelope = { from, to, body: text, ...(opts.kind === undefined ? {} : { kind: opts.kind }) };
68
+ const posted = withStore(store => opts.originKey === undefined
69
+ ? store.post(envelope)
70
+ : store.appendWithProvenance(envelope, opts.originKey));
67
71
  emit(posted, opts.json, () => out.log(formatMessage(posted)));
68
72
  })());
69
73
 
@@ -95,7 +99,11 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
95
99
  .option('--to <seat>').option('--json')
96
100
  .action((opts: CommonOptions & { to?: string }) => run(() => {
97
101
  const to = opts.to === undefined ? undefined : seat(opts.to, '--to');
98
- const rows = withStore(store => store.unread().filter(row => to === undefined || row.recipient === to));
102
+ const rows = withStore(store => {
103
+ if (to === undefined) return store.unread();
104
+ const count = store.unreadCount(to);
105
+ return count ? [{ recipient: to, count }] : [];
106
+ });
99
107
  emit(rows, opts.json, () => {
100
108
  if (!rows.length) out.log('No unread messages.');
101
109
  for (const row of rows) out.log(`${row.recipient}: ${row.count} unread`);
@@ -104,9 +112,11 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
104
112
 
105
113
  msg.command('watch').description('Poll for new messages without acknowledging them (Ctrl-C to stop)')
106
114
  .option('--to <seat>').option('--after <id>', 'exclusive message ID (default: acknowledged cursor)')
115
+ .option('--since-ack', 'start from the recipient seat’s acknowledged cursor')
107
116
  .option('--interval <ms>', 'poll interval in milliseconds', '1000')
108
117
  .option('--once', 'poll once and exit').option('--json')
109
- .action((opts: RecipientOptions & { interval?: string; once?: boolean }) => run(async () => {
118
+ .action((opts: RecipientOptions & { interval?: string; once?: boolean; sinceAck?: boolean }) => run(async () => {
119
+ if (opts.sinceAck && opts.after !== undefined) throw new Error('--since-ack cannot be used with --after');
110
120
  const to = seat(opts.to, '--to');
111
121
  const interval = integer(opts.interval, '--interval', 1000, 1, 60000);
112
122
  const after = opts.after === undefined ? undefined : integer(opts.after, '--after', 0, 0, Number.MAX_SAFE_INTEGER);
package/src/cli/pr-cli.ts CHANGED
@@ -1195,6 +1195,59 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1195
1195
  const cwd = opts.cwd ?? process.cwd();
1196
1196
  const run = deps.run ?? defaultCmdRunner;
1197
1197
  const pinned = opts.pr !== undefined || opts.expectedHead !== undefined;
1198
+ if (opts.dryRun && opts.pr !== undefined && opts.expectedHead === undefined) {
1199
+ if (opts.overlapEvidence !== undefined || opts.hold) {
1200
+ out.error('✗ dry-run: --overlap-evidence 와 --hold 는 번호 지정 읽기 전용 미리보기에서 사용할 수 없습니다.');
1201
+ return 1;
1202
+ }
1203
+ const pr = Number(opts.pr);
1204
+ if (!/^[1-9]\d*$/.test(opts.pr) || !Number.isSafeInteger(pr)) {
1205
+ out.error('✗ dry-run: --pr 에는 양의 정수를 지정해야 합니다.');
1206
+ return 1;
1207
+ }
1208
+ const viewed = run('gh', ['pr', 'view', String(pr), '--json', 'headRefOid,state,headRefName,isDraft,mergeable,mergeStateStatus,url'], { cwd });
1209
+ if (!viewed.ok) {
1210
+ out.error(`✗ dry-run: PR #${pr} 를 조회하지 못했습니다 — 존재하지 않거나 GitHub 조회가 실패했습니다.`);
1211
+ return 1;
1212
+ }
1213
+ let value: { headRefOid?: string; state?: string; headRefName?: string; isDraft?: boolean; mergeable?: string; mergeStateStatus?: string; url?: string };
1214
+ try {
1215
+ const parsed: unknown = JSON.parse(viewed.out);
1216
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('not an object');
1217
+ value = parsed as typeof value;
1218
+ }
1219
+ catch {
1220
+ out.error(`✗ dry-run: PR #${pr} 조회 결과를 읽을 수 없습니다.`);
1221
+ return 1;
1222
+ }
1223
+ if (typeof value.headRefOid !== 'string' || !/^[a-f0-9]{40}$/i.test(value.headRefOid) || value.state !== 'OPEN') {
1224
+ out.error(`✗ dry-run: PR #${pr} 가 열려 있지 않거나 head 정보가 없습니다 (state=${value.state ?? 'unmeasured'}).`);
1225
+ return 1;
1226
+ }
1227
+ out.log(`PR #${pr} · head ${value.headRefOid} · state ${value.state}${value.headRefName ? ` · branch ${value.headRefName}` : ''}`);
1228
+ out.log('Landing plan (read-only preview):');
1229
+ out.log(`- GitHub PR state/head: PASS (${value.state}, ${value.headRefOid})`);
1230
+ if (value.isDraft === true) out.log('- Draft status: GitHub reports DRAFT; landing eligibility UNMEASURED.');
1231
+ else if (value.isDraft === false) out.log('- Draft status: observed — ready for review.');
1232
+ else if (value.isDraft !== false) out.log('- Draft status: UNMEASURED');
1233
+ if (value.mergeable === 'CONFLICTING') out.log('- Warning: GitHub reports merge conflicts; landing eligibility UNMEASURED.');
1234
+ else if (value.mergeable === 'MERGEABLE') out.log('- Mergeability: GitHub reports MERGEABLE; repository landing gates remain UNMEASURED.');
1235
+ else out.log(`- Mergeability: UNMEASURED${value.mergeable ? ` (${value.mergeable})` : ''}`);
1236
+ if (value.mergeStateStatus === 'BLOCKED' || value.mergeStateStatus === 'DIRTY' || value.mergeStateStatus === 'DRAFT') {
1237
+ out.log(`- Warning: GitHub merge state is ${value.mergeStateStatus}; landing eligibility UNMEASURED.`);
1238
+ } else if (value.mergeStateStatus && value.mergeStateStatus !== 'CLEAN') {
1239
+ out.log(`- GitHub merge state: ${value.mergeStateStatus} (additional requirements may apply; landing eligibility UNMEASURED).`);
1240
+ }
1241
+ out.log('- Local HEAD/worktree, branch/base, and same-branch guard: UNMEASURED — local git is not run');
1242
+ out.log('- Changed-file scope, active-run files, untracked files, plan artifacts, and deletion advisory: UNMEASURED — local files are not read');
1243
+ out.log('- Typecheck, isolation, mock-module restore, model-hardcode, daemon-port, test-interference, and global checks: UNMEASURED — gates are not run');
1244
+ out.log('- Public export/import, public leak, and docs CLI checks: UNMEASURED — gates are not run');
1245
+ out.log('- Android, iOS, and PWA gates: CONDITIONAL / UNMEASURED — changed-file applicability and builds are not checked');
1246
+ out.log('- Freeze, PR base/bench guards, sibling PRs, recent landings, overlap, and approval: UNMEASURED — not queried or approved');
1247
+ out.log('- GitHub draft/mergeability status: observed above; this preview does not approve or merge the PR.');
1248
+ out.log('- Mutations (ready/edit/create/merge, git commit/push, worktree): NOT RUN');
1249
+ return 0;
1250
+ }
1198
1251
  if (opts.overlapEvidence !== undefined && (!pinned || !opts.overlapEvidence.trim())) {
1199
1252
  out.error('✗ overlap-evidence: --overlap-evidence <file> 는 --pr ⊕ --expected-head 머리 고정 착지에서만 쓴다.');
1200
1253
  return 1;
@@ -11,12 +11,12 @@
11
11
  * ⓐ `gate.baseCommit` 이 고정 머리의 조상 ⊕ 지금 끝의 조상이고 ⓑ 창 `gate.baseCommit..지금 끝`의 커밋이 «하나도»
12
12
  * PR 의 바뀐 파일(머리 vs merge-base)을 안 건드리면 통과(관측 reason `base-advanced-disjoint`).
13
13
  * 겹치면 `base-stale-overlap`(REJUDGE 가 재게이트를 청한다) · 못 재면 `base-window-unmeasured` · ⓐ 가 아니면 종전 사유.
14
- * ③ 리뷰 — 증거가 가리키는 `review-results/<card>-<pr>-<head12>.json`(TA 가 받는 `self review --json` stdout)이
15
- * 같은 PR·같은 머리에 대해 `reviewed:true` · `verdict ∈ {pass, warn}` · must-fix 가 «잰 수» 0 인가
14
+ * ③ 리뷰 — 증거가 가리키는 `review-results/<card>-<pr>-<head접두 10~40>.json`(TA 가 받는 `self review --json` stdout)이
15
+ * 파일명에서 같은 PR·머리 접두이고 내용에서 전체 PR·머리가 정확히 같으며 `reviewed:true` · `verdict ∈ {pass, warn}` · must-fix 가 «잰 수» 0 인가
16
16
  * (자동 병합 canAuto · TA-LAND-WARN-MUSTFIX0 과 같은 규칙 — must-fix 를 못 셌으면 거부).
17
17
  * ⓐ 이미 조상(TC #26019 실측) — 겹친 최근 착지 커밋이 «전부» 고정 머리의 조상이면 그 겹침은 이미 그 머리의 게이트·리뷰에
18
18
  * 반영됐다 ⇒ ①② 없이 ③ 만으로 통과(관측 reason `already-ancestor`). 하나라도 조상이 아니면 ①②③ 전부.
19
- * ⓐ' 증거 파일이 없을 때(`verifyAncestorAutoPass`) — 같은 ⓐ 를 표준 자리 `review-results/*-<pr>-<head12>.json` 만으로 잰다
19
+ * ⓐ' 증거 파일이 없을 때(`verifyAncestorAutoPass`) — 같은 ⓐ 를 표준 자리 `review-results/*-<pr>-<head접두 10~40>.json` 만으로 잰다
20
20
  * (관측 reason `already-ancestor-auto`). 조상이 아닌 착지가 하나라도 있거나 리뷰 결과가 없으면 거부.
21
21
  * 하나라도 못 재거나 어긋나면 거부 — 호출부는 종전 거부를 그대로 낸다(fail-closed).
22
22
  * - 관측: `pr.land` · `overlap-evidence` (decision · reason · 잰 값).
@@ -33,7 +33,7 @@ interface OverlapEvidenceFile {
33
33
  head: string;
34
34
  /** ①② 경로에서만 필수 — 겹친 착지가 전부 조상(already-ancestor)이면 생략해도 된다. */
35
35
  gate?: { head: string; baseCommit: string; passed: boolean };
36
- /** `self review --json` stdout 을 받은 파일(TA: `<state>/review-results/<card>-<pr>-<head12>.json`). */
36
+ /** `self review --json` stdout 을 받은 파일(TA: `<state>/review-results/<card>-<pr>-<head접두 10~40>.json`); 내용의 전체 head도 확인한다. */
37
37
  reviewResult: string;
38
38
  }
39
39
 
@@ -65,9 +65,14 @@ const COMMIT_MARK = '\u0001';
65
65
 
66
66
  function checkReview(input: Pick<VerifyOverlapEvidenceInput, 'pr' | 'readFile'>, e: Partial<OverlapEvidenceFile>, head: string, baseTip?: string): { ok: false; reason: string; baseTip?: string } | { ok: true; reviewResult: string } {
67
67
  if (typeof e.reviewResult !== 'string' || !e.reviewResult) return { ok: false, reason: 'review-missing', baseTip };
68
- // TA 가 받는 자리(`<state>/review-results/<card>-<pr>-<head12>.json` · reviewResultPath)만 — 아무 디렉터리의 같은 이름은 안 받는다.
68
+ // TA 가 받는 자리(`<state>/review-results/<card>-<pr>-<head접두 10~40>.json` · reviewResultPath)만; 내용의 전체 head도 확인한다.
69
+ const resultName = basename(e.reviewResult);
70
+ const resultMatch = /^([A-Za-z0-9][A-Za-z0-9._-]*)-(\d+)-([a-f0-9]{10,40})\.json$/i.exec(resultName);
69
71
  if (basename(dirname(e.reviewResult)) !== 'review-results'
70
- || !new RegExp(`^[A-Za-z0-9][A-Za-z0-9._-]*-${input.pr}-${head.slice(0, 12)}\\.json$`, 'i').test(basename(e.reviewResult))) {
72
+ || !resultMatch
73
+ || Number(resultMatch[2]) !== input.pr
74
+ || resultMatch[3]!.length < 10
75
+ || !head.startsWith(resultMatch[3]!.toLowerCase())) {
71
76
  return { ok: false, reason: 'review-path-not-for-this-pr-head', baseTip };
72
77
  }
73
78
  let text: string;
@@ -206,7 +211,7 @@ export interface VerifyAncestorAutoInput {
206
211
  cwd: string;
207
212
  run: CmdRunner;
208
213
  readFile: (path: string) => string;
209
- /** 표준 자리 `<state>/review-results` — 이 안의 `*-<pr>-<head12>.json` 만 본다. */
214
+ /** 표준 자리 `<state>/review-results` — 이 안의 `*-<pr>-<head접두 10~40>.json`만 후보로 보고 내용의 전체 head를 확인한다. */
210
215
  reviewResultsDir: string;
211
216
  listDir: (dir: string) => readonly string[];
212
217
  /** 겹침 관문이 찾은 «겹친» 최근 착지 커밋들. */
@@ -222,10 +227,13 @@ function checkAncestorAuto(input: VerifyAncestorAutoInput): AncestorAutoVerdict
222
227
  if (notAncestor !== undefined) return { ok: false, reason: `ancestor-auto-not-ancestor: ${notAncestor.slice(0, 12)} is not an ancestor of ${head.slice(0, 12)}` };
223
228
  let names: readonly string[];
224
229
  try { names = input.listDir(input.reviewResultsDir); } catch { return { ok: false, reason: 'review-missing' }; }
225
- const suffix = new RegExp(`-${input.pr}-${head.slice(0, 12)}\\.json$`, 'i');
226
- // 같은 <pr>-<head12> 리뷰가 여럿이면 «전부» 통과해야 한다 — 하나라도 거부면 거부(fail-closed · 처음 통과한 하나로 승인하지 않는다).
230
+ const filename = new RegExp(`^[A-Za-z0-9][A-Za-z0-9._-]*-${input.pr}-([a-f0-9]{10,40})\\.json$`, 'i');
231
+ // 같은 PR·머리 접두의 리뷰가 여럿이면 «전부» 통과해야 한다 — 내용 headCommit 이 정확히 맞아야 승인한다.
227
232
  let accepted: string | undefined;
228
- for (const name of [...names].filter((n) => suffix.test(n)).sort()) {
233
+ for (const name of [...names].filter((n) => {
234
+ const match = filename.exec(n);
235
+ return Boolean(match && match[1]!.length >= 10 && head.startsWith(match[1]!.toLowerCase()));
236
+ }).sort()) {
229
237
  const review = checkReview(input, { reviewResult: join(input.reviewResultsDir, name) }, head);
230
238
  if (!review.ok) return { ok: false, reason: review.reason };
231
239
  accepted = review.reviewResult;
@@ -28,7 +28,7 @@ import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js
28
28
  import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
29
29
  import { envLiteral } from '../platform/env-literal.js';
30
30
  import { getUserConfig, userConfigPath } from '../user-config.js';
31
- import { addItem, cellsReferencingDoc, claimItem, decodeClaimHistoryEntry, devVersion, lintDocRefs, listChecklist, normalizeRefs, ownerMatches, parseOwner, parityGap, refRoots, removeItem, renderRefsStatus, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind } from '../release-loop/checklist.js';
31
+ import { addItem, cellsReferencingDoc, claimItem, decodeClaimHistoryEntry, devVersion, inspectChecklistTargetPaths, lintDocRefs, listChecklist, normalizeRefs, ownerMatches, parseOwner, parityGap, refRoots, removeItem, renderRefsStatus, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind, type ChecklistDispatchKind } from '../release-loop/checklist.js';
32
32
  import * as features from '../release-loop/feature-store.js';
33
33
  import { evidencePlan, landedButYellow, type MergedChecklistPr } from '../release-loop/landed-but-yellow.js';
34
34
  import { formatSchedule, getSchedule, listSchedules, setSchedule } from '../release-loop/release-schedule.js';
@@ -488,6 +488,28 @@ async function jsonAction<T>(json: boolean | undefined, body: (log: (l: string)
488
488
  }
489
489
  }
490
490
 
491
+ function warnChecklistTargetPaths(input: { title: string; evidence?: string; refs?: readonly string[]; dispatchKind?: ChecklistDispatchKind }): void {
492
+ const cwd = process.cwd();
493
+ const existsAtMain = (path: string): boolean => {
494
+ if (path.startsWith('/') || path.split('/').includes('..')) return false;
495
+ const result = spawnSync('git', ['cat-file', '-e', `origin/main:${path}`], { cwd, timeout: 5_000, stdio: 'ignore' });
496
+ return result.status === 0;
497
+ };
498
+ const mergedPrFiles = (number: number): string[] => {
499
+ const result = spawnSync('gh', ['pr', 'view', String(number), '--json', 'state,files'], { cwd, encoding: 'utf8', timeout: 10_000 });
500
+ if (result.status !== 0) return [];
501
+ try {
502
+ const pr = JSON.parse(result.stdout) as { state?: string; files?: Array<{ path?: string }> };
503
+ return pr.state === 'MERGED' ? (pr.files ?? []).map((file) => file.path).filter((path): path is string => typeof path === 'string') : [];
504
+ } catch { return []; }
505
+ };
506
+ const report = inspectChecklistTargetPaths(input, { existsAtMain, mergedPrFiles });
507
+ for (const warning of report.warnings) {
508
+ console.error(`⚠ 대상 경로: ${warning}${report.candidates.length ? ` · 후보: ${report.candidates.join(' · ')}` : ''}`);
509
+ debug.log('release.checklist', 'target-path-warning', { warning, candidates: report.candidates });
510
+ }
511
+ }
512
+
491
513
  function checklistCodenames(): Record<string, string> {
492
514
  const path = getElanousConfigDirOverride() ? join(effectiveInstanceRoot(), 'config.json') : userConfigPath();
493
515
  if (!existsSync(path)) return {};
@@ -815,7 +837,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
815
837
  process.exitCode = 1;
816
838
  }
817
839
  });
818
- withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄 · postpub = 발행 뒤 실측 칸 — 판 관문이 발행 전 판정에서 빼고 따로 센다')
840
+ withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄 · postpub = 발행 뒤 실측 칸 — 판 관문이 발행 전 판정에서 빼고 따로 센다 · code|ops|measure|decision|umbrella = 발사 분류')
819
841
  .option('--priority <priority>', 'P0|P1|P2').option('--deadline-version <v>', '마감 판').option('--predecessor <id...>', '선행 칸 id')
820
842
  .option('--ceo-minutes <minutes>', '대표 손 분량(분)').option('--ceo-date <date>', '대표 손 날짜 YYYY-MM-DD (없으면 판 착지일 KST)')
821
843
  .option('--accelerator', '가속 등급 — 다른 칸의 처리량을 올리는 칸(자율성·효율성·동시성). 생략하면 등급 없음')
@@ -824,12 +846,14 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
824
846
  .action((id: string, title: string, opts: { ref?: string[]; owner?: string; kind?: string; priority?: string; deadlineVersion?: string; predecessor?: string[]; ceoMinutes?: string; ceoDate?: string; accelerator?: boolean; allowDuplicateId?: boolean }, cmd: Command) => {
825
847
  const { version, json } = context(cmd);
826
848
  if (!mayWrite(cmd)) return;
827
- const data = addItem(version, { id, title, ...(opts.ref !== undefined ? { refs: opts.ref } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor } : {}), ...(opts.ceoMinutes !== undefined ? { ceoMinutes: ceoMinutes(opts.ceoMinutes) } : {}), ...(opts.ceoDate !== undefined ? { ceoDate: opts.ceoDate } : {}), ...(opts.accelerator ? { accelerator: true } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
849
+ const dispatchKind = opts.kind && !['screen', 'postpub'].includes(opts.kind) ? opts.kind as ChecklistDispatchKind : undefined;
850
+ const data = addItem(version, { id, title, ...(opts.ref !== undefined ? { refs: opts.ref } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined && !dispatchKind ? { kind: opts.kind as ChecklistKind } : {}), ...(dispatchKind ? { dispatchKind } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor } : {}), ...(opts.ceoMinutes !== undefined ? { ceoMinutes: ceoMinutes(opts.ceoMinutes) } : {}), ...(opts.ceoDate !== undefined ? { ceoDate: opts.ceoDate } : {}), ...(opts.accelerator ? { accelerator: true } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
851
+ warnChecklistTargetPaths({ title, refs: opts.ref, dispatchKind });
828
852
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 추가`);
829
853
  });
830
854
  withContext(checklist.command('set <id>').description('칸 상태·근거·담당·처분 갱신'))
831
855
  .option('--status <status>', 'green|yellow|red|done').option('--evidence <evidence>', '근거').option('--owner <owner>', '담당')
832
- .option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄 · postpub = 발행 뒤 실측 칸 — 판 관문이 발행 전 판정에서 빼고 따로 센다')
856
+ .option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄 · postpub = 발행 뒤 실측 칸 — 판 관문이 발행 전 판정에서 빼고 따로 센다 · code|ops|measure|decision|umbrella = 발사 분류')
833
857
  .option('--priority <priority>', 'P0|P1|P2').option('--deadline-version <v>', '마감 판').option('--predecessor <id...>', '선행 칸 id').option('--no-predecessor', '선행 칸 비우기(진짜 의존이 아니면 · CHECKLIST-PRED-CLEAR)')
834
858
  .option('--ceo-minutes <minutes>', '대표 손 분량(분)').option('--ceo-date <date>', '대표 손 날짜 YYYY-MM-DD')
835
859
  .option('--accelerator', '가속 등급 부여').option('--no-accelerator', '가속 등급 해제(기본은 없음)')
@@ -840,7 +864,8 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
840
864
  if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new CliUserError(`잘못된 처분: ${opts.disposition}`, 'move|known-issue|block');
841
865
  if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined && opts.priority === undefined && opts.deadlineVersion === undefined && opts.predecessor === undefined && opts.ceoMinutes === undefined && opts.ceoDate === undefined && opts.accelerator === undefined && opts.ref === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--ref · --status · --evidence · --owner · --disposition · --kind · --priority · --deadline-version · --predecessor · --ceo-minutes · --ceo-date · --accelerator · --no-accelerator 중 하나');
842
866
  if (!mayWrite(cmd)) return;
843
- const data = setItem(version, id, { ...(opts.ref !== undefined ? { refs: opts.ref } : {}), ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor === false ? [] : opts.predecessor } : {}), ...(opts.ceoMinutes !== undefined ? { ceoMinutes: ceoMinutes(opts.ceoMinutes) } : {}), ...(opts.ceoDate !== undefined ? { ceoDate: opts.ceoDate } : {}), ...(opts.accelerator !== undefined ? { accelerator: opts.accelerator ? true : null } : {}) }, process.env.ELANOUS_TRACK || 'cli');
867
+ const dispatchKind = opts.kind && !['screen', 'postpub'].includes(opts.kind) ? opts.kind as ChecklistDispatchKind : undefined;
868
+ const data = setItem(version, id, { ...(opts.ref !== undefined ? { refs: opts.ref } : {}), ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}), ...(opts.kind !== undefined && !dispatchKind ? { kind: opts.kind as ChecklistKind } : {}), ...(dispatchKind ? { dispatchKind } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor === false ? [] : opts.predecessor } : {}), ...(opts.ceoMinutes !== undefined ? { ceoMinutes: ceoMinutes(opts.ceoMinutes) } : {}), ...(opts.ceoDate !== undefined ? { ceoDate: opts.ceoDate } : {}), ...(opts.accelerator !== undefined ? { accelerator: opts.accelerator ? true : null } : {}) }, process.env.ELANOUS_TRACK || 'cli');
844
869
  const item = data.items.find((entry) => entry.id === id);
845
870
  const why = item?.kind === 'screen' && item.status === 'green' ? parityGap(item.evidence) : null;
846
871
  if (why) {
@@ -891,6 +916,8 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
891
916
  if (!mayWrite(cmd)) return;
892
917
  const snapshot = listChecklist(version);
893
918
  features.retitle(id, title, actor(), snapshot.released, snapshot.dev);
919
+ const item = snapshot.items.find((entry) => entry.id === id);
920
+ warnChecklistTargetPaths({ title, evidence: item?.evidence, refs: item?.refs, dispatchKind: item?.dispatchKind });
894
921
  if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${id} 제목 수정`);
895
922
  });
896
923
  const evidence = checklist.command('evidence').description('칸의 PR·커밋 근거');