elanous 0.2.27 → 0.2.28

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 (285) 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-a9c338f5bb556f42.js → 1454-29d1d773d5996a49.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-07110da72c45e68f.js → page-24453f6a302816b4.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/{page-d2d76d4a7eae91bd.js → page-6c131d30bb1cf871.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/page-fa46d4588e8442c2.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-e5a9302c105725a3.js → page-dc0772b11861af51.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-6a16a48d1aa6e91f.js → page-256ffd365d32e100.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e0de78ae7b99f01c.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/exec/{page-39f82acf619cca23.js → page-4c741eb760f2f77c.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/{page-7c2c8f81b185f9c0.js → page-e9ba98f5f1440a21.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/inside/{page-ae7aff07e32676bb.js → page-dafc901bb1b72b5f.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-482c60c1bc540d0a.js → page-2f6f303fa114ec24.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/{layout-7cb996c1aa08b6f5.js → layout-1634081c5272006c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-623e08fe4477d606.js → page-ced0bcfc6542b3fc.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-db460cd8aecb8490.js → page-bb1f05cd51c38944.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-ff27902d0b11ef4f.js → page-646b50f63b6fe732.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-1917ea87ea71ab6b.js → page-f736324afa2117c1.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-25a302273b054fd7.js → page-6f131b51193a36f9.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/{not-found-670510eda8b9443f.js → not-found-435f5e2faf02bc01.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-497e0eedf20e7890.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a8d28fdf995e16ea.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4bb7556e2e97a750.js → page-8966452228753c5e.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/{page-4eb96fc7241c2456.js → page-66a67eb19ebd976b.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-0d64ff212dc55d03.js → page-c71267e7681da982.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-7f096248993fc2d4.js → page-a21d1e1797172692.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-e47fc72049149a38.js → page-cb249e81ed339318.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-f7d808e31e005895.js → page-781cfc5114e097da.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-5c2b24b5496dad73.js → page-478157214db6a023.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-76cdb531668b5191.js → page-86141f12cf090b7f.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/page-7256c73bb1c36751.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/today/{page-dab163b338e3b966.js → page-979d318a534e92da.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-0260ffc7eda50db1.js → page-21050928a697ed7a.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-99c4e6c7345a9156.js → page-601fb77340e63c9a.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-11bf9e5725a72c22.js +1 -0
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +13 -13
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +13 -13
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +13 -13
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +13 -13
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +13 -13
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/decisions/index.html +1 -1
  72. package/apps/pwa/out/decisions/index.txt +13 -13
  73. package/apps/pwa/out/design-check/index.html +1 -1
  74. package/apps/pwa/out/design-check/index.txt +13 -13
  75. package/apps/pwa/out/editor/index.html +1 -1
  76. package/apps/pwa/out/editor/index.txt +13 -13
  77. package/apps/pwa/out/exec/index.html +1 -1
  78. package/apps/pwa/out/exec/index.txt +13 -13
  79. package/apps/pwa/out/field/index.html +1 -1
  80. package/apps/pwa/out/field/index.txt +13 -13
  81. package/apps/pwa/out/healthz +1 -1
  82. package/apps/pwa/out/index.html +1 -1
  83. package/apps/pwa/out/index.txt +13 -13
  84. package/apps/pwa/out/inside/index.html +1 -1
  85. package/apps/pwa/out/inside/index.txt +13 -13
  86. package/apps/pwa/out/intake/index.html +1 -1
  87. package/apps/pwa/out/intake/index.txt +13 -13
  88. package/apps/pwa/out/live/index.html +1 -1
  89. package/apps/pwa/out/live/index.txt +13 -13
  90. package/apps/pwa/out/live-run/index.html +1 -1
  91. package/apps/pwa/out/live-run/index.txt +13 -13
  92. package/apps/pwa/out/loops/index.html +1 -1
  93. package/apps/pwa/out/loops/index.txt +13 -13
  94. package/apps/pwa/out/market/index.html +1 -1
  95. package/apps/pwa/out/market/index.txt +13 -13
  96. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  97. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  98. package/apps/pwa/out/morning/index.html +1 -1
  99. package/apps/pwa/out/morning/index.txt +13 -13
  100. package/apps/pwa/out/observatory/index.html +1 -1
  101. package/apps/pwa/out/observatory/index.txt +13 -13
  102. package/apps/pwa/out/ops/checklist/index.html +1 -1
  103. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  104. package/apps/pwa/out/ops/release/index.html +1 -1
  105. package/apps/pwa/out/ops/release/index.txt +13 -13
  106. package/apps/pwa/out/ops/seats/index.html +1 -1
  107. package/apps/pwa/out/ops/seats/index.txt +13 -13
  108. package/apps/pwa/out/outputs/index.html +1 -1
  109. package/apps/pwa/out/outputs/index.txt +13 -13
  110. package/apps/pwa/out/reflection/index.html +1 -1
  111. package/apps/pwa/out/reflection/index.txt +14 -14
  112. package/apps/pwa/out/scheduler/index.html +1 -1
  113. package/apps/pwa/out/scheduler/index.txt +13 -13
  114. package/apps/pwa/out/sessions/index.html +1 -1
  115. package/apps/pwa/out/sessions/index.txt +13 -13
  116. package/apps/pwa/out/settings/devices/index.html +1 -1
  117. package/apps/pwa/out/settings/devices/index.txt +13 -13
  118. package/apps/pwa/out/settings/index.html +1 -1
  119. package/apps/pwa/out/settings/index.txt +13 -13
  120. package/apps/pwa/out/setup/done/index.html +1 -1
  121. package/apps/pwa/out/setup/done/index.txt +13 -13
  122. package/apps/pwa/out/setup/index.html +1 -1
  123. package/apps/pwa/out/setup/index.txt +13 -13
  124. package/apps/pwa/out/share/index.html +1 -1
  125. package/apps/pwa/out/share/index.txt +13 -13
  126. package/apps/pwa/out/showroom/index.html +1 -1
  127. package/apps/pwa/out/showroom/index.txt +13 -13
  128. package/apps/pwa/out/tasks/index.html +1 -1
  129. package/apps/pwa/out/tasks/index.txt +13 -13
  130. package/apps/pwa/out/term/index.html +1 -1
  131. package/apps/pwa/out/term/index.txt +13 -13
  132. package/apps/pwa/out/today/index.html +1 -1
  133. package/apps/pwa/out/today/index.txt +13 -13
  134. package/apps/pwa/out/trace/index.html +1 -1
  135. package/apps/pwa/out/trace/index.txt +13 -13
  136. package/apps/pwa/out/vault/index.html +1 -1
  137. package/apps/pwa/out/vault/index.txt +13 -13
  138. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  139. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  140. package/apps/pwa/out/workflows/index.html +1 -1
  141. package/apps/pwa/out/workflows/index.txt +13 -13
  142. package/apps/pwa/out/workspace/index.html +1 -1
  143. package/apps/pwa/out/workspace/index.txt +13 -13
  144. package/apps/pwa/out/worktrees/index.html +1 -1
  145. package/apps/pwa/out/worktrees/index.txt +13 -13
  146. package/graphs/implement-loop.yaml +38 -1
  147. package/graphs/steward/recipes.yaml +6 -0
  148. package/graphs/steward/release-day.yaml +23 -0
  149. package/graphs/steward/steward.yaml +9 -1
  150. package/graphs/work/code-pr.yaml +23 -0
  151. package/package.json +1 -1
  152. package/scripts/browser-observe-cycle.ts +76 -2
  153. package/scripts/ci-typecheck-changed.ts +50 -17
  154. package/scripts/detect-test-interference.ts +31 -1
  155. package/scripts/release-loop/docs-node.ts +51 -22
  156. package/scripts/release-loop/gate-node.ts +37 -4
  157. package/scripts/release-loop/node-verdict.ts +3 -1
  158. package/scripts/release-loop/publish-node.ts +3 -0
  159. package/scripts/release-loop/tui-node-checks.ts +5 -2
  160. package/scripts/skills/public-check.ts +5 -2
  161. package/scripts/test-deterministic.ts +520 -37
  162. package/scripts/ux-sim.ts +7 -1
  163. package/scripts/verify-kpack6-live.ts +230 -0
  164. package/src/acp/codex-plugins.ts +17 -0
  165. package/src/agent/task-notification.ts +2 -1
  166. package/src/agent-mission/in-agent-plugin.ts +8 -3
  167. package/src/agent-substrate/self-review-cli.ts +34 -10
  168. package/src/briefing/brief-items.ts +151 -35
  169. package/src/budget/llm-cost.ts +25 -2
  170. package/src/budget/usage-outcomes.ts +32 -0
  171. package/src/claims/claims-ledger.ts +23 -0
  172. package/src/claims/claims-render.ts +22 -2
  173. package/src/cli/brief-cli.ts +131 -10
  174. package/src/cli/claims-cli.ts +24 -4
  175. package/src/cli/msg-cli.ts +165 -9
  176. package/src/cli/plugin-cli.ts +11 -3
  177. package/src/cli/pr-cli.ts +80 -18
  178. package/src/cli/pr-land-refusal.ts +20 -0
  179. package/src/cli/release-cli.ts +2 -0
  180. package/src/cli/rfc-cli.ts +24 -0
  181. package/src/cli/tailscale-serve.ts +5 -8
  182. package/src/cli/tasks-cli.ts +393 -26
  183. package/src/cli/usage-cli.ts +9 -4
  184. package/src/dashboard/first-screen-band.ts +63 -13
  185. package/src/dashboard/index.ts +8 -3
  186. package/src/dashboard/skill-detection-runtime.ts +24 -4
  187. package/src/domains/morning-report.ts +3 -1
  188. package/src/github/pr-labels.ts +10 -4
  189. package/src/graph-runner/runner.ts +101 -14
  190. package/src/harness/harness-clean.ts +21 -3
  191. package/src/harness/harness-cli-command.ts +22 -6
  192. package/src/harness/harness-pod-dispatch.ts +11 -4
  193. package/src/hq/seats.ts +17 -3
  194. package/src/index.ts +34 -5
  195. package/src/intelligence-map/model-bench-scores.ts +102 -0
  196. package/src/loops/orchestrator/inbox.ts +73 -0
  197. package/src/loops/orchestrator/resync-shadow.ts +100 -12
  198. package/src/loops/orchestrator/tick.ts +73 -2
  199. package/src/maturity/feature-maturity.ts +1 -1
  200. package/src/msg/msg-sse-server.ts +141 -0
  201. package/src/msg/msg-store.ts +84 -14
  202. package/src/patents/ledger.ts +128 -0
  203. package/src/plugins/install/market-fetch.ts +32 -1
  204. package/src/pod-lease/admission.ts +116 -41
  205. package/src/release-loop/checklist.ts +11 -0
  206. package/src/release-loop/merged-pr-checklist.ts +24 -5
  207. package/src/release-loop/rubric.ts +70 -0
  208. package/src/rfc-registry/registry.ts +88 -0
  209. package/src/seat-dispatch/seat-request-ledger.ts +4 -0
  210. package/src/seat-loop/knowhow-lint.ts +233 -0
  211. package/src/seat-loop/seat-pack.ts +16 -4
  212. package/src/seat-loop/traffic-signal.ts +7 -0
  213. package/src/self-dev/ask-launch-flow.ts +48 -8
  214. package/src/self-dev/ask-path-candidates.ts +69 -14
  215. package/src/self-dev/decompose-proposal.ts +43 -1
  216. package/src/self-dev/graph-journey-nodes.ts +2 -0
  217. package/src/self-dev/launch-preflight-text.ts +5 -1
  218. package/src/self-dev/launch-preflight.ts +143 -10
  219. package/src/self-implement/gate-cli.ts +6 -2
  220. package/src/self-implement/goal-author.ts +2 -1
  221. package/src/self-implement/goal-new-paths.ts +4 -5
  222. package/src/self-implement/graph-authority.ts +17 -3
  223. package/src/self-implement/graph-yaml.ts +15 -7
  224. package/src/self-implement/headless-elanous-driver.ts +16 -1
  225. package/src/self-implement/host-regate.ts +44 -9
  226. package/src/self-implement/orchestrator.ts +3 -10
  227. package/src/self-implement/rework-policy.ts +80 -2
  228. package/src/self-implement/speed-claims.ts +145 -6
  229. package/src/skills/tools/persistent-grounding.ts +59 -4
  230. package/src/skills/tools/shell.ts +79 -1
  231. package/src/steward/rejudge-tick.ts +12 -1
  232. package/src/steward/release-day.ts +158 -0
  233. package/src/steward/resync-tick.ts +123 -0
  234. package/src/task-agent/actions.ts +1 -0
  235. package/src/task-agent/adopt-pr.ts +140 -0
  236. package/src/task-agent/board.ts +11 -1
  237. package/src/task-agent/card-evidence.ts +37 -5
  238. package/src/task-agent/cover.ts +84 -17
  239. package/src/task-agent/failure-notify.ts +61 -0
  240. package/src/task-agent/green-sweep.ts +170 -0
  241. package/src/task-agent/land-worktree.ts +52 -5
  242. package/src/task-agent/live-move-history.ts +1 -0
  243. package/src/task-agent/live-moves.ts +43 -12
  244. package/src/task-agent/parent-host.ts +1 -1
  245. package/src/task-agent/postland-card.ts +139 -0
  246. package/src/task-agent/rejudge-on-head.ts +106 -7
  247. package/src/task-agent/remote-rejudge-bind.ts +62 -0
  248. package/src/task-agent/repair-branch-cas.ts +182 -0
  249. package/src/task-agent/repair-cycle.ts +275 -0
  250. package/src/task-agent/repair-next-move.ts +31 -0
  251. package/src/task-agent/repair-review.ts +24 -0
  252. package/src/task-agent/repair-worker.ts +87 -0
  253. package/src/task-agent/review-result-capture.ts +2 -1
  254. package/src/task-agent/shadow.ts +13 -7
  255. package/src/task-agent/task-hand.ts +115 -6
  256. package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
  257. package/src/task-orchestrator/surfaces/self-implement-pod.ts +70 -12
  258. package/src/telegram.ts +30 -1
  259. package/src/user-config.ts +8 -1
  260. package/src/ux-sim/fault-sqlite-busy.ts +50 -0
  261. package/src/version/packed-revision.json +1 -1
  262. package/apps/pwa/out/_next/static/chunks/app/approvals/page-a1ce306364f08da9.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/consult/page-20662900d6b4b455.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/control/page-f707a509515ac50c.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/market/page-8397a5c6ca569bf4.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/outputs/page-15c0a4095baddfba.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d376e0c83edc041a.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/trace/page-08019efef111ed2e.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-980ce6fe36ad95eb.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/workspace/page-0ab42e0f05823013.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +0 -1
  284. /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
  285. /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
@@ -11,6 +11,10 @@ export type SeatRequestRow = {
11
11
  text: string;
12
12
  queuedAt: string;
13
13
  reason?: string;
14
+ /** One-line explanation of the automated action recorded on this row. */
15
+ why?: string;
16
+ /** Pointer to the evidence supporting the action (log line, run id, ledger path, …). */
17
+ evidenceRef?: string;
14
18
  closedAt?: string;
15
19
  [field: string]: unknown;
16
20
  };
@@ -0,0 +1,233 @@
1
+ import { seatPackLeakPatterns } from './seat-pack.js';
2
+
3
+ /** 필수 칸 — 형식 v2 선언(각 자리 KNOWHOW 머리글)의 공통 아홉. 괄호 수식어(신호(관측) 등)는 정규화해서 비교한다. */
4
+ const REQUIRED_FIELDS = ['상황', '신호', '원인', '처방', '재는 명령', '기대', '유효 조건', '검증', '출처'] as const;
5
+
6
+ /** Only commands whose executable and subcommand are known to be observational are accepted. */
7
+ const READ_COMMANDS: Record<string, ReadonlySet<string> | null> = {
8
+ cat: null, echo: null, rg: null, grep: null, head: null, tail: null,
9
+ wc: null, sort: null, uniq: null, cut: null, jq: null, ls: null, stat: null,
10
+ readlink: null, pwd: null, test: null,
11
+ git: new Set(['rev-parse', 'status', 'log', 'show', 'merge-base']),
12
+ eln: new Set(['logs', 'where', 'tasks', 'config', 'release']),
13
+ 'bun bin/elanous.mjs': new Set(['logs', 'where', 'release']),
14
+ };
15
+
16
+ /** Git's options are commands too: e.g. show --textconv runs a configured external driver. */
17
+ const READ_GIT_OPTIONS: Record<string, ReadonlySet<string>> = {
18
+ 'rev-parse': new Set(['--git-dir', '--show-toplevel', '--is-inside-work-tree', '--verify', '--short', '--abbrev-ref', '--show-prefix']),
19
+ status: new Set(['--short', '--porcelain', '--branch', '--untracked-files=all', '--untracked-files=no']),
20
+ log: new Set(['--oneline', '--no-patch', '--stat', '--name-only', '--name-status', '--all', '--decorate']),
21
+ show: new Set(['--no-patch', '--stat', '--name-only', '--name-status', '--oneline']),
22
+ 'merge-base': new Set(['--is-ancestor', '--fork-point', '--all']),
23
+ };
24
+
25
+ /** `logs level <value>` changes daemon state. Accept only the top-level query options. */
26
+ const LOGS_QUERY_FLAGS = new Set([
27
+ '--follow', '-f', '--list-categories', '--list-events', '--explain', '--top-failures',
28
+ '--json', '--json-data', '--test', '--all', '--include-test', '-r', '--help', '-h',
29
+ ]);
30
+ const LOGS_QUERY_VALUES = new Set([
31
+ '--level', '--surface', '--space', '--category', '--exact-category', '--axis', '--event',
32
+ '--grep', '--rework-recurrence-disagreement', '--since', '--until', '--before',
33
+ '--session', '--limit', '--threshold', '--instance', '--remote',
34
+ ]);
35
+
36
+ function readOnlyLogs(args: string[]): boolean {
37
+ for (let i = 0; i < args.length; i++) {
38
+ const arg = args[i]!;
39
+ if (LOGS_QUERY_FLAGS.has(arg)) continue;
40
+ if (LOGS_QUERY_VALUES.has(arg) && args[i + 1] && !args[i + 1]!.startsWith('-')) { i++; continue; }
41
+ return false;
42
+ }
43
+ return true;
44
+ }
45
+
46
+ function readOnlyGit(rest: string[]): boolean {
47
+ const [subcommand, ...args] = rest;
48
+ const options = READ_GIT_OPTIONS[subcommand ?? ''];
49
+ if (!options) return false;
50
+ return args.every((arg) => !arg.startsWith('-') || options.has(arg)
51
+ || ((subcommand === 'log' || subcommand === 'show')
52
+ && (/^--(?:format|pretty|grep|since|until|max-count)=.+$/u.test(arg) || /^-[1-9][0-9]*$/u.test(arg))));
53
+ }
54
+
55
+ /** Do not execute the measurement here: parse only a small, fail-closed shell subset. */
56
+ function readOnlyMeasure(command: string): boolean {
57
+ const source = command.trim().replace(/^`|`$/gu, '');
58
+ const tokens: string[] = [];
59
+ let word = '';
60
+ let quote: '"' | "'" | null = null;
61
+ for (let i = 0; i < source.length; i++) {
62
+ const char = source[i]!;
63
+ if (char === '\\' || char === '`' || char === '$' || char === '\n') return false;
64
+ if (quote) {
65
+ if (char === quote) quote = null;
66
+ else word += char;
67
+ } else if (char === '"' || char === "'") {
68
+ quote = char;
69
+ } else if (/\s/u.test(char)) {
70
+ if (word) { tokens.push(word); word = ''; }
71
+ } else if ('|&;<>()[{}]'.includes(char)) {
72
+ if (word) { tokens.push(word); word = ''; }
73
+ if (char === '|' || char === '&') {
74
+ if (source[i + 1] === char) { tokens.push(char + char); i++; }
75
+ else if (char === '|') tokens.push('|');
76
+ else return false;
77
+ } else if (char === '>') {
78
+ if (source[i + 1] === '>') return false;
79
+ if (source.slice(i + 1, i + 3) === '&1') { tokens.push('>&1'); i += 2; }
80
+ else tokens.push('>');
81
+ } else return false;
82
+ } else word += char;
83
+ }
84
+ if (quote) return false;
85
+ if (word) tokens.push(word);
86
+ if (!tokens.length) return false;
87
+ const segments: string[][] = [[]];
88
+ for (let i = 0; i < tokens.length; i++) {
89
+ const token = tokens[i]!;
90
+ if (token === '>' && tokens[i + 1] === '/dev/null') { i++; continue; }
91
+ if (token === '2' && tokens[i + 1] === '>&1') { i++; continue; }
92
+ if (token === '&&' || token === '||' || token === '|') {
93
+ if (!segments.at(-1)!.length) return false;
94
+ segments.push([]);
95
+ } else if (token === '>' || token === '>&1') return false;
96
+ else segments.at(-1)!.push(token);
97
+ }
98
+ return segments.every((args) => {
99
+ if (!args.length || args.some((arg) => /^(?:--pre|--output|--ext-diff|--exec|--config|--config-env|--write)(?:=|$)/u.test(arg))) return false;
100
+ if (args.some((arg) => /^(?:--output(?:=\S*)?|--replace|--in-place)$/u.test(arg))) return false;
101
+ if (args[0] === 'sort' && args.some((arg) => /^-o\S*|^--compress-program(?:=|$)/u.test(arg))) return false;
102
+ let executable = args[0]!;
103
+ let rest = args.slice(1);
104
+ if (executable === 'bun' && rest[0] === 'bin/elanous.mjs') { executable = 'bun bin/elanous.mjs'; rest = rest.slice(1); }
105
+ if (!Object.hasOwn(READ_COMMANDS, executable)) return false;
106
+ const allowed = READ_COMMANDS[executable]!;
107
+ if (allowed === null) return true;
108
+ if (executable === 'bun bin/elanous.mjs' && rest[0] === '--test') rest = rest.slice(1);
109
+ if (!allowed.has(rest[0] ?? '')) return false;
110
+ if (executable === 'git') return readOnlyGit(rest);
111
+ if ((executable === 'eln' || executable === 'bun bin/elanous.mjs') && rest[0] === 'logs') return readOnlyLogs(rest.slice(1));
112
+ if ((executable === 'eln' || executable === 'bun bin/elanous.mjs') && rest[0] === 'tasks') return rest[1] === 'close' && rest[2] === '--help';
113
+ if ((executable === 'eln' || executable === 'bun bin/elanous.mjs') && rest[0] === 'config') return rest[1] === 'get';
114
+ if ((executable === 'eln' || executable === 'bun bin/elanous.mjs') && rest[0] === 'release') return rest[1] === 'checklist' && rest[2] === 'status';
115
+ return true;
116
+ });
117
+ }
118
+
119
+ /** 비밀 표식 — 팩 스캔 표식(seatPackLeakPatterns)에 실제 자격 모양을 더한 집합. */
120
+ const SECRET_SHAPE_PATTERNS: Array<[string, RegExp]> = [
121
+ ['api key', /\b(?:sk|rk)-(?:proj-)?[A-Za-z0-9_-]{16,}\b/u],
122
+ ['github token', /\b(?:gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{16,})\b/u],
123
+ ['slack token', /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/u],
124
+ ['aws key id', /\bAKIA[0-9A-Z]{12,}\b/u],
125
+ ['private key block', /-----BEGIN [A-Z ]*PRIVATE KEY-----/u],
126
+ ['credential assignment', /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*["']?[A-Za-z0-9][A-Za-z0-9._/+~-]{11,}/iu],
127
+ ];
128
+
129
+ export class KnowhowLintError extends Error {
130
+ constructor(file: string, card: string, reason: string) {
131
+ super(`knowhow lint: ${file} ${card} — ${reason}`);
132
+ this.name = 'KnowhowLintError';
133
+ }
134
+ }
135
+
136
+ interface KnowhowCard {
137
+ id: string;
138
+ lines: Array<[number, string]>;
139
+ }
140
+
141
+ /** 카드 제목 — `## K-<자리>-<번호>` 뒤에는 줄 끝·공백·콜론·em 대시만 온다. */
142
+ const CARD_HEADING = /^[ \t]{0,3}##\s+(K-[A-Za-z]+-\d+)(?=$|\s|:|—)/u;
143
+ /** 카드처럼 보이는 `##` 제목(예: `## K-OP-7a`) — CARD_HEADING 을 벗어나면 인식 실패로 명시 거부한다. */
144
+ const CARD_HEADING_LIKE = /^[ \t]{0,3}##\s*(K-[^\s:]*)/iu;
145
+
146
+ function parseCards(body: string, file: string): KnowhowCard[] {
147
+ const cards: KnowhowCard[] = [];
148
+ let current: KnowhowCard | null = null;
149
+ let fence: { marker: string; length: number } | null = null;
150
+ for (const [index, line] of body.split(/\r?\n/u).entries()) {
151
+ const fenceLine = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/u);
152
+ if (fence) {
153
+ if (fenceLine && fenceLine[1]![0] === fence.marker && fenceLine[1]!.length >= fence.length
154
+ && /^[ \t]*$/u.test(fenceLine[2]!)) fence = null;
155
+ continue;
156
+ }
157
+ if (fenceLine) {
158
+ fence = { marker: fenceLine[1]![0]!, length: fenceLine[1]!.length };
159
+ continue;
160
+ }
161
+ const heading = line.match(CARD_HEADING);
162
+ if (heading) {
163
+ current = { id: heading[1]!, lines: [] };
164
+ cards.push(current);
165
+ continue;
166
+ }
167
+ if (/^[ \t]{0,3}#/u.test(line)) {
168
+ const like = line.match(CARD_HEADING_LIKE);
169
+ if (like) throw new KnowhowLintError(file, like[1]!, '카드 제목 형식이 K-<자리>-<번호>를 벗어남');
170
+ current = null;
171
+ } else if (current) current.lines.push([index + 1, line]);
172
+ }
173
+ return cards;
174
+ }
175
+
176
+ function parseField(line: string): { label: string; value: string } | null {
177
+ const match = line.match(/^-\s*([^:—]+?)\s*[—:]\s*(.*)$/u);
178
+ if (!match) return null;
179
+ return { label: match[1]!.replace(/\s*\([^)]*\)/gu, '').trim(), value: match[2]!.trim() };
180
+ }
181
+
182
+ /** 자리 KNOWHOW.md 카드를 내보내기 전에 검증한다 — 카드 제목 형식 · 필수 칸 · 읽기 전용 재는 명령(이어 적은 여러 줄 포함) · 비밀 표식. */
183
+ export function lintKnowhow(body: string, file: string): void {
184
+ const secrets = [...seatPackLeakPatterns(), ...SECRET_SHAPE_PATTERNS];
185
+ for (const [index, line] of body.split(/\r?\n/u).entries()) {
186
+ for (const [label, pattern] of secrets) {
187
+ if (pattern.test(line)) throw new KnowhowLintError(file, 'file', `비밀 표식 (${label}) — ${file}:${index + 1}`);
188
+ }
189
+ }
190
+ const cards = parseCards(body, file);
191
+ if (!cards.length) throw new KnowhowLintError(file, 'file', '카드가 0개 — 최소 한 개의 K-<자리>-<번호> 카드가 필요함');
192
+ for (const card of cards) {
193
+ const fields = new Map<string, string>();
194
+ const fieldEntries: Array<{ label: string; value: string }> = [];
195
+ let current: { label: string; value: string } | null = null;
196
+ for (const [, line] of card.lines) {
197
+ const field = parseField(line);
198
+ if (field) {
199
+ if (current) fieldEntries.push(current);
200
+ current = { label: field.label, value: field.value };
201
+ } else if (current && /^\s+\S/u.test(line)) {
202
+ current.value += `\n${line.trim()}`;
203
+ } else if (current) {
204
+ fieldEntries.push(current);
205
+ current = null;
206
+ }
207
+ }
208
+ if (current) fieldEntries.push(current);
209
+ for (const field of fieldEntries) {
210
+ if (fields.has(field.label)) throw new KnowhowLintError(file, card.id, `중복 칸: ${field.label}`);
211
+ if ((REQUIRED_FIELDS as readonly string[]).includes(field.label) || field.label === '상태') {
212
+ if (!field.value) throw new KnowhowLintError(file, card.id, `필수 칸 누락: ${field.label}`);
213
+ }
214
+ if (field.label === '재는 명령'
215
+ && !field.value.split('\n').every((command) => readOnlyMeasure(command))) {
216
+ throw new KnowhowLintError(file, card.id, '재는 명령이 읽기 전용이 아님');
217
+ }
218
+ fields.set(field.label, field.value);
219
+ }
220
+ for (const required of REQUIRED_FIELDS) {
221
+ if (!fields.get(required)) throw new KnowhowLintError(file, card.id, `필수 칸 누락: ${required}`);
222
+ }
223
+ const source = fields.get('출처') ?? '';
224
+ const statusInSource = source.match(/(?:^|[· \t])상태[ \t]*[:—][ \t]*([^·\n]*)/u);
225
+ if (!source.slice(0, statusInSource?.index ?? source.length).replace(/[·\s]+$/u, '').trim()) {
226
+ throw new KnowhowLintError(file, card.id, '필수 칸 누락: 출처');
227
+ }
228
+ const combinedStatus = statusInSource?.[1]?.trim();
229
+ if (!fields.get('상태') && !combinedStatus) {
230
+ throw new KnowhowLintError(file, card.id, '필수 칸 누락: 상태');
231
+ }
232
+ }
233
+ }
@@ -1,8 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
3
- import { dirname, join, relative, resolve, sep } from 'node:path';
3
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
4
4
  import { userInfo } from 'node:os';
5
5
  import { DEFAULT_CONCURRENT_PODS, DEFAULT_DAILY_GOALS } from '../org/seat-budget.js';
6
+ import { lintKnowhow } from './knowhow-lint.js';
6
7
 
7
8
  export interface SeatPackManifest {
8
9
  name: string;
@@ -31,13 +32,20 @@ function walk(directory: string, root: string): string[] {
31
32
  });
32
33
  }
33
34
 
35
+ /** Static leak markers shared by the pack scan and the KNOWHOW lint. Account-name patterns stay in `scan` — they depend on the exporting host's user. */
36
+ export function seatPackLeakPatterns(): Array<[string, RegExp]> {
37
+ return [
38
+ ['royal marker', /\u{1F451}/u],
39
+ ['track marker', /(?:🅞|🅣|🅢|🅕|\*\*\[(?:OP|TC|MK|UX|S|T|O|F)\]\*\*|\[(?:OP|TC|MK|UX|S|T|O|F)\])/u],
40
+ ['absolute user path', /\/Users\/[A-Za-z0-9._-]+/u],
41
+ ];
42
+ }
43
+
34
44
  function scan(file: string, body: string): void {
35
45
  const usernames = new Set([userInfo().username, process.env.ELANOUS_LEAK_HOME_USER].filter((name): name is string => Boolean(name)));
36
46
  const accountPatterns = [...usernames].map((name) => new RegExp(`(?<![A-Za-z0-9._-])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9._-])`, 'iu'));
37
47
  const patterns: Array<[string, RegExp]> = [
38
- ['royal marker', /\u{1F451}/u],
39
- ['track marker', /(?:🅞|🅣|🅢|🅕|\*\*\[(?:OP|TC|MK|UX|S|T|O|F)\]\*\*|\[(?:OP|TC|MK|UX|S|T|O|F)\])/u],
40
- ['absolute user path', /\/Users\/[A-Za-z0-9._-]+/u],
48
+ ...seatPackLeakPatterns(),
41
49
  ...accountPatterns.map((pattern): [string, RegExp] => ['account name', pattern]),
42
50
  ];
43
51
  for (const [index, line] of body.split(/\r?\n/u).entries()) {
@@ -61,6 +69,10 @@ export function exportSeatPack(seat: string, out: string, root = resolve(import.
61
69
  for (const file of roleFiles) {
62
70
  const body = readFileSync(join(repo, file));
63
71
  contents.set(file, body);
72
+ // Public leak markers take precedence over formatting errors: callers need
73
+ // the file:line and exit 2 before an export directory can be created.
74
+ scan(file, body.toString('utf8'));
75
+ if (basename(file) === 'KNOWHOW.md') lintKnowhow(body.toString('utf8'), file);
64
76
  for (const match of body.toString('utf8').matchAll(/graphs\/([A-Za-z0-9_./-]+(?:\.ya?ml|\/\*\*))/gu)) {
65
77
  const ref = `graphs/${match[1]!}`;
66
78
  if (ref.split('/').includes('..')) throw new Error(`invalid seat graph: ${ref}`);
@@ -10,6 +10,13 @@ export type TrafficSignal = {
10
10
  };
11
11
  export type TrafficSignalRead = TrafficSignal | { unreadable: true } | null;
12
12
 
13
+ /** Pure proposal classification; applying a proposal belongs to the downstream seat-message caller. */
14
+ export function classifySeatTrafficProposal(message: string): 'hold' | 'UNHOLD' | null {
15
+ if (message.includes('첫 줄 확정')) return 'UNHOLD';
16
+ if (message.includes('손 구현 착수')) return 'hold';
17
+ return null;
18
+ }
19
+
13
20
  const defaultPath = (name: string): string => join(homedir(), 'elanous-hq', 'seat-state', 'OP', name);
14
21
  const missing = (error: unknown): boolean => (error as NodeJS.ErrnoException).code === 'ENOENT';
15
22
 
@@ -11,6 +11,8 @@
11
11
  // ⛔ 종료(`process.exit`)는 여기가 «안» 한다 — 호출자가 자기 표면에 맞게 끝낸다.
12
12
  // 그래서 결과를 «세 값»으로 낸다: 발사 / 저작 전 중단 / 전제 검사 중단.
13
13
  import { linesOutsideFencedCode } from '../self-implement/goal-author.js';
14
+ import { existsSync } from 'node:fs';
15
+ import { debug } from '../debug/log.js';
14
16
  import { recordAuthoredGoal } from './authored-goal-trace.js';
15
17
  import { findAskPathCandidates, type AskPathCandidateOptions } from './ask-path-candidates.js';
16
18
  import { retiredEntranceNotice, type EntranceDeclaration } from './entrance-registry.js';
@@ -18,6 +20,7 @@ import {
18
20
  classifyConcurrentAuthoring,
19
21
  countRepeatedBlocks,
20
22
  decideAskPreflight,
23
+ discardNonFileTracedPathItems,
21
24
  parseAskTargetPathHintsResult,
22
25
  parseBlockedChoice,
23
26
  planBlockedPrompt,
@@ -797,10 +800,10 @@ async function intakeClarifications(
797
800
  selfResolveClarifications: selfResolveClarifications.value,
798
801
  });
799
802
  const parentDocument = document;
800
- effectiveGoalFile = reauthored.path;
803
+ effectiveGoalFile = isAbsolutePath(reauthored.path) ? reauthored.path : resolvePath(deps.cwd(), reauthored.path);
801
804
  deps.print(`[ask] ⑴b 재저작됨: ${reauthored.path}`);
802
805
  // ⛔ 「재저작이 «났다»」와 「재저작이 «옳았다»」는 다른 물음이다(JDG-T35 3단 ⑶).
803
- const childDocument = reauthored.authored?.document ?? deps.readFile(reauthored.path);
806
+ const childDocument = reauthored.authored?.document ?? deps.readFile(effectiveGoalFile);
804
807
  const askFidelity = classifyReauthoredAsk(parentDocument, childDocument);
805
808
  if (askFidelity !== 'preserved') {
806
809
  deps.print(`[ask] ⑴b ⚠️ 재저작 ask 판정: ${askFidelity} — ⛔ 「preserved」가 아니면 사람의 ask 가 살아남았는지 «확인하라»`);
@@ -939,7 +942,31 @@ export async function runAskLaunchFlow(
939
942
  }, markerCounts !== null && markerCounts.missingMarkers > 0 ? 'warn' : 'info');
940
943
 
941
944
  // ⛔ 되묻기에 답하면 «새 골»이 나온다 — 아래 전제 검사·발사는 그 «최종» 골을 써야 한다.
942
- const goalFile = await intakeClarifications(authored, input, deps, selfResolveClarifications);
945
+ const goalFile = await intakeClarifications({
946
+ ...authored,
947
+ path: isAbsolutePath(authored.path) ? authored.path : resolvePath(deps.cwd(), authored.path),
948
+ }, input, deps, selfResolveClarifications);
949
+ // The child reads this file again. Filtering only preflight.paths would leave the same bogus
950
+ // TRACED PATHS prose in the child and repeat its missing-file failure.
951
+ let sanitizeFailed = false;
952
+ let sanitizeFailure: unknown;
953
+ if (existsSync(goalFile)) {
954
+ try {
955
+ const original = deps.readFile(goalFile);
956
+ const sanitized = discardNonFileTracedPathItems(original, deps.cwd());
957
+ if (sanitized.dropped.length > 0) {
958
+ // The child reads this same file, so a failed rewrite must stop launch.
959
+ (deps.writeGoalDocument ?? deps.writeFile)(goalFile, sanitized.document);
960
+ for (const dropped of sanitized.dropped) {
961
+ try { debug.log('harness.preflight', 'traced-path-discarded', { goalFile, dropped }); }
962
+ catch { /* observation must not change launch */ }
963
+ }
964
+ }
965
+ } catch (error) {
966
+ sanitizeFailed = true;
967
+ sanitizeFailure = error;
968
+ }
969
+ }
943
970
  const preflightDeps = await deps.buildPreflightDeps();
944
971
  observeLaunchingTree(deps, preflightDeps);
945
972
  const askOutsidePathHints = parseAskTargetPathHintsResult(input.askText).paths;
@@ -954,11 +981,11 @@ export async function runAskLaunchFlow(
954
981
  ...(machineSuggestedTargetPaths.length > 0 ? { machineSuggestedTargetPaths } : {}),
955
982
  declaredPathsRoot: deps.cwd(),
956
983
  },
957
- preflightDeps,
984
+ sanitizeFailed ? { ...preflightDeps, readGoalDocument: () => { throw sanitizeFailure; } } : preflightDeps,
958
985
  input.forceRequested,
959
986
  );
960
987
  const preflight = decision.result;
961
- deps.print(renderLaunchPreflight(preflight, input.forceRequested && preflight.blockers.length > 0, 'before-launch'));
988
+ deps.print(renderLaunchPreflight(preflight, input.forceRequested && decision.shouldLaunch && preflight.blockers.length > 0, 'before-launch'));
962
989
  observeLaunchPreflight(deps, 'before-launch', decision);
963
990
  // ⛔ 자식 격리 worktree를 만들기 전, 이 흐름을 호출한 작업 트리에서만 센다.
964
991
  // 이 관측은 발사 판정의 입력이 아니다 — 의도적으로 옛 판에서 쏘는 경우도 있다.
@@ -1001,7 +1028,7 @@ export async function runAskLaunchFlow(
1001
1028
  liveRunWindowMs: preflight.liveRunWindowMs,
1002
1029
  // ⛔ 「플래그를 줬다」와 「실제로 뚫었다」는 다른 값이다(리뷰 should-fix).
1003
1030
  forceRequested: input.forceRequested,
1004
- bypassed: input.forceRequested && preflight.blockers.length > 0,
1031
+ bypassed: input.forceRequested && decision.shouldLaunch && preflight.blockers.length > 0,
1005
1032
  }, preflight.blockers.length > 0 ? 'warn' : 'info');
1006
1033
  return { goalFile, decision };
1007
1034
  };
@@ -1023,14 +1050,27 @@ export async function runAskLaunchFlow(
1023
1050
  } catch (error) {
1024
1051
  candidates = { tokens: [], paths: [], error: error instanceof Error ? error.message : String(error) };
1025
1052
  }
1053
+ const selectedPath = candidates.selection?.path
1054
+ ?? (!candidates.selection && candidates.paths.length === 1 ? candidates.paths[0] : undefined);
1055
+ const selectionReason = candidates.selection?.reason
1056
+ ?? (candidates.error ? 'search-error' : selectedPath ? 'only-candidate'
1057
+ : candidates.paths.length > 0 ? 'ambiguous-without-evidence' : 'no-candidates');
1026
1058
  deps.log('ask-auto-target-candidates', {
1027
1059
  tokens: candidates.tokens, paths: candidates.paths,
1060
+ evidence: candidates.evidence ?? [],
1061
+ selection: { path: selectedPath ?? null, reason: selectionReason },
1028
1062
  ...(candidates.error === undefined ? {} : { error: candidates.error }),
1029
- }, candidates.error === undefined ? 'info' : 'warn');
1063
+ }, candidates.error === undefined && selectedPath ? 'info' : 'warn');
1030
1064
  }
1031
- const selectedPaths = candidates.paths.slice(0, 1);
1065
+ const selectedPaths = candidates.selection?.path
1066
+ ? [candidates.selection.path]
1067
+ : !candidates.selection && candidates.paths.length === 1 ? [candidates.paths[0]!] : [];
1032
1068
  if (selectedPaths.length > 0) {
1033
1069
  deps.print(`[ask] ⑴ 코드에서 후보 경로 ${selectedPaths.length}개를 찾아 넣고 다시 저작한다: ${selectedPaths.join(' · ')}`);
1070
+ } else if (candidates.paths.length > 0) {
1071
+ deps.print(`[ask] ⑴ ⚠️ 후보 경로를 하나로 판정할 근거가 없다. 대상 경로를 확인해야 한다: ${candidates.paths.join(' · ')}`);
1072
+ } else {
1073
+ deps.print('[ask] ⑴ ⚠️ 자동 재저작에 넣을 대상 경로 후보가 없다. 대상 경로를 확인해야 한다');
1034
1074
  }
1035
1075
  authored = await authorAndPreflight(selectedPaths.length > 0
1036
1076
  ? `대상 경로: ${selectedPaths.join(' · ')}\n${input.askText}`
@@ -1,15 +1,31 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { statSync } from 'node:fs';
3
- import { isAbsolute, relative, resolve } from 'node:path';
3
+ import { basename, extname, isAbsolute, relative, resolve } from 'node:path';
4
4
 
5
5
  export interface AskPathCandidateOptions {
6
6
  readonly cwd: string;
7
7
  readonly run?: (command: string, args: readonly string[], options: { cwd: string; encoding: 'utf8' }) => string;
8
8
  }
9
9
 
10
- export type AskPathCandidates =
11
- | { readonly tokens: string[]; readonly paths: string[]; readonly error?: undefined }
12
- | { readonly tokens: string[]; readonly paths: string[]; readonly error: string };
10
+ export interface AskPathCandidates {
11
+ readonly tokens: string[];
12
+ readonly paths: string[];
13
+ readonly evidence?: AskPathEvidence[];
14
+ readonly selection?: AskPathSelection;
15
+ readonly error?: string;
16
+ }
17
+
18
+ export interface AskPathEvidence {
19
+ readonly path: string;
20
+ readonly pathTokens: string[];
21
+ readonly strongestPathMatch: 'explicit-file' | 'filename' | 'directory' | null;
22
+ readonly contentTokens: string[];
23
+ readonly score: number;
24
+ }
25
+
26
+ export type AskPathSelection =
27
+ | { readonly path: string; readonly reason: 'unique-strongest-evidence' }
28
+ | { readonly path: null; readonly reason: 'tied-strongest-evidence' };
13
29
 
14
30
  const STOP_WORDS = new Set(['the', 'and', 'test', 'src', 'scripts', 'file', 'path', 'paths', 'code', 'with', 'from', 'this', 'that', 'into', 'for', 'then', 'when', 'have', 'does', 'should', 'will', 'please', 'target', 'change']);
15
31
  const WORD = /[-/]?[A-Za-z][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_.-]+)*/g;
@@ -63,7 +79,15 @@ function existingFile(fragment: string, cwd: string): string | null {
63
79
 
64
80
  /** Find existing code paths by lexical evidence from an unmodified ask. */
65
81
  export function findAskPathCandidates(askText: string, { cwd, run = execFileSync }: AskPathCandidateOptions): AskPathCandidates {
66
- const scores = new Map<string, Set<string>>();
82
+ const scores = new Map<string, { explicit: Set<string>; filename: Set<string>; directory: Set<string>; content: Set<string> }>();
83
+ const hitsFor = (path: string) => {
84
+ let hits = scores.get(path);
85
+ if (!hits) {
86
+ hits = { explicit: new Set(), filename: new Set(), directory: new Set(), content: new Set() };
87
+ scores.set(path, hits);
88
+ }
89
+ return hits;
90
+ };
67
91
  const tokens: string[] = [];
68
92
  const seen = new Set<string>();
69
93
  const body = stripAskWrapper(askText);
@@ -73,11 +97,7 @@ export function findAskPathCandidates(askText: string, { cwd, run = execFileSync
73
97
  const raw = match[0];
74
98
  const token = raw.replace(/^\//, '');
75
99
  const file = existingFile(token, cwd);
76
- if (file) {
77
- const hits = scores.get(file) ?? new Set<string>();
78
- hits.add(token);
79
- scores.set(file, hits);
80
- }
100
+ if (file) hitsFor(file).explicit.add(token);
81
101
  if (STOP_WORDS.has(token.toLowerCase()) || isGenericToken(token) || seen.has(token)) continue;
82
102
  if (!file && !token.includes('/') && !token.includes('.') && !token.includes('-') && !token.includes('_')
83
103
  && !/[a-z][A-Z]/.test(token) && token.length < 4) continue;
@@ -85,6 +105,24 @@ export function findAskPathCandidates(askText: string, { cwd, run = execFileSync
85
105
  tokens.push(token);
86
106
  }
87
107
  }
108
+ if (tokens.length > 0) {
109
+ let files: string;
110
+ try {
111
+ files = run('rg', ['--files', '--glob', '!*.test.ts', 'src', 'scripts'], { cwd, encoding: 'utf8' });
112
+ } catch (error) {
113
+ if ((error as { status?: number }).status === 1) files = '';
114
+ else return { tokens, paths: [], error: error instanceof Error ? error.message : String(error) };
115
+ }
116
+ for (const path of files.split(/\r?\n/).filter(Boolean)) {
117
+ const fileStem = basename(path, extname(path)).toLowerCase();
118
+ const directories = path.toLowerCase().split('/').slice(0, -1);
119
+ for (const token of tokens) {
120
+ const lower = token.toLowerCase();
121
+ if (fileStem === lower || basename(path).toLowerCase() === lower) hitsFor(path).filename.add(token);
122
+ else if (directories.includes(lower)) hitsFor(path).directory.add(token);
123
+ }
124
+ }
125
+ }
88
126
  for (const token of tokens) {
89
127
  let matches: string;
90
128
  try {
@@ -94,13 +132,30 @@ export function findAskPathCandidates(askText: string, { cwd, run = execFileSync
94
132
  return { tokens, paths: [], error: error instanceof Error ? error.message : String(error) };
95
133
  }
96
134
  for (const path of matches.split(/\r?\n/).filter(Boolean)) {
97
- const hits = scores.get(path) ?? new Set<string>();
98
- hits.add(token);
99
- scores.set(path, hits);
135
+ hitsFor(path).content.add(token);
100
136
  }
101
137
  }
138
+ const evidence = [...scores].map<AskPathEvidence>(([path, hits]) => {
139
+ const tier = hits.explicit.size > 0 ? 3 : hits.filename.size > 0 ? 2 : hits.directory.size > 0 ? 1 : 0;
140
+ return {
141
+ path,
142
+ pathTokens: [...new Set([...hits.explicit, ...hits.filename, ...hits.directory])],
143
+ strongestPathMatch: tier === 3 ? 'explicit-file' : tier === 2 ? 'filename' : tier === 1 ? 'directory' : null,
144
+ contentTokens: [...hits.content],
145
+ // Lexicographic tiers: even many directory hits cannot outrank one filename hit.
146
+ score: tier * 1000 + Math.min(hits.explicit.size + hits.filename.size + hits.directory.size, 9) * 100
147
+ + Math.min(hits.content.size, 99),
148
+ };
149
+ }).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path, 'en'));
150
+ const top = evidence[0];
102
151
  return {
103
152
  tokens,
104
- paths: [...scores].sort(([a, aHits], [b, bHits]) => bHits.size - aHits.size || a.localeCompare(b, 'en')).slice(0, 5).map(([path]) => path),
153
+ paths: evidence.slice(0, 5).map(({ path }) => path),
154
+ ...(top ? {
155
+ evidence: evidence.slice(0, 5),
156
+ selection: evidence[1]?.score === top.score
157
+ ? { path: null, reason: 'tied-strongest-evidence' as const }
158
+ : { path: top.path, reason: 'unique-strongest-evidence' as const },
159
+ } : {}),
105
160
  };
106
161
  }
@@ -348,7 +348,31 @@ export interface DecomposeApplication {
348
348
  * ⓐ 같은 goal 을 «두 번» 쪼개지 않는다(alreadyDecomposed)
349
349
  * ⓑ 조각이 둘 미만인 제안은 애초에 안 온다(pickDecomposeProposals 가 거른다)
350
350
  * ⓒ 상한을 넘기면 «쪼개지 않는다» — 조각이 무한히 불어나는 것을 막는다
351
+ *
352
+ * ⭐ **조각은 원 골의 «대상 경로:» 머리줄을 물려받는다** (DECOMPOSE-PIECE-TARGET-LINE · 10-11 부검 run-76941fca):
353
+ * 조각 feature 가 골을 «통째로» 갈아 끼우면 첫 줄 선언이 사라져 Pod 예비 검사가 «대상 경로 0» →
354
+ * 재저작 NOT-GROUNDED 로 조각 전부가 exit 1 했다. 좁히는 규칙 = 원 선언 경로 중 조각 문장에
355
+ * 나오는 것만(없으면 원 선언 전부) · «신설:» 줄도 같은 규칙 · 첫 줄이 라벨이 아니면 지금 그대로.
351
356
  */
357
+
358
+ // ── 머리줄 선언을 읽고 좁히는 순수 조각 (위 규칙의 실물) ──────────────────────────
359
+ const TARGET_PATHS_LINE = /^대상 경로:[ \t]*(.*)$/;
360
+ const NEW_PATHS_LINE = /^신설:[ \t]*(.*)$/;
361
+
362
+ /** 선언 줄의 경로를 나눈다 — « · »·쉼표(`goal-new-paths` 와 같은 분리 규칙). 유효성은 걸러내지 않는다: 선언 그대로다. */
363
+ function splitDeclaredPaths(payload: string): string[] {
364
+ return payload.split(/[·,]/).map((token) => token.trim()).filter((token) => token.length > 0);
365
+ }
366
+
367
+ /** 조각 문장에 «나오는» 선언 경로만 남긴다. 하나도 안 나오면 원 선언 전부 — 선언 밖 경로는 지어내지 않는다. */
368
+ function narrowDeclaredPaths(declared: readonly string[], pieceFeature: string): string[] {
369
+ const mentioned = declared.filter((path) => {
370
+ const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
371
+ return new RegExp(`(^|[^\\w./-])${escaped}(?=$|[^\\w./-]|\\.(?:\\s|$))`).test(pieceFeature);
372
+ });
373
+ return mentioned.length ? mentioned : [...declared];
374
+ }
375
+
352
376
  export function applyDecomposeProposals(
353
377
  goals: readonly SelfDevGoal[],
354
378
  results: readonly SelfDevJobResult[],
@@ -374,11 +398,29 @@ export function applyDecomposeProposals(
374
398
  // ⛔ 상한을 넘기면 «쪼개지 않는다» — 조용히 자르지 않고 원본을 그대로 둔다.
375
399
  if (out.length + pieces.length + (goals.length - out.length - 1) > maxGoals) { out.push(goal); continue; }
376
400
  decomposed.push(goal.feature);
401
+ // 원 골의 머리줄 선언 — 첫 줄이 «대상 경로:» 가 아니면 조각은 지금 그대로(바이트 불변)다.
402
+ const goalLines = goal.feature.split('\n');
403
+ const targetLine = TARGET_PATHS_LINE.exec(goalLines[0] ?? '');
404
+ // «신설:» 상속은 머리줄 선언이 «있을 때만» — 첫 줄이 라벨이 아니면 조각은 바이트 불변이어야 한다.
405
+ const newLine = targetLine ? goalLines.map((line) => NEW_PATHS_LINE.exec(line)).find((m) => m !== null) : null;
377
406
  for (const piece of pieces) {
407
+ const inherited: string[] = [];
408
+ let paths: string[] = [];
409
+ if (targetLine) {
410
+ const declared = splitDeclaredPaths(targetLine[1]!);
411
+ paths = declared.length ? narrowDeclaredPaths(declared, piece.feature) : declared;
412
+ // 선언이 비어 있으면 원 줄을 그대로 실는다 — 라벨만이라도 예비 검사가 읽게 한다.
413
+ inherited.push(declared.length ? `대상 경로: ${paths.join(' · ')}` : goalLines[0]!);
414
+ }
415
+ if (newLine) {
416
+ const declaredNew = splitDeclaredPaths(newLine[1]!);
417
+ const narrowedNew = declaredNew.length ? narrowDeclaredPaths(declaredNew, piece.feature) : declaredNew;
418
+ inherited.push(declaredNew.length ? `신설: ${narrowedNew.join(' · ')}` : newLine[0]!);
419
+ }
378
420
  out.push({
379
421
  ...goal, // 승격 플래그(openPr·autoReview·autoMerge·base)를 물려받는다
380
422
  id: piece.id,
381
- feature: piece.feature,
423
+ feature: inherited.length ? [...inherited, piece.feature].join('\n') : piece.feature,
382
424
  ...(piece.dependsOn.length ? { dependsOn: [...piece.dependsOn] } : {}),
383
425
  // ⛔⭐ `...goal` 이 «부모의» hotPaths 를 그대로 흘린다 — 그러면 조각 전부가 서로 겹쳐
384
426
  // `orchestrate.ts` 가 전부를 직렬화한다(위상이 아니라 «한 줄»이 된다).
@@ -37,6 +37,8 @@ export const JOURNEY_NODE_IDS = [
37
37
  'salvage',
38
38
  'heal',
39
39
  'next-move',
40
+ 'ready',
41
+ 'pr-head-checkout',
40
42
  'stopped',
41
43
  ] as const;
42
44
  export type JourneyNodeId = typeof JOURNEY_NODE_IDS[number];
@@ -17,7 +17,7 @@ export interface LiveRunOverlapPath {
17
17
 
18
18
  /** 검사 대상 한 건이 막힌 이유. `name` 은 사람이 바로 열어 볼 수 있는 식별자다. */
19
19
  export interface LaunchPreflightBlocker {
20
- readonly kind: 'open-pr' | 'sibling-pr' | 'live-run' | 'no-target-paths' | 'recent-change' | 'ask-outside-path' | 'ask-marker';
20
+ readonly kind: 'open-pr' | 'sibling-pr' | 'live-run' | 'no-target-paths' | 'missing-traced-path' | 'goal-unreadable' | 'recent-change' | 'ask-outside-path' | 'ask-marker';
21
21
  /** `#8100` · `run-6ce5765b…` 처럼 «바로 조회 가능한» 이름. */
22
22
  readonly name: string;
23
23
  /** 왜 막혔는지 한 줄 — 경로와 수를 포함한다. */
@@ -490,6 +490,10 @@ export function renderLaunchPreflight(
490
490
  }
491
491
  lines.push(`[preflight] ⛔ 막는 것 ${result.blockers.length}건${forced ? '' : ' — 발사하지 않는다'}`);
492
492
  for (const blocker of result.blockers) lines.push(`[preflight] · ${blocker.name} — ${blocker.detail}`);
493
+ if (result.blockers.some((blocker) => blocker.kind === 'missing-traced-path' || blocker.kind === 'goal-unreadable')) {
494
+ lines.push('[preflight] 골 판독 실패 또는 traced path 오류는 --force-preflight 로 우회할 수 없다 — 골 문서와 경로를 바로잡아야 한다');
495
+ return lines.join('\n');
496
+ }
493
497
  lines.push(forced
494
498
  ? '[preflight] ⚠️ --force-preflight — 위 막힘을 «뚫고» 발사한다 (이 우회는 관측에 남는다)'
495
499
  : '[preflight] 그래도 가려면 --force-preflight (그 우회는 관측에 남는다) — elanous dev');