elanous 0.2.23 → 0.2.24

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 (347) 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/{272-b23d2e92161d11fd.js → 272-0eed0fb0aab716b6.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{3961-fbef064c213b2b18.js → 3961-aaae04ed1d32622f.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d5341ac96203a983.js → page-3b9fc39362473ae7.js} +1 -1
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f2b62c19ca68fef8.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/board/{page-55c631c032dcc47e.js → page-d7602d489edf5f37.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-ad3bd8084048bee4.js → page-24282bf9f90031a5.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/page-8b7c5ab6b5bc8d64.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5c5d0e370f495ef6.js → page-4eb84d454f8c3415.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-c325e21543cd48a8.js → page-b2e494bdad57fcb5.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/{page-6168a7c43e6ef8f5.js → page-c0ab229673a8b0eb.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-3e2b45f3c36dbac4.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/decisions/page-c22aa511b28743ad.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/page-26345242e4049f55.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/editor/{page-25bb425afdb27541.js → page-bf88c0f558d8ef54.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/exec/{page-32d72fdef1afe11a.js → page-52fec523e5b5d3ad.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/page-67723dfdcde92b31.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/inside/{page-2bd9ce9c020d02d4.js → page-22725e2570f1126b.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-a34096245e63b9f2.js → page-1891335950d68b70.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/layout-3267af1ebdc73cf5.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-4cc66d22ca4ec502.js → page-c314b75c11234998.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-83b14c729fcea837.js → page-9f9c85415f61782b.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-c1d4276059085587.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/page-833aab587f26ad4c.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-ccdb83ed40c8d724.js → page-8526c17ae38e6a50.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-7ad3de8fa06fdea9.js → page-f998637a8402115c.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-b7bb53828b17b335.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1b1850de0257e46d.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-786111f30b6f0969.js → page-6f23159fedb41f67.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-48fb31810693c357.js → page-1d809a87139dd024.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4e17cce5e2ed0fee.js → page-da9e570234d481fd.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-cb154acadb025414.js → page-b92caddb5c715f30.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/page-8ce3a38e210b810a.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-c2dd2b19c93f90ab.js → page-57ffeeb6019440fd.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-abbb417bf514630b.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-d3c007c8a0ddd7ee.js → page-2c2b3f29cec1efa4.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7837ce2aacb97b04.js → page-9369279f72fc7b51.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/page-64861f14039499c3.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-ddc4c93efd8ab94b.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/setup/page-901a1391b81053bc.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/share/page-68d9bbaac9841a58.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-119be9a0444d2482.js → page-b5c0c996f329371e.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-fa806c018833e332.js → page-89881ccbcf728978.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/term/page-0e87a803b3d7165e.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/today/{page-d431fbdea6ad93dd.js → page-58d4e5cb30ef4871.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-02ce4c3b5f39e4d2.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/page-e620d1bb8eadcec2.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-847c3de6ca8a4416.js → page-acf868c64d77f950.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/page-54fa605fa6dba457.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-74e69ba2c8e970d4.js → page-bc79f0833533fa84.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-70038f41ae7cf931.js → page-d5c4398eea03d8f3.js} +1 -1
  53. package/apps/pwa/out/_next/static/css/72becfd42807b50d.css +1 -0
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +14 -14
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +14 -14
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +14 -14
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +14 -14
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +14 -14
  64. package/apps/pwa/out/ceo/index.html +1 -1
  65. package/apps/pwa/out/ceo/index.txt +14 -14
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +14 -14
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +14 -14
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +14 -14
  72. package/apps/pwa/out/decisions/index.html +1 -1
  73. package/apps/pwa/out/decisions/index.txt +14 -14
  74. package/apps/pwa/out/design-check/index.html +1 -1
  75. package/apps/pwa/out/design-check/index.txt +14 -14
  76. package/apps/pwa/out/editor/index.html +1 -1
  77. package/apps/pwa/out/editor/index.txt +14 -14
  78. package/apps/pwa/out/exec/index.html +1 -1
  79. package/apps/pwa/out/exec/index.txt +14 -14
  80. package/apps/pwa/out/field/index.html +1 -1
  81. package/apps/pwa/out/field/index.txt +14 -14
  82. package/apps/pwa/out/healthz +1 -1
  83. package/apps/pwa/out/index.html +1 -1
  84. package/apps/pwa/out/index.txt +14 -14
  85. package/apps/pwa/out/inside/index.html +1 -1
  86. package/apps/pwa/out/inside/index.txt +14 -14
  87. package/apps/pwa/out/intake/index.html +1 -1
  88. package/apps/pwa/out/intake/index.txt +14 -14
  89. package/apps/pwa/out/live/index.html +1 -1
  90. package/apps/pwa/out/live/index.txt +14 -14
  91. package/apps/pwa/out/live-run/index.html +1 -1
  92. package/apps/pwa/out/live-run/index.txt +14 -14
  93. package/apps/pwa/out/loops/index.html +1 -1
  94. package/apps/pwa/out/loops/index.txt +14 -14
  95. package/apps/pwa/out/market/index.html +1 -1
  96. package/apps/pwa/out/market/index.txt +14 -14
  97. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  98. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  99. package/apps/pwa/out/morning/index.html +1 -1
  100. package/apps/pwa/out/morning/index.txt +14 -14
  101. package/apps/pwa/out/observatory/index.html +1 -1
  102. package/apps/pwa/out/observatory/index.txt +14 -14
  103. package/apps/pwa/out/ops/checklist/index.html +1 -1
  104. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  105. package/apps/pwa/out/ops/release/index.html +1 -1
  106. package/apps/pwa/out/ops/release/index.txt +14 -14
  107. package/apps/pwa/out/ops/seats/index.html +1 -1
  108. package/apps/pwa/out/ops/seats/index.txt +14 -14
  109. package/apps/pwa/out/outputs/index.html +1 -1
  110. package/apps/pwa/out/outputs/index.txt +14 -14
  111. package/apps/pwa/out/reflection/index.html +1 -1
  112. package/apps/pwa/out/reflection/index.txt +15 -15
  113. package/apps/pwa/out/scheduler/index.html +1 -1
  114. package/apps/pwa/out/scheduler/index.txt +14 -14
  115. package/apps/pwa/out/sessions/index.html +1 -1
  116. package/apps/pwa/out/sessions/index.txt +14 -14
  117. package/apps/pwa/out/settings/devices/index.html +1 -1
  118. package/apps/pwa/out/settings/devices/index.txt +14 -14
  119. package/apps/pwa/out/settings/index.html +1 -1
  120. package/apps/pwa/out/settings/index.txt +14 -14
  121. package/apps/pwa/out/setup/done/index.html +1 -1
  122. package/apps/pwa/out/setup/done/index.txt +14 -14
  123. package/apps/pwa/out/setup/index.html +1 -1
  124. package/apps/pwa/out/setup/index.txt +14 -14
  125. package/apps/pwa/out/share/index.html +1 -1
  126. package/apps/pwa/out/share/index.txt +14 -14
  127. package/apps/pwa/out/showroom/index.html +1 -1
  128. package/apps/pwa/out/showroom/index.txt +14 -14
  129. package/apps/pwa/out/tasks/index.html +1 -1
  130. package/apps/pwa/out/tasks/index.txt +14 -14
  131. package/apps/pwa/out/term/index.html +1 -1
  132. package/apps/pwa/out/term/index.txt +14 -14
  133. package/apps/pwa/out/today/index.html +1 -1
  134. package/apps/pwa/out/today/index.txt +14 -14
  135. package/apps/pwa/out/trace/index.html +1 -1
  136. package/apps/pwa/out/trace/index.txt +14 -14
  137. package/apps/pwa/out/vault/index.html +1 -1
  138. package/apps/pwa/out/vault/index.txt +14 -14
  139. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  140. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  141. package/apps/pwa/out/workflows/index.html +1 -1
  142. package/apps/pwa/out/workflows/index.txt +14 -14
  143. package/apps/pwa/out/workspace/index.html +1 -1
  144. package/apps/pwa/out/workspace/index.txt +14 -14
  145. package/apps/pwa/out/worktrees/index.html +1 -1
  146. package/apps/pwa/out/worktrees/index.txt +14 -14
  147. package/catalog/models/openrouter/deepseek-v4-pro-0813.yaml +39 -0
  148. package/catalog/models/openrouter/deepseek-v4.1-flash.yaml +38 -0
  149. package/catalog/models/openrouter/glm-5.3-flash.yaml +35 -0
  150. package/catalog/providers/openrouter.yaml +2 -0
  151. package/graphs/implement-loop.yaml +31 -0
  152. package/graphs/ops/harness-gc.yaml +17 -0
  153. package/graphs/ops/recipes.yaml +3 -0
  154. package/graphs/overlays/README.md +1 -1
  155. package/graphs/overlays/stop-heal-ladder.yaml +105 -0
  156. package/package.json +1 -1
  157. package/scripts/add-api-key.sh +1 -1
  158. package/scripts/ci-typecheck-changed.ts +71 -14
  159. package/scripts/fleet/bedrock-pod-secret.ts +122 -0
  160. package/scripts/fleet/node-setup-linux.sh +97 -0
  161. package/scripts/fleet/node-setup.sh +11 -0
  162. package/scripts/fleet/zai-pod-secret.ts +86 -0
  163. package/scripts/land-global-checks.ts +3 -0
  164. package/scripts/measure-log-diet-2.ts +213 -0
  165. package/scripts/measure-tsc-affected-roots.ts +289 -0
  166. package/scripts/release-loop/gate-executor.ts +28 -0
  167. package/scripts/release-loop/gate-node.ts +262 -26
  168. package/scripts/release-loop/resume-release.ts +9 -3
  169. package/scripts/release-loop/resume.ts +41 -0
  170. package/scripts/release-loop/unattended-release.ts +22 -7
  171. package/scripts/tsc-baseline-cache.ts +147 -0
  172. package/scripts/usage-rollup.ts +58 -6
  173. package/src/agent/run-context.ts +10 -0
  174. package/src/agent-mission/driver.ts +18 -3
  175. package/src/agent-mission/resource-ladder.ts +8 -3
  176. package/src/agent-mission/review-loop.ts +8 -2
  177. package/src/agent-substrate/acp-implementer.ts +139 -0
  178. package/src/agent-substrate/pr-reviewer.ts +31 -4
  179. package/src/agent-substrate/self-review-cli.ts +48 -9
  180. package/src/autopilot/mission-critique.ts +18 -5
  181. package/src/autopilot/mission-feasibility-negotiate.ts +11 -10
  182. package/src/autopilot/mission-trace.ts +12 -1
  183. package/src/autopilot/system-lookback-run.ts +16 -10
  184. package/src/bench/bench.ts +183 -0
  185. package/src/budget/fetchers/grok.ts +15 -5
  186. package/src/budget/llm-cost.ts +37 -5
  187. package/src/budget/types.ts +8 -2
  188. package/src/budget/unified-usage.ts +88 -6
  189. package/src/cli/bench-cli.ts +34 -0
  190. package/src/cli/guardian-cli.ts +26 -1
  191. package/src/cli/image-cli.ts +101 -0
  192. package/src/cli/log-event-names.ts +4 -0
  193. package/src/cli/pr-cli.ts +62 -5
  194. package/src/cli/pr-land-overlap-evidence.ts +160 -12
  195. package/src/cli/release-cli.ts +18 -9
  196. package/src/cli/rfc-status.ts +52 -2
  197. package/src/cli/self-update.ts +104 -0
  198. package/src/cli/tasks-cli.ts +109 -4
  199. package/src/cli/usage-cli.ts +91 -8
  200. package/src/core-turn/run-goal-loop.ts +5 -4
  201. package/src/dashboard/ad-run-setup-bindings.ts +279 -0
  202. package/src/dashboard/index.ts +18 -276
  203. package/src/dashboard/stale-install-check.ts +11 -0
  204. package/src/debug/log-diet.ts +38 -7
  205. package/src/debug/log.ts +19 -1
  206. package/src/domains/release-tool.ts +39 -0
  207. package/src/error-report/report.ts +21 -4
  208. package/src/github/pr-labels.ts +1 -0
  209. package/src/graph-runner/graph-cli.ts +42 -1
  210. package/src/graph-runner/runner.ts +167 -6
  211. package/src/grok/usage.ts +23 -2
  212. package/src/grounding/ref-index.ts +253 -0
  213. package/src/harness/dispatch-backpressure.ts +10 -2
  214. package/src/harness/harness-clean.ts +199 -78
  215. package/src/harness/harness-cli-command.ts +122 -7
  216. package/src/harness/harness-gc.ts +331 -0
  217. package/src/harness/harness-stop.ts +4 -2
  218. package/src/harness/harness-substrate-default.ts +142 -7
  219. package/src/harness/launch-registry.ts +6 -0
  220. package/src/harness/worktree-branch-prefix.ts +9 -0
  221. package/src/hq/fence.ts +187 -0
  222. package/src/hq/seats.ts +38 -22
  223. package/src/images/image-ledger.ts +179 -0
  224. package/src/index.ts +93 -11
  225. package/src/instance/child-scope.ts +32 -1
  226. package/src/instance/dev-project.ts +133 -0
  227. package/src/instance/resolve.ts +24 -0
  228. package/src/instance/spawn-elanous.ts +45 -0
  229. package/src/intake-plane/ref-prefetch.ts +110 -0
  230. package/src/intelligence-map/model-catalog.ts +72 -0
  231. package/src/llm/bedrock-eventstream.ts +215 -0
  232. package/src/llm/bedrock-provider.ts +114 -41
  233. package/src/llm/bedrock.ts +174 -28
  234. package/src/llm/openrouter-byok-cost.ts +96 -0
  235. package/src/llm/openrouter-deepseek.ts +151 -0
  236. package/src/llm/openrouter-upstream-pins.config.ts +25 -0
  237. package/src/llm/zai-coding-plan.ts +700 -0
  238. package/src/llm.ts +87 -16
  239. package/src/loops/checker.ts +148 -9
  240. package/src/loops/orchestrator/resync-shadow.ts +34 -7
  241. package/src/maturity/feature-maturity.ts +2 -2
  242. package/src/model-tier/llm-tier-map.ts +9 -5
  243. package/src/models/prompts.ts +2 -1
  244. package/src/mss/logging/log-store.ts +58 -11
  245. package/src/nexus/api/missions.ts +73 -2
  246. package/src/nexus/api/ops-api.ts +18 -0
  247. package/src/nexus/index.ts +7 -75
  248. package/src/nexus/tools-kind.ts +78 -0
  249. package/src/oauth/codex-account-rotation.ts +39 -2
  250. package/src/oauth/codex-account-store.ts +323 -24
  251. package/src/oauth/fallback-chain.ts +53 -15
  252. package/src/onboarding/first-run.ts +37 -18
  253. package/src/onboarding/web-first.ts +20 -2
  254. package/src/ops/machine-heartbeat.ts +185 -0
  255. package/src/ops/overview-cache.ts +175 -0
  256. package/src/ops/overview.ts +8 -0
  257. package/src/prompt-cache/openai.ts +3 -0
  258. package/src/prompt-cache/types.ts +5 -0
  259. package/src/prompt-enhance/enhance.ts +23 -0
  260. package/src/release-loop/placement.ts +16 -1
  261. package/src/requirements/requirement-ledger.ts +536 -0
  262. package/src/retro/daily.ts +5 -5
  263. package/src/seat-loop/seat-loop.ts +32 -10
  264. package/src/self-dev/dev-cli.ts +44 -10
  265. package/src/self-dev/dev-pipeline.ts +7 -2
  266. package/src/self-dev/draft-residue.ts +14 -2
  267. package/src/self-dev/draft-sweep.ts +103 -0
  268. package/src/self-dev/graph-journey-route.ts +6 -0
  269. package/src/self-dev/orchestrate-cli.ts +25 -3
  270. package/src/self-dev/orchestrate.ts +41 -6
  271. package/src/self-dev/run-supervisor.ts +188 -15
  272. package/src/self-dev/run-types.ts +13 -0
  273. package/src/self-dev/supervisor-verdict-edges.ts +1 -0
  274. package/src/self-implement/budget-gate.ts +53 -13
  275. package/src/self-implement/child-context-composition.ts +139 -0
  276. package/src/self-implement/child-llm-preference.ts +13 -5
  277. package/src/self-implement/claude-acp-implement.ts +67 -0
  278. package/src/self-implement/flex-signals.ts +59 -0
  279. package/src/self-implement/frozen-merges.ts +8 -3
  280. package/src/self-implement/goal-author-decompose.ts +10 -2
  281. package/src/self-implement/goal-author.ts +24 -9
  282. package/src/self-implement/graph-overlay-condition.ts +28 -0
  283. package/src/self-implement/graph-overlay-yaml.ts +41 -7
  284. package/src/self-implement/graph-templates.ts +33 -1
  285. package/src/self-implement/graph-yaml.ts +56 -0
  286. package/src/self-implement/heal-intake.ts +5 -1
  287. package/src/self-implement/orchestrator.ts +144 -15
  288. package/src/self-implement/running-runs.ts +75 -5
  289. package/src/self-implement/seams.ts +47 -1
  290. package/src/self-implement/self-implement-cli.ts +9 -4
  291. package/src/self-implement/stop-autoheal.ts +69 -2
  292. package/src/session-runtime/retry-policy.ts +2 -1
  293. package/src/steward/launch.ts +5 -4
  294. package/src/steward/triage-budget.ts +62 -0
  295. package/src/steward/triage.ts +83 -22
  296. package/src/task-agent/actions.ts +62 -5
  297. package/src/task-agent/completion-evidence.ts +5 -0
  298. package/src/task-agent/cover.ts +101 -3
  299. package/src/task-agent/judge.ts +16 -0
  300. package/src/task-agent/land-order-ledger.ts +95 -0
  301. package/src/task-agent/live-move-history.ts +87 -1
  302. package/src/task-agent/live-moves.ts +350 -20
  303. package/src/task-agent/rejudge-on-head.ts +75 -20
  304. package/src/task-agent/review-prior-fix.ts +146 -0
  305. package/src/task-agent/run-adopt.ts +136 -0
  306. package/src/task-agent/shadow.ts +53 -9
  307. package/src/task-agent/task-hand.ts +54 -7
  308. package/src/task-orchestrator/surfaces/pod-account-broker.ts +38 -5
  309. package/src/task-orchestrator/surfaces/pod-bedrock-child.ts +74 -0
  310. package/src/task-orchestrator/surfaces/pod-child-default.ts +50 -0
  311. package/src/task-orchestrator/surfaces/pod-child-providers.ts +14 -1
  312. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +79 -6
  313. package/src/task-orchestrator/surfaces/pod-lease.ts +17 -5
  314. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  315. package/src/task-orchestrator/surfaces/pod-pool.ts +20 -4
  316. package/src/task-orchestrator/surfaces/pod-zai-child.ts +101 -0
  317. package/src/task-orchestrator/surfaces/self-implement-pod.ts +213 -23
  318. package/src/task-orchestrator/surfaces/self-implement.ts +19 -1
  319. package/src/typecheck-ratchet.ts +1 -0
  320. package/src/user-config.ts +51 -11
  321. package/src/version/packed-revision.json +1 -1
  322. package/src/workflow-runtime/triggers/registry.ts +59 -0
  323. package/src/workflow-runtime/triggers/webhook-source.ts +35 -5
  324. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-0160ab83c8685a12.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/bots/page-e6784759a2531031.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/control/page-31417d3604154a6d.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/decisions/page-e9606dc0bdec9305.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/design-check/page-a648bc464c9dc754.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/field/page-d27b794c8ba76bbb.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/layout-22170f4f00a15496.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/loops/page-6e2781c523dad761.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/market/page-26dd415c8f13f341.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/not-found-015c02c4fec4023e.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6b0ae5b5c42f6d2f.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/page-16258e956ea93fc2.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f35dd1dd96ae87.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/settings/page-7350ee93f4fe1033.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-00723458575f2b19.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/setup/page-745d9b6c015076f8.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/share/page-736d33c7414a8a1a.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/term/page-c782041b5f5199ba.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/trace/page-0c056abff82b9891.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/vault/page-242d668e4fbf72c4.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/workflows/page-81dd0d8ae923ed72.js +0 -1
  345. package/apps/pwa/out/_next/static/css/5619d59e87eddf66.css +0 -1
  346. /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_buildManifest.js +0 -0
  347. /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_ssgManifest.js +0 -0
@@ -7,21 +7,29 @@
7
7
  * 회수한 pass 는 그 틱의 판단부·land 관문 입력(`selfReview`)이 될 뿐 land 판정 로직은 그대로다.
8
8
  * - `propose-green`: 카드에 green «제안»(`greenProposal`)만 적는다 — 체크리스트는 손대지 않는다.
9
9
  * - `propose-land`: 카드·현재 머리의 self review 통과(pass «또는» warn ⊕ must-fix 0 · TA-LAND-WARN-MUSTFIX0)·non-draft 를 확인하고 기존 `pr land` 를 머리 고정으로 한 번 부른다.
10
+ * - 사다리 R4: `narrow-relaunch` · `wait-retry` · `salvage-relaunch` · `alternative` 도 허용 목록 어휘다 — 실제 발사는 실행부
11
+ * `executeNextAction`(`actions.ts`)이 «목록 ⊕ 같은 수 2회 상한 ⊕ 회당 1건 ⊕ 부하 관문»을 모두 넘을 때만 한다.
12
+ * - `decision-card`: 막힌 과제를 «근거(서명 · 화면 · must-fix)» 붙은 결정 카드로 올린다 — 근거가 하나도 없으면 카드를 올리지 않는다.
13
+ * 카드는 종전 실행부와 같은 `decisions raise` 모양이라 OP 거르기(`seat-loop/op-decide.ts`)가 그대로 본다 · 같은 근거는 카드 history 로 한 번만.
10
14
  * - 설정 `taskAgent.liveMoves`(config > env `ELANOUS_TASK_AGENT_LIVE_MOVES` > 없음). 모르는 항목은 경고하고 버린다.
11
15
  * - 관측: 실행한 수마다 `debug.log('task-agent', 'live-move', {kind, card, ok, detail, live:true})`.
12
16
  */
13
17
  import { spawn, spawnSync } from 'node:child_process';
14
- import { closeSync, mkdirSync, openSync, statSync } from 'node:fs';
18
+ import { createHash } from 'node:crypto';
19
+ import { closeSync, mkdirSync, openSync, readFileSync, statSync } from 'node:fs';
15
20
  import { dirname, isAbsolute, resolve } from 'node:path';
16
21
  import { debug } from '../debug/log.js';
17
22
  import { findGitDir } from '../git-fs/locate.js';
18
- import { getUserConfig } from '../user-config.js';
23
+ import { getUserConfig, userConfigPath } from '../user-config.js';
19
24
  import type { DetachedSpawn } from '../cli/tasks-cli.js';
20
- import { taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
25
+ import { LIVE_MOVE_HISTORY_EVENT, taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
26
+ import { LIVE_RETRY_MOVES } from './judge.js';
27
+ import { decisionCardGrounds, decisionRaiseArgs, type DecisionCardEvidence, type NextAction } from './actions.js';
21
28
  import { reviewResultPath } from './review-result-capture.js';
29
+ import { buildPriorFixContext, PRIOR_FIX_SCOPE_LINE, priorFixBlock, priorFixObservation, type PriorDiffReader, type PriorFixContext } from './review-prior-fix.js';
22
30
  import { createLandWorktree, localHead, observeLandWorktree, type LandWorktree, type LandWorktreeFailure, type LandWorktreeInput, type LandWorktreeRemoval } from './land-worktree.js';
23
31
 
24
- export const TASK_AGENT_LIVE_MOVES = ['review', 'propose-green', 'propose-land'] as const;
32
+ export const TASK_AGENT_LIVE_MOVES = ['review', 'propose-green', 'propose-land', 'decision-card', ...LIVE_RETRY_MOVES] as const;
25
33
  export type TaskAgentLiveMove = typeof TASK_AGENT_LIVE_MOVES[number];
26
34
  export const TASK_AGENT_LIVE_MOVES_ENV = 'ELANOUS_TASK_AGENT_LIVE_MOVES';
27
35
 
@@ -70,11 +78,15 @@ export interface LiveMoveDeps {
70
78
  * TA-REVIEW-RESULT-CAPTURE — `capture.resultPath` 가 오면 자식 `--json` stdout 을 그 파일로 받는다(회수 = `review-result-capture.ts`).
71
79
  */
72
80
  requestReview?: (pr: number, intent: string, cwd?: string, capture?: { resultPath: string }) => Promise<void>;
81
+ /** REVIEW-CONVERGE-PRIOR-FIX — 같은 PR 재리뷰의 직전 머리 → 지금 머리 diff(시험 주입 · 기본 = `git diff --numstat`). */
82
+ priorDiff?: PriorDiffReader;
73
83
  /** Fresh PR head and draft flag for landing; separate from the unchanged review lookup. */
74
84
  landPrHead?: (pr: number, cwd: string) => Promise<(PrHeadView & { isDraft: boolean }) | null>;
75
85
  /** One existing pr land operation; inject in tests instead of invoking the CLI. */
76
86
  /** TA-REJUDGE-ON-HEAD — `opts.overlapEvidence` 가 있으면 `pr land --overlap-evidence <file>` 로 넘긴다(없으면 종전 인자). */
77
87
  land?: (pr: number, head: string, cwd: string, opts?: { overlapEvidence?: string }) => Promise<{ status: number; stdout: string; stderr?: string }>;
88
+ /** `decision-card` — `decisions raise` 한 번(시험 주입 · 기본 = 같은 우주의 CLI 자식). */
89
+ raiseDecision?: (args: string[]) => Promise<{ status: number; stdout: string; stderr?: string }>;
78
90
  log?: (category: string, event: string, data: Record<string, unknown>) => void;
79
91
  /** TA-LAND-WORKTREE — cwd HEAD 읽기 · PR 머리 임시 워크트리 만들기(시험 주입 · 기본 = git). */
80
92
  landWorktree?: { localHead?: (cwd: string) => string | null; create?: (input: LandWorktreeInput) => Promise<LandWorktree | LandWorktreeFailure> };
@@ -87,6 +99,8 @@ export interface LiveMoveResult {
87
99
  reviewRepo?: { resolved: boolean; source?: ReviewRepoCandidate['source']; cwd?: string; proof?: 'head-sha' | 'branch'; reason: string };
88
100
  /** TA-REVIEW-RESULT-CAPTURE — 띄운 review 의 결과 파일(자식 stdout) — 실행한 review 에만 실린다. */
89
101
  resultPath?: string;
102
+ /** TA-LAND-RETRY-ON-INFRA — 실행한 land 의 그 머리 몇 번째 시도인가 · 실패면 갈래(infra|judgement|base-stale)·사유·재시도 가능 여부. */
103
+ landAttempt?: { attempt: number; class?: LandFailureClass; reason?: string; retryAllowed?: boolean };
90
104
  }
91
105
 
92
106
  export interface PrHeadView { head: string; state: string; branch?: string; url?: string }
@@ -242,11 +256,13 @@ export async function defaultLand(pr: number, head: string, cwd: string, opts: {
242
256
  child.once('error', reject);
243
257
  child.once('close', (code) => {
244
258
  if (code !== 0) { resolveResult({ status: code ?? 1, stdout, stderr }); return; }
259
+ // TA-LAND-RETRY-ON-INFRA — 왜 미확인인지 «마지막 줄»로 가른다(꼬리 300자에서도 남게): 못 읽음 = 인프라 · 읽었는데 MERGED 아님 = 판정.
260
+ const unconfirmed = (line: string, before = stderr || stdout || 'landing completed without confirmed merge') => ({ status: 1, stdout, stderr: `${before.slice(-(299 - line.length))}\n${line}` });
245
261
  void defaultLandPrHead(pr, cwd).then((view) => {
246
262
  resolveResult(view?.state === 'MERGED'
247
263
  ? { status: 0, stdout, stderr }
248
- : { status: 1, stdout, stderr: (stderr || stdout || 'landing completed without confirmed merge').slice(-300) });
249
- }).catch((error) => resolveResult({ status: 1, stdout, stderr: String(error).slice(-300) }));
264
+ : unconfirmed(view ? `✗ ta-land: pr land exited 0 but PR is ${view.state}` : '✗ ta-land: merge state unreadable after pr land exit 0'));
265
+ }).catch((error) => resolveResult(unconfirmed('✗ ta-land: merge state unreadable after pr land exit 0', String(error))));
250
266
  });
251
267
  });
252
268
  }
@@ -269,13 +285,17 @@ export async function defaultLandPrHead(pr: number, cwd: string): Promise<(PrHea
269
285
  } catch { return null; }
270
286
  }
271
287
 
272
- export function liveReviewIntent(card: TaskCard, runId: string | null, stopReason: string): string {
288
+ /**
289
+ * REVIEW-CONVERGE-PRIOR-FIX — `prior` 는 같은 PR 의 두 번째 이후 리뷰에만 온다: «직전 반영분»에 직전 머리 범위·diff·must-fix 를,
290
+ * 범위 경계에 «바뀌지 않은 줄의 새 must-fix 는 근거 줄» 한 줄을 붙인다. 없으면(첫 리뷰) 종전 문면 바이트 그대로.
291
+ */
292
+ export function liveReviewIntent(card: TaskCard, runId: string | null, stopReason: string, prior?: PriorFixContext | null): string {
273
293
  const goal = card.text.split('\n')[0]!.trim().slice(0, 300);
274
294
  return [
275
295
  `목표: ${goal}`,
276
296
  `수용기준: 과제 카드 ${card.id} 원문 —\n${card.text.trim().slice(0, 4000)}`,
277
- `직전 반영분: 런 ${runId ?? '-'} 이 ${stopReason} 로 멈춤 — 수확 가능`,
278
- '의도적 범위 경계: task-agent live review 요청(TA-JUDGE-LIVE-SAFE) — 리뷰만 한다 · 착지·재발사는 이 요청의 결정이 아니다',
297
+ `직전 반영분: 런 ${runId ?? '-'} 이 ${stopReason} 로 멈춤 — 수확 가능${prior ? `\n${priorFixBlock(prior)}` : ''}`,
298
+ `의도적 범위 경계: task-agent live review 요청(TA-JUDGE-LIVE-SAFE) — 리뷰만 한다 · 착지·재발사는 이 요청의 결정이 아니다${prior ? `\n${PRIOR_FIX_SCOPE_LINE}` : ''}`,
279
299
  ].join('\n');
280
300
  }
281
301
 
@@ -321,18 +341,25 @@ export async function executeLiveReview(
321
341
  const at = (deps.now ?? (() => new Date()))().toISOString();
322
342
  // TA-REVIEW-RESULT-CAPTURE — 머리마다 한 번이라 경로도 머리마다 하나다. 요청 칸에 먼저 적는다(회수가 이 칸을 본다).
323
343
  const resultPath = reviewResultPath(path, card.id, pr, head.head);
324
- const claim = { claimed: false, missing: false };
344
+ const claim: { claimed: boolean; missing: boolean; before?: TaskCard } = { claimed: false, missing: false };
325
345
  updateTaskCard(path, card.id, (current) => {
326
346
  if (!current) { claim.missing = true; return undefined; }
327
347
  // 머리 «이력»으로 본다 — A→B→A 로 돌아와도 A 는 다시 요청하지 않는다.
328
348
  if ((current.reviewRequests ?? []).some((entry) => entry.pr === pr && entry.head === head!.head)) return undefined;
329
349
  claim.claimed = true;
350
+ claim.before = current;
330
351
  return { ...current, reviewRequests: [...(current.reviewRequests ?? []), { pr, head: head!.head, at, resultPath }] };
331
352
  });
332
353
  if (claim.missing) return observe(deps, withRepo({ kind, executed: false, card: card.id, ok: false, detail: 'task card not in state file' }));
333
354
  if (!claim.claimed) return observe(deps, withRepo({ kind, executed: false, card: card.id, ok: true, detail: `already requested for #${pr} head ${head.head.slice(0, 12)}` }));
334
355
  try {
335
- const intent = liveReviewIntent(card, ctx.runId, ctx.stopReason);
356
+ // REVIEW-CONVERGE-PRIOR-FIX — 같은 PR 의 직전 요청(잠금 안에서 본 이력)이 있으면 그 라운드를 intent 에 싣는다 · 맥락을 못 만들면 종전 문면.
357
+ let prior: PriorFixContext | null = null;
358
+ try { prior = claim.before ? buildPriorFixContext(claim.before, pr, head.head, { ...(cwd ? { cwd } : {}), ...(deps.priorDiff ? { priorDiff: deps.priorDiff } : {}) }) : null; } catch { prior = null; }
359
+ if (prior) {
360
+ try { (deps.log ?? ((c, e, d) => debug.log(c, e, d)))('task-agent', 'review-prior-fix-filled', priorFixObservation(card.id, pr, prior)); } catch { /* fail-soft */ }
361
+ }
362
+ const intent = liveReviewIntent(card, ctx.runId, ctx.stopReason, prior);
336
363
  await (deps.requestReview ? deps.requestReview(pr, intent, cwd, { resultPath }) : defaultRequestReview(pr, intent, cwd, { resultPath }));
337
364
  } catch (error) {
338
365
  const reason = error instanceof Error ? error.message : String(error);
@@ -344,13 +371,155 @@ export async function executeLiveReview(
344
371
  return observe(deps, withRepo({ kind, executed: true, card: card.id, ok: true, detail: `review requested for #${pr} head ${head.head.slice(0, 12)}`, resultPath }));
345
372
  }
346
373
 
374
+ /**
375
+ * TA-LAND-RETRY-ON-INFRA — land 실패를 둘로 가른다.
376
+ *
377
+ * - 왜: «같은 머리에 land 한 번»이 TA 쪽 인프라 결손(호스트에 PR 머리 트리가 없어 `✗ expected-head: … matching local HEAD and clean
378
+ * worktree required`)에 그 한 번을 써 버렸고, 결손을 고친(#26037) 뒤에도 같은 머리는 `already attempted` 로 막혀 사람이 머리를
379
+ * 흔들어야 했다(2026-10-11 #26030 · 카드 ta-20261010-c6b53b).
380
+ * - `infra` — 아래 «명시적 허용 목록»에 있는 사유만. 같은 머리에 한 번 더(머리당 총 `LAND_ATTEMPTS_PER_HEAD_ON_INFRA`).
381
+ * - `judgement` — 그 밖 전부(must-fix · 겹침 관문 · 머리 바뀜 · 동결 · 게이트 · 충돌 · 목록에 없는 ✗ 줄 · 기록 없는 옛 시도) — 종전대로 한 번 끝(fail-closed).
382
+ * - 판독 근거: `pr land` 의 실제 출력 줄(`src/cli/pr-cli.ts` 의 `✗ <관문>:` 문면 그대로)과 `executeLiveLand` 의 사유.
383
+ * - 임시 워크트리 «생성 실패»는 여기 오지 않는다 — #26037 이 land 를 부르지 않고 시도 기록을 «되돌린다»(시도를 안 쓴다).
384
+ * - `base-stale` — REJUDGE base-stale(OVERLAP-EVIDENCE-BASE-WINDOW 짝 · #26060): 겹침 증거의 게이트 base 뒤에 main 이 «PR 파일을 건드리며»
385
+ * 움직였다(`✗ overlap-evidence: base-stale-overlap: …` ⊕ 딸린 `✗ expected-head: overlap merge approval unavailable …` 줄 «만»).
386
+ * 같은 머리 «즉시» 재시도는 없다(`retryAllowed:false`) — 재판정 틱이 새 base 위 재게이트를 청하고, 그 재게이트가 «통과한 뒤»
387
+ * (`executeLiveLand` ctx `gateAt` = 그 재게이트 요청 시각)에만 이 시도는 «소비됨»으로 빠진다. judgement·infra 와 섞이지 않는다.
388
+ */
389
+ export const LAND_ATTEMPTS_PER_HEAD_ON_INFRA = 2;
390
+ export type LandFailureClass = 'infra' | 'judgement' | 'base-stale';
391
+
392
+ /** `pr land` 출력 줄 → 인프라 사유. ⛔ 줄 «전체»가 맞아야 한다 — 비슷한 문면을 넓게 받지 않는다. */
393
+ export const LAND_INFRA_OUTPUT_REASONS: ReadonlyArray<{ reason: string; line: RegExp }> = [
394
+ // 머리 고정 사전 조건 — gh 로 본 PR(OPEN·non-draft·머리)은 executeLiveLand 가 «직전에» 확인했다 · 남는 원인은 로컬 HEAD·더러운 트리·gh 조회 실패.
395
+ { reason: 'precondition-local-head-clean-tree', line: /^✗ expected-head: PR OPEN non-draft, matching local HEAD and clean worktree required$/ },
396
+ { reason: 'precondition-branch', line: /^✗ branch: (?:PR head branch와 현재 브랜치가 다릅니다\.|현재 브랜치를 해석하지 못했습니다\.)$/ },
397
+ { reason: 'base-unresolved', line: /^✗ base: 비교 base를 선택하지 못했습니다\.$/ },
398
+ { reason: 'git-read-failed', line: /^✗ granularity: (?:비교 base를 해석하지 못했습니다\.|로컬 git log 를 읽지 못했습니다\.)$/ },
399
+ { reason: 'gh-lookup-failed', line: /^✗ find: / },
400
+ { reason: 'gh-merge-state-unknown', line: /^✗ merge: squash merge (?:결과를 모름 \(상태 미확정\)|상태 되읽기 실패)$/ },
401
+ // defaultLand — `pr land` 가 0 으로 끝났는데 병합 상태를 gh 로 다시 못 읽었다(병합됐으면 다음 시도는 MERGED 로 관문 앞에서 멈춘다).
402
+ { reason: 'gh-merge-state-unreadable', line: /^✗ ta-land: merge state unreadable after pr land exit 0$/ },
403
+ ];
404
+ /** 인프라 사유에 «딸려 나오는» 줄 — 그 사유가 같이 있을 때만 판정 거절로 세지 않는다. */
405
+ const LAND_INFRA_CONSEQUENCE_LINES: ReadonlyArray<{ of: string; line: RegExp }> = [
406
+ { of: 'gh-lookup-failed', line: /^✗ upsert-ready: gh: pinned PR is not the branch open PR$/ },
407
+ ];
408
+ /** REJUDGE base-stale — 겹침 증거의 게이트 base 가 낡았고 그 사이 착지가 PR 파일을 건드렸다(`pr land` · #26060 문면). */
409
+ const LAND_BASE_STALE_LINE = /^✗ overlap-evidence: (base-stale-overlap: .*)$/;
410
+ /** base-stale 에 «딸려 나오는» 줄 — 증거가 거절되면 `pr land` 가 이어서 낸다(그 줄만 있으면 판정 거절로 세지 않는다). */
411
+ const LAND_BASE_STALE_CONSEQUENCE_LINES: readonly RegExp[] = [/^✗ expected-head: overlap merge approval unavailable for the reviewed head$/];
412
+ /** 실패가 아닌 ✗ 줄 — `pr land` 가 «계속합니다»로 넘어간다(판정에 쓰지 않는다). */
413
+ const LAND_NON_FATAL_LINES: readonly RegExp[] = [/^✗ active-run-files: .*계속합니다\.?$/];
414
+ /** executeLiveLand 쪽 인프라 사유(출력 줄이 아니라 실행 사실). */
415
+ export const LAND_INFRA_EXECUTOR_REASONS = ['land-process-error', 'land-no-result-line'] as const;
416
+ const [LAND_PROCESS_ERROR, LAND_NO_RESULT_LINE] = LAND_INFRA_EXECUTOR_REASONS;
417
+
418
+ const RESULT_MARK = /^(?:\[dry-run\] )?[✓✗⚠⏳]/u;
419
+
420
+ export interface LandFailureClassification { class: LandFailureClass; reason: string }
421
+
422
+ /**
423
+ * 실패한 land 의 출력(또는 던진 오류)을 가른다 — 목록에 없는 ✗ 줄이 «하나라도» 있으면 판정 거절이다.
424
+ * `thrown` = land 를 띄우거나 기다리다 던졌다(프로세스 오류).
425
+ */
426
+ export function classifyLandFailure(outcome: { status: number; stdout?: string; stderr?: string } | { thrown: string }): LandFailureClassification {
427
+ if ('thrown' in outcome) return { class: 'infra', reason: LAND_PROCESS_ERROR };
428
+ const lines = `${outcome.stdout ?? ''}\n${outcome.stderr ?? ''}`.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
429
+ const marked = lines.filter((line) => RESULT_MARK.test(line));
430
+ // 결과 줄이 «하나도» 없이 0 아닌 코드로 끝났다 — 관문까지 못 간 채 죽었다(크래시 · 신호).
431
+ if (!marked.length) return outcome.status !== 0 ? { class: 'infra', reason: LAND_NO_RESULT_LINE } : { class: 'judgement', reason: 'no-failure-line' };
432
+ const failures = marked.map((line) => line.replace(/^\[dry-run\] /, '')).filter((line) => line.startsWith('✗') && !LAND_NON_FATAL_LINES.some((re) => re.test(line)));
433
+ const infra = new Set<string>();
434
+ const unlisted: string[] = [];
435
+ for (const line of failures) {
436
+ const hit = LAND_INFRA_OUTPUT_REASONS.find((entry) => entry.line.test(line));
437
+ if (hit) infra.add(hit.reason); else unlisted.push(line);
438
+ }
439
+ const judged = unlisted.filter((line) => !LAND_INFRA_CONSEQUENCE_LINES.some((entry) => infra.has(entry.of) && entry.line.test(line)));
440
+ // REJUDGE base-stale — 거절 줄이 base-stale-overlap(⊕ 딸린 줄) «뿐»이고 인프라 사유가 없을 때만. 하나라도 다른 ✗ 줄이 섞이면 judgement(fail-closed).
441
+ const stale = judged.map((line) => LAND_BASE_STALE_LINE.exec(line)?.[1]).find((reason): reason is string => !!reason);
442
+ if (stale && !infra.size && judged.every((line) => LAND_BASE_STALE_LINE.test(line) || LAND_BASE_STALE_CONSEQUENCE_LINES.some((re) => re.test(line)))) {
443
+ return { class: 'base-stale', reason: stale.slice(0, 160) };
444
+ }
445
+ if (judged.length) return { class: 'judgement', reason: judged[0]!.slice(0, 120) };
446
+ if (infra.size) return { class: 'infra', reason: [...infra].join('+') };
447
+ return { class: 'judgement', reason: 'no-failure-line' };
448
+ }
449
+
450
+ type LandAttempt = NonNullable<TaskCard['landAttempts']>[number];
451
+ type LandWorktreeCreateFailure = { pr: number; head: string; at: string; reason: string };
452
+ declare module './task-hand.js' {
453
+ interface TaskCard { landWorktreeCreateFailures?: LandWorktreeCreateFailure[] }
454
+ }
455
+ const sameHeadOf = (pr: number, head: string) => (entry: LandAttempt) => entry.pr === pr && entry.head.toLowerCase() === head.toLowerCase();
456
+
457
+ function landWorktreeCreateCap(): number {
458
+ try {
459
+ const config = JSON.parse(readFileSync(userConfigPath(), 'utf8')) as { tasks?: { landWorktreeCreateCap?: unknown } };
460
+ const configured = config.tasks?.landWorktreeCreateCap;
461
+ return typeof configured === 'number' && Number.isSafeInteger(configured) && configured > 0 ? configured : 3;
462
+ } catch { return 3; }
463
+ }
464
+
465
+ function consecutiveLandWorktreeCreateFailures(card: TaskCard, pr: number, head: string): LandWorktreeCreateFailure[] {
466
+ const entries = (card.landWorktreeCreateFailures ?? []).filter((entry) => entry.pr === pr);
467
+ const last = entries.at(-1);
468
+ return last?.head.toLowerCase() === head.toLowerCase()
469
+ ? entries.filter((entry) => entry.head.toLowerCase() === head.toLowerCase())
470
+ : [];
471
+ }
472
+
473
+ function recordLandWorktreeCreateFailure(card: TaskCard, failure: LandWorktreeCreateFailure): TaskCard {
474
+ const samePr = (card.landWorktreeCreateFailures ?? []).filter((entry) => entry.pr !== failure.pr);
475
+ const samePrHead = (card.landWorktreeCreateFailures ?? []).filter((entry) => entry.pr === failure.pr && entry.head.toLowerCase() === failure.head.toLowerCase());
476
+ return { ...card, landWorktreeCreateFailures: [...samePr, ...samePrHead, failure] };
477
+ }
478
+
479
+ function clearLandWorktreeCreateFailures(card: TaskCard, pr: number): TaskCard {
480
+ const remaining = (card.landWorktreeCreateFailures ?? []).filter((entry) => entry.pr !== pr);
481
+ const { landWorktreeCreateFailures: _removed, ...rest } = card;
482
+ return { ...rest, ...(remaining.length ? { landWorktreeCreateFailures: remaining } : {}) };
483
+ }
484
+
485
+ function cappedLandWorktreeReason(failures: readonly LandWorktreeCreateFailure[], cap: number): string {
486
+ return `land worktree create capped (${failures.length}/${cap}): ${failures.at(-1)?.reason ?? 'unknown'}`;
487
+ }
488
+
489
+ function observeLandWorktreeCreateCapped(deps: LiveMoveDeps, data: { card: string; pr: number; head: string; failures: number; cap: number; reason: string }): void {
490
+ try { (deps.log ?? ((c, e, d) => debug.log(c, e, d)))('task-agent', 'land-worktree-create-capped', data); } catch { /* fail-soft */ }
491
+ }
492
+
493
+ /** 같은 머리에 한 번 더 시도해도 되나 — 앞 시도가 «전부» 끝났고(ok:false) «전부» infra 이고 상한 밑일 때만. */
494
+ function landRetryAllowed(prior: readonly LandAttempt[]): boolean {
495
+ return prior.length > 0 && prior.length < LAND_ATTEMPTS_PER_HEAD_ON_INFRA && prior.every((entry) => entry.ok === false && entry.class === 'infra');
496
+ }
497
+
498
+ /**
499
+ * REJUDGE base-stale — `gateAt`(새 base 위 재게이트를 «요청한» 시각) 앞의 base-stale 시도는 그 재게이트가 통과해 소비됐다 —
500
+ * 같은 머리 land 판단에서 뺀다. 나머지(infra·judgement·그 뒤의 시도)는 종전 규칙 그대로.
501
+ */
502
+ export function openLandAttempts<T extends Pick<LandAttempt, 'at' | 'class'>>(prior: readonly T[], gateAt?: string): T[] {
503
+ if (!gateAt) return [...prior];
504
+ const since = Date.parse(gateAt);
505
+ return prior.filter((entry) => !(entry.class === 'base-stale' && Number.isFinite(since) && Date.parse(entry.at) < since));
506
+ }
507
+
508
+ /** 카드의 이 PR·머리에 인프라 재시도가 남았나(재판정 틱이 «머리 그대로»여도 land 단계까지 가게). */
509
+ export function landRetryPending(card: Pick<TaskCard, 'landAttempts'>, pr: number, head: string): boolean {
510
+ return landRetryAllowed((card.landAttempts ?? []).filter(sameHeadOf(pr, head)));
511
+ }
512
+
513
+ export const LAND_RETRY_ON_INFRA_EVENT = 'land-retry-on-infra';
514
+
347
515
  const landedCycles = new Set<string>();
348
516
 
349
517
  /** `propose-land` — only a passing self review (pass or warn, with must-fix 0) of this exact OPEN, non-draft head may invoke pr land. */
350
518
  export async function executeLiveLand(
351
519
  card: TaskCard | undefined, pr: number | undefined,
352
520
  ctx: { runId?: string | null; cycleId?: string; cwd?: string; produced?: { headCommit?: string; branch?: string; prUrl?: string }; review?: { verdict: 'pass' | 'warn' | 'fail'; head: string; /** TA-LAND-WARN-MUSTFIX0 — `false` 면 리뷰가 실제로 안 돌았다 → land 거부. 슈퍼바이저 입력은 reviewed 리뷰로만 만들어져 칸이 없다. */ reviewed?: boolean };
353
- /** TA-REJUDGE-ON-HEAD — 겹침 증거 파일(있으면 land 에 넘길 뿐 · 판정은 그대로). */ overlapEvidence?: string },
521
+ /** TA-REJUDGE-ON-HEAD — 겹침 증거 파일(있으면 land 에 넘길 뿐 · 판정은 그대로). */ overlapEvidence?: string;
522
+ /** REJUDGE base-stale — 증거의 재게이트를 요청한 시각 · 그 앞의 base-stale 시도는 소비됨(`openLandAttempts`). */ gateAt?: string },
354
523
  deps: LiveMoveDeps = {},
355
524
  ): Promise<LiveMoveResult> {
356
525
  const kind = 'land' as const;
@@ -390,12 +559,26 @@ export async function executeLiveLand(
390
559
  if (cycle && landedCycles.has(cycle)) return reject(card.id, `cycle ${cycle} already landed — stays shadow`, reviewRepo);
391
560
  const path = deps.statePath ?? taskAgentStatePath();
392
561
  const at = (deps.now ?? (() => new Date()))().toISOString();
562
+ const local = (deps.landWorktree?.localHead ?? localHead)(cwd);
563
+ const needsLandWorktree = !!local && local.toLowerCase() !== view.head.toLowerCase();
564
+ const createCap = landWorktreeCreateCap();
393
565
  let claimed = false;
394
566
  let missing = false;
567
+ let cappedReason: string | undefined;
568
+ // TA-LAND-RETRY-ON-INFRA — 같은 머리의 앞 시도들(잠금 안에서 읽은 «지금» 카드) · 재시도면 그 근거.
569
+ let prior: LandAttempt[] = [];
395
570
  try {
396
571
  updateTaskCard(path, card.id, (current) => {
397
572
  if (!current) { missing = true; return undefined; }
398
- if ((current.landAttempts ?? []).some(entry => entry.pr === pr && entry.head.toLowerCase() === view!.head.toLowerCase())) return undefined;
573
+ if (needsLandWorktree) {
574
+ const failures = consecutiveLandWorktreeCreateFailures(current, pr, view!.head);
575
+ if (failures.length >= createCap) {
576
+ cappedReason = cappedLandWorktreeReason(failures, createCap);
577
+ return undefined;
578
+ }
579
+ }
580
+ prior = openLandAttempts((current.landAttempts ?? []).filter(sameHeadOf(pr, view!.head)), ctx.gateAt);
581
+ if (prior.length && !landRetryAllowed(prior)) return undefined;
399
582
  claimed = true;
400
583
  return { ...current, landAttempts: [...(current.landAttempts ?? []), { pr, head: view!.head, at }] };
401
584
  });
@@ -403,14 +586,23 @@ export async function executeLiveLand(
403
586
  return reject(card.id, `land attempt could not be recorded: ${String(error).slice(-250)}`, reviewRepo);
404
587
  }
405
588
  if (missing) return reject(card.id, 'task card not in state file', reviewRepo);
406
- if (!claimed) return observe(deps, withRepo({ kind, card: card.id, ok: true, executed: false, detail: `already attempted land for #${pr} head ${view.head.slice(0, 12)}` }));
589
+ if (cappedReason) return reject(card.id, cappedReason, reviewRepo);
590
+ if (!claimed) {
591
+ const last = prior.at(-1);
592
+ const why = last ? ` (attempts ${prior.length} · last ${last.ok === false ? `class ${last.class ?? 'unclassified'}${last.reason ? ` ${last.reason}` : ''}` : last.ok === true ? 'ok' : 'unfinished'})` : '';
593
+ return observe(deps, withRepo({ kind, card: card.id, ok: true, executed: false, detail: `already attempted land for #${pr} head ${view.head.slice(0, 12)}${why}` }));
594
+ }
595
+ const attempt = prior.length + 1;
596
+ if (attempt > 1) {
597
+ const last = prior.at(-1)!;
598
+ try { (deps.log ?? ((c, e, d) => debug.log(c, e, d)))('task-agent', LAND_RETRY_ON_INFRA_EVENT, { card: card.id, pr, head: view.head, attempt, reason: last.reason ?? 'unknown', class: 'infra', decision: 'retrying' }); } catch { /* fail-soft */ }
599
+ }
407
600
  if (cycle) landedCycles.add(cycle);
408
601
  // TA-LAND-WORKTREE — `pr land --expected-head` 는 «로컬 HEAD = 고정 머리»를 요구한다. cwd 가 다른 머리면(Pod 런 PR · 호스트 checkout)
409
602
  // PR 머리의 임시 워크트리에서 부르고 끝나면 걷는다. 같은 머리(로컬 런 작업 트리)거나 HEAD 를 못 읽으면 종전 그대로 cwd 에서.
410
603
  let landCwd = cwd;
411
604
  let worktree: LandWorktree | undefined;
412
- const local = (deps.landWorktree?.localHead ?? localHead)(cwd);
413
- if (local && local.toLowerCase() !== view.head.toLowerCase()) {
605
+ if (needsLandWorktree && local) {
414
606
  let made: LandWorktree | LandWorktreeFailure;
415
607
  try {
416
608
  made = await (deps.landWorktree?.create ?? createLandWorktree)({ card: card.id, pr, head: view.head, branch: view.branch, repoCwd: cwd, statePath: path });
@@ -418,6 +610,15 @@ export async function executeLiveLand(
418
610
  if ('reason' in made) {
419
611
  const reason = made.reason;
420
612
  observeLandWorktree({ card: card.id, pr, head: view.head, created: made.created, removed: made.removed, reason }, deps.log);
613
+ let failures: LandWorktreeCreateFailure[] = [];
614
+ let recordFailureError: unknown;
615
+ try {
616
+ updateTaskCard(path, card.id, (current) => {
617
+ if (!current) return undefined;
618
+ failures = [...consecutiveLandWorktreeCreateFailures(current, pr, view!.head), { pr, head: view!.head, at, reason }];
619
+ return recordLandWorktreeCreateFailure(current, { pr, head: view!.head, at, reason });
620
+ });
621
+ } catch (error) { recordFailureError = error; }
421
622
  // land 를 부르지 않았다 — 이 머리의 시도 기록과 주기 표시를 되돌려 다음 틱이 다시 시도할 수 있게 한다.
422
623
  if (cycle) landedCycles.delete(cycle);
423
624
  try {
@@ -429,16 +630,30 @@ export async function executeLiveLand(
429
630
  observeLandWorktree({ card: card.id, pr, head: view.head, created: made.created, removed: made.removed, reason: `land attempt release failed: ${String(error).slice(-200)}` }, deps.log);
430
631
  return reject(card.id, `PR #${pr} land worktree unavailable: ${reason} · attempt release failed (head stays attempted)`, reviewRepo);
431
632
  }
633
+ if (recordFailureError) return reject(card.id, `PR #${pr} land worktree unavailable: ${reason} · failure record failed: ${String(recordFailureError).slice(-200)}`, reviewRepo);
634
+ if (failures.length >= createCap) {
635
+ const capped = cappedLandWorktreeReason(failures, createCap);
636
+ if (failures.length === createCap) observeLandWorktreeCreateCapped(deps, { card: card.id, pr, head: view.head, failures: failures.length, cap: createCap, reason });
637
+ return reject(card.id, capped, reviewRepo);
638
+ }
432
639
  return reject(card.id, `PR #${pr} land worktree unavailable: ${reason}`, reviewRepo);
433
640
  }
434
641
  worktree = made;
435
642
  landCwd = made.cwd;
643
+ // 실패 장부 정리는 fail-soft — 정리 쓰기 실패로 정상 착지를 막지 않는다(막으면 잡아 둔 landAttempts 가 남아 그 head 가 영영 «already attempted»).
644
+ try {
645
+ updateTaskCard(path, card.id, (current) => current?.landWorktreeCreateFailures?.some((entry) => entry.pr === pr)
646
+ ? clearLandWorktreeCreateFailures(current, pr) : undefined);
647
+ } catch (error) {
648
+ try { (deps.log ?? ((c, e, d) => debug.log(c, e, d, { level: 'warn' })))('task-agent', 'land-worktree-create-failures-clear-failed', { card: card.id, pr, head: view.head, error: String(error).slice(-200) }); } catch { /* fail-soft */ }
649
+ }
436
650
  observeLandWorktree({ card: card.id, pr, head: view.head, created: true, removed: false, reason: `cwd HEAD ${local.slice(0, 12)} ≠ PR head ${view.head.slice(0, 12)}` }, deps.log);
437
651
  }
438
652
  // 증거 경로는 «원래» cwd 기준으로 고정한다 — 임시 트리로 cwd 를 바꿔도 상대경로가 다른 파일을 가리키지 않게(같은 cwd 면 그대로).
439
653
  const overlapEvidence = ctx.overlapEvidence && worktree ? resolve(cwd, ctx.overlapEvidence) : ctx.overlapEvidence;
440
654
  let ok = false;
441
655
  let detail: string;
656
+ let failure: LandFailureClassification | undefined;
442
657
  try {
443
658
  // The CLI rechecks the pinned PR/head immediately before merge, after its own gates and freeze admission.
444
659
  const outcome = overlapEvidence
@@ -446,7 +661,14 @@ export async function executeLiveLand(
446
661
  : await (deps.land ?? defaultLand)(pr, view.head, landCwd);
447
662
  ok = outcome.status === 0;
448
663
  detail = (ok ? outcome.stdout || `land merged #${pr} head ${view.head.slice(0, 12)}` : outcome.stderr || outcome.stdout || `exit ${outcome.status}`).slice(-300);
449
- } catch (error) { detail = (error instanceof Error ? error.message : String(error)).slice(-300); }
664
+ if (!ok) failure = classifyLandFailure(outcome);
665
+ } catch (error) {
666
+ detail = (error instanceof Error ? error.message : String(error)).slice(-300);
667
+ failure = classifyLandFailure({ thrown: detail });
668
+ }
669
+ const retryAllowed = failure?.class === 'infra' && attempt < LAND_ATTEMPTS_PER_HEAD_ON_INFRA;
670
+ // 인프라 실패로 재시도가 남으면 주기 표시도 푼다 — 같은 주기 id(재판정 틱의 `rejudge:<card>:<head>`)가 그 한 번을 막지 않게.
671
+ if (retryAllowed && cycle) landedCycles.delete(cycle);
450
672
  if (worktree) {
451
673
  // 성공이든 실패든 걷는다.
452
674
  let removal: LandWorktreeRemoval;
@@ -454,15 +676,38 @@ export async function executeLiveLand(
454
676
  observeLandWorktree({ card: card.id, pr, head: view.head, created: true, removed: removal.removed, ...(removal.branchRemoved !== undefined ? { branchRemoved: removal.branchRemoved } : {}),
455
677
  reason: removal.reason ?? `land ${ok ? 'ok' : 'failed'} — removed` }, deps.log);
456
678
  }
679
+ const landAttempt: LiveMoveResult['landAttempt'] = failure
680
+ ? { attempt, class: failure.class, reason: failure.reason, retryAllowed }
681
+ : { attempt };
682
+ if (failure) {
683
+ try {
684
+ (deps.log ?? ((c, e, d) => debug.log(c, e, d)))('task-agent', LAND_RETRY_ON_INFRA_EVENT, {
685
+ card: card.id, pr, head: view.head, attempt, reason: failure.reason, class: failure.class,
686
+ decision: retryAllowed ? 'retry-allowed' : failure.class === 'infra' ? 'retry-exhausted' : failure.class === 'base-stale' ? 'base-stale-awaiting-regate' : 'no-retry',
687
+ });
688
+ } catch { /* fail-soft */ }
689
+ }
457
690
  try {
458
691
  updateTaskCard(path, card.id, (current) => current?.landAttempts?.some(entry => entry.pr === pr && entry.head === view!.head && entry.at === at)
459
- ? { ...current, landAttempts: current.landAttempts.map(entry => entry.pr === pr && entry.head === view!.head && entry.at === at ? { ...entry, ok, detail } : entry) }
692
+ ? {
693
+ ...current,
694
+ landAttempts: current.landAttempts.map(entry => entry.pr === pr && entry.head === view!.head && entry.at === at
695
+ ? { ...entry, ok, detail, ...(failure ? { class: failure.class, reason: failure.reason } : {}) } : entry),
696
+ // TA-LAND-RETRY-ON-INFRA — 실패한 시도마다 카드 history 한 줄(logs 창이 지나도 «왜 다시 했나/안 했나»가 남게).
697
+ ...(failure ? { history: [...(current.history ?? []), {
698
+ at, event: LAND_RETRY_ON_INFRA_EVENT, kind: failure.class, pr, head: view!.head,
699
+ effect: retryAllowed ? 'retry-allowed' : failure.class === 'infra' ? 'retry-exhausted' : failure.class === 'base-stale' ? 'awaiting-regate' : 'no-retry',
700
+ detail: failure.class === 'base-stale'
701
+ ? `attempt ${attempt} · base-stale → 재게이트 대기 (land again only after a re-gate on a new base passes) · ${failure.reason}`
702
+ : `attempt ${attempt}/${failure.class === 'infra' ? LAND_ATTEMPTS_PER_HEAD_ON_INFRA : attempt} · ${failure.reason}`,
703
+ }] } : {}),
704
+ }
460
705
  : undefined);
461
706
  } catch (error) {
462
- return observe(deps, withRepo({ kind, card: card.id, ok: false, executed: true,
707
+ return observe(deps, withRepo({ kind, card: card.id, ok: false, executed: true, landAttempt,
463
708
  detail: `land ${ok ? 'succeeded' : 'failed'} but card update failed: ${String(error).slice(-230)}` }));
464
709
  }
465
- return observe(deps, withRepo({ kind, card: card.id, ok, executed: true, detail }));
710
+ return observe(deps, withRepo({ kind, card: card.id, ok, executed: true, detail, landAttempt }));
466
711
  }
467
712
 
468
713
  /** `propose-green` — 비지 않은 근거가 있을 때만 카드에 제안을 적는다(한 번). 체크리스트는 쓰지 않는다. */
@@ -484,3 +729,88 @@ export function executeLiveGreenProposal(card: TaskCard | undefined, evidence: R
484
729
  if (!wrote.proposed) return observe(deps, { kind, executed: false, card: card.id, ok: true, detail: 'already proposed' });
485
730
  return observe(deps, { kind, executed: true, card: card.id, ok: true, detail: `green proposed (${Object.values(refs).join(', ')}) · checklist untouched` });
486
731
  }
732
+
733
+ export interface LiveDecisionCardContext {
734
+ runId: string | null; stopReason: string; reason: string; pr?: number;
735
+ risk?: NextAction['risk'];
736
+ /** 서명 · 화면 · must-fix — 하나도 비지 않아야 카드가 올라간다. */
737
+ evidence: DecisionCardEvidence;
738
+ }
739
+
740
+ const DECISION_SIGNATURE_KEEP = 300;
741
+ const DECISION_SCREEN_KEEP = 1500;
742
+
743
+ /** 같은 런 · 사유 · 근거면 같은 지문 — 카드 history 의 «이미 올렸다» 판정 키. */
744
+ export function decisionCardFingerprint(runId: string | null, reason: string, evidence: { signature?: string; screen?: string; mustFix: readonly string[] }): string {
745
+ return createHash('sha1').update(JSON.stringify([runId, reason, evidence.signature ?? null, evidence.screen ?? null, evidence.mustFix])).digest('hex').slice(0, 12);
746
+ }
747
+
748
+ /** `decisions raise` 를 같은 우주의 CLI 자식으로 한 번 부른다 — 카드는 결정 원장에 들어가 OP 거르기가 읽는다. */
749
+ export async function defaultRaiseDecision(args: string[]): Promise<{ status: number; stdout: string; stderr?: string }> {
750
+ const { ELANOUS_CLI_ENTRY } = await import('../cli/tasks-cli.js');
751
+ const { getElanousConfigDirOverride } = await import('../elanous-config-dir.js');
752
+ const { effectiveInstanceRoot } = await import('../instance/resolve.js');
753
+ const argv = [resolve(ELANOUS_CLI_ENTRY), '--config-dir', getElanousConfigDirOverride() ?? effectiveInstanceRoot(), ...args];
754
+ return new Promise((resolveResult, reject) => {
755
+ const child = spawn(process.execPath, argv, { stdio: ['ignore', 'pipe', 'pipe'] });
756
+ let stdout = '';
757
+ let stderr = '';
758
+ child.stdout.on('data', (chunk: Buffer) => { stdout = (stdout + chunk.toString()).slice(-300); });
759
+ child.stderr.on('data', (chunk: Buffer) => { stderr = (stderr + chunk.toString()).slice(-300); });
760
+ child.once('error', reject);
761
+ child.once('close', (code) => resolveResult({ status: code ?? 1, stdout, stderr }));
762
+ });
763
+ }
764
+
765
+ /**
766
+ * `decision-card` — 막힌 과제를 근거 붙은 결정 카드로 올린다. 근거(서명 · 화면 · must-fix)가 하나도 없으면 «올리지 않는다».
767
+ * 같은 지문의 카드는 한 번만(카드 history 의 `live-move` 줄 · 먼저 적고 올린다) · 올리기 실패는 줄을 걷어 다음 틱이 다시 시도한다.
768
+ */
769
+ export async function executeLiveDecisionCard(card: TaskCard | undefined, ctx: LiveDecisionCardContext, deps: LiveMoveDeps = {}): Promise<LiveMoveResult> {
770
+ const kind = 'decision-card' as const;
771
+ if (!card) return observe(deps, { kind, executed: false, card: null, ok: false, detail: 'no task card — stays shadow' });
772
+ const grounds = decisionCardGrounds(ctx.evidence);
773
+ if (!grounds.length) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: 'no evidence (signature · screen · must-fix) — no card raised' });
774
+ const signature = ctx.evidence.signature?.trim().slice(0, DECISION_SIGNATURE_KEEP) || undefined;
775
+ const screen = ctx.evidence.screen?.trim().slice(-DECISION_SCREEN_KEEP) || undefined;
776
+ const mustFix = (ctx.evidence.mustFix ?? []).filter((fix) => typeof fix === 'string' && fix.trim()).map((fix) => fix.trim());
777
+ const mark = `fp:${decisionCardFingerprint(ctx.runId, ctx.reason, { ...(signature ? { signature } : {}), ...(screen ? { screen } : {}), mustFix })}`;
778
+ const path = deps.statePath ?? taskAgentStatePath();
779
+ const at = (deps.now ?? (() => new Date()))().toISOString();
780
+ const ours = (entry: TaskCard['history'][number]) => entry.event === LIVE_MOVE_HISTORY_EVENT && entry.kind === kind && !!entry.detail?.includes(mark);
781
+ const claim = { claimed: false, missing: false };
782
+ updateTaskCard(path, card.id, (current) => {
783
+ if (!current) { claim.missing = true; return undefined; }
784
+ if ((current.history ?? []).some(ours)) return undefined;
785
+ claim.claimed = true;
786
+ return { ...current, history: [...(current.history ?? []), {
787
+ at, event: LIVE_MOVE_HISTORY_EVENT, kind, executed: false, executorResult: 'live' as const,
788
+ detail: `decision card claimed · ${mark}`, ...(ctx.runId ? { runId: ctx.runId } : {}), ...(ctx.pr !== undefined ? { pr: ctx.pr } : {}),
789
+ effect: 'none — decision card claimed, not yet raised',
790
+ }] };
791
+ });
792
+ if (claim.missing) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: 'task card not in state file' });
793
+ if (!claim.claimed) return observe(deps, { kind, executed: false, card: card.id, ok: true, detail: `already raised for ${mark}` });
794
+ const evidence = JSON.stringify({
795
+ card: card.id, runId: ctx.runId, pr: ctx.pr ?? null, stopReason: ctx.stopReason, grounds,
796
+ ...(signature ? { signature } : {}), ...(screen ? { screen } : {}), mustFix, selected: kind, reason: ctx.reason,
797
+ });
798
+ const args = decisionRaiseArgs({ taskId: card.id, reason: ctx.reason, ...(ctx.risk ? { risk: ctx.risk } : {}), evidence });
799
+ let failure: string | undefined;
800
+ try {
801
+ const outcome = await (deps.raiseDecision ?? defaultRaiseDecision)(args);
802
+ if (outcome.status !== 0) failure = (outcome.stderr || outcome.stdout || `exit ${outcome.status}`).slice(-250);
803
+ } catch (error) { failure = (error instanceof Error ? error.message : String(error)).slice(-250); }
804
+ const mine = (entry: TaskCard['history'][number]) => ours(entry) && entry.at === at;
805
+ const detail = failure ? `decisions raise failed: ${failure}` : `decision card raised (${grounds.join(' · ')}) · ${mark}`;
806
+ try {
807
+ updateTaskCard(path, card.id, (current) => current?.history?.some(mine)
808
+ ? { ...current, history: failure
809
+ ? current.history.filter((entry) => !mine(entry))
810
+ : current.history.map((entry) => mine(entry) ? { ...entry, executed: true, ok: true, detail, effect: `decision card raised · grounds ${grounds.join('+')} · decision ledger (OP filter reads it)` } : entry) }
811
+ : undefined);
812
+ } catch (error) {
813
+ return observe(deps, { kind, executed: !failure, card: card.id, ok: false, detail: `${detail} but card update failed: ${String(error).slice(-200)}` });
814
+ }
815
+ return observe(deps, { kind, executed: !failure, card: card.id, ok: !failure, detail });
816
+ }