kenaz 0.3.3 → 0.3.6

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 (451) hide show
  1. package/core/.agents/skills/typesafe-ai/LICENSE +21 -21
  2. package/core/.agents/skills/typesafe-ai/SKILL.md +149 -149
  3. package/core/.claude/commands/Ansuz.md +15 -2
  4. package/core/.claude/hooks/__tests__/task-2395-project-registration.test.js +84 -0
  5. package/core/.claude/hooks/handoff-staleness.js +2 -2
  6. package/core/.claude/hooks/session-init.ps1 +49 -49
  7. package/core/.claude/hooks/turn-context.js +50 -2
  8. package/core/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -26
  9. package/core/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -97
  10. package/core/.claude/skills/ui-ux-pro-max/data/icons.csv +101 -101
  11. package/core/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -31
  12. package/core/.claude/skills/ui-ux-pro-max/data/products.csv +96 -96
  13. package/core/.claude/skills/ui-ux-pro-max/data/react-performance.csv +45 -45
  14. package/core/.claude/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -54
  15. package/core/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -53
  16. package/core/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -56
  17. package/core/.claude/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -53
  18. package/core/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -53
  19. package/core/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -51
  20. package/core/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -59
  21. package/core/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -52
  22. package/core/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -54
  23. package/core/.claude/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -61
  24. package/core/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -54
  25. package/core/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -51
  26. package/core/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -50
  27. package/core/.claude/skills/ui-ux-pro-max/data/styles.csv +68 -68
  28. package/core/.claude/skills/ui-ux-pro-max/data/typography.csv +57 -57
  29. package/core/.claude/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -101
  30. package/core/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +99 -99
  31. package/core/.claude/skills/ui-ux-pro-max/data/web-interface.csv +31 -31
  32. package/core/.claude/skills/ui-ux-pro-max/scripts/core.py +253 -253
  33. package/core/.claude/skills/ui-ux-pro-max/scripts/design_system.py +1067 -1067
  34. package/core/.claude/skills/ui-ux-pro-max/scripts/search.py +106 -106
  35. package/core/.collaboration/core/task-api.js +1 -4
  36. package/core/.formation/README.md +21 -146
  37. package/core/.formation/bifrost/codex-doctor.js +37 -2
  38. package/core/.formation/bifrost/codex-doctor.test.js +18 -0
  39. package/core/.formation/bifrost/launch.js +1 -1
  40. package/core/.formation/bifrost/openai/codex-hook-shim.js +6 -0
  41. package/core/.formation/bifrost/openai/codex-hook-shim.test.js +11 -1
  42. package/core/.formation/bifrost/setup-codex.js +3 -0
  43. package/core/.formation/native/claude/README.md +1 -1
  44. package/core/.formation/role-map.yaml +3 -3
  45. package/core/.formation/spec/task_operations.parity.test.js +0 -1
  46. package/core/.formation/spec/task_operations.yaml +3 -3
  47. package/core/.formation/tests/hello.txt +2 -2
  48. package/core/.formation/tools/__tests__/subagent-bifrost-guard.test.js +4 -4
  49. package/core/.formation/tools/dashboard-models-chain.test.js +0 -6
  50. package/core/.formation/tools/dispatch-detail/cross-project-worktree.md +1 -9
  51. package/core/.formation/tools/dispatch-detail/worktree-subagent-mode.md +2 -2
  52. package/core/.formation/tools/dispatch.js +33 -554
  53. package/core/.formation/tools/mcp-allowlist.test.js +0 -1
  54. package/core/.formation/tools/post-complete/task2345.test.js +0 -5
  55. package/core/.formation/tools/pre-dispatch-snapshot.test.js +0 -100
  56. package/core/.formation/tools/prompt.js +33 -1623
  57. package/core/.formation/tools/prompt.test.js +26 -594
  58. package/core/.formation/unit/do.yaml +2 -4
  59. package/core/.kenaz/knowledge/ladybug_2c51da1e_shadow_file_frame_group.patch +163 -163
  60. package/core/.kenaz/knowledge/ladybug_30cf85690e_shadow_page_read_during_checkpoint.patch +393 -393
  61. package/core/.kenaz/knowledge/ladybug_7a17858d3a_node_group_delete_lock.patch +102 -102
  62. package/core/.kenaz/knowledge/ladybug_926a0ba1_column_checkpoint.patch +412 -412
  63. package/core/.kenaz/knowledge/ladybug_a346c4c4b6_pk_index_header_pages.patch +144 -144
  64. package/core/.kenaz/knowledge/ladybug_ba5f38815b_csr_node_group.patch +86 -86
  65. package/core/.kenaz/muninn_episode_write.ps1 +17 -17
  66. package/core/.kenaz/specialist/field_report_watermark.json +1 -1
  67. package/core/.kenaz/specialist/memory_guard_fired.json +1 -1
  68. package/core/.kenaz/specialist/memory_guard_trigger_cache.json +1 -1
  69. package/core/.kenaz/specialist/t1777_probe.txt +1 -1
  70. package/core/.matrix/index.matrix +5 -5
  71. package/core/.rule/general/agent-workflow.yaml +4 -4
  72. package/core/.rule/general/file-size-threshold.yaml +16 -4
  73. package/core/.rule/general/rust-verify-target.yaml +0 -1
  74. package/core/.rule/general/system-invariants.yaml +8 -25
  75. package/core/.specialist/Ansuz/SYSTEM_DOC.md +17 -42
  76. package/core/.specialist/Ansuz/capability.yaml +7 -14
  77. package/core/.specialist/Ansuz/concepts/formation_quick_ref.yaml +4 -11
  78. package/core/.specialist/Ansuz/elixir.yaml +3 -4
  79. package/core/.specialist/Ansuz/identity_contract.yaml +1 -1
  80. package/core/.specialist/Ansuz/knowledge.yaml +7 -10
  81. package/core/.specialist/Ansuz/rule.yaml +2 -2
  82. package/core/.specialist/Ansuz/tools/verify-and-land.js +22 -0
  83. package/core/.specialist/Ansuz/tools/verify-and-land.test.js +59 -5
  84. package/core/.specialist/Ansuz/workflow.yaml +7 -11
  85. package/core/.specialist/Forseti/core.yaml +1 -1
  86. package/core/.specialist/Heimdall/SKILL.md +1 -1
  87. package/core/.specialist/Heimdall/core.yaml +2 -2
  88. package/core/.specialist/Huginn/rule.yaml +1 -1
  89. package/core/.specialist/tools/README.md +2 -2
  90. package/core/.specialist/tools/__tests__/freya-cli-contract.test.js +0 -1
  91. package/core/.system/collaboration/README.md +1 -1
  92. package/core/.system/formation/LEGACY_SKILLS.md +2 -0
  93. package/core/.system/formation/POST_COMPLETE.md +2 -0
  94. package/core/.system/formation/README.md +2 -0
  95. package/core/KENAZ_CORE_VERSION +1 -1
  96. package/core/dev/ANSUZ_STARTUP_INJECTION_REVIEW.md +355 -355
  97. package/core/dev/JEV_INTEGRATION_PLAN.md +189 -189
  98. package/core/dev/design/matrix_format_EXAMPLE_memory.matrix +13 -13
  99. package/core/dev/design/matrix_format_EXAMPLE_project_status.matrix +9 -9
  100. package/core/dev/design/matrix_format_EXAMPLE_tasks.matrix +14 -14
  101. package/core/dev/design/matrix_v2_EXAMPLE.matrix +67 -67
  102. package/core/dev/jev-eval/ACTIVE_CORRECTION.md +52 -52
  103. package/core/dev/jev-eval/CONTEXT_REVIEW.md +31 -31
  104. package/core/dev/jev-eval/EVALUATION_STATUS.md +39 -39
  105. package/core/dev/jev-eval/HISTORICAL_CORPUS.md +33 -33
  106. package/core/dev/jev-eval/README.md +99 -99
  107. package/core/dev/jev-eval/THALAMUS_RANKING.md +58 -58
  108. package/core/dev/jev-eval/reports/annotation-pilot-2026-09-20.json +32 -32
  109. package/core/dev/jev-eval/reports/context-review-2026-09-20.json +48 -48
  110. package/core/dev/jev-eval/reports/history-corpus-2026-09-20.json +52 -52
  111. package/core/dev/pipeline/SELF_ENHANCEMENT_STATUS.json.bak +47 -47
  112. package/core/dev/pipeline/tasks/.gitkeep +1 -1
  113. package/core/dev/pipeline/test_results/TASK_TEST_PARALLEL_002_result.txt +27 -27
  114. package/core/dev/scripts/check-file-size.js +44 -17
  115. package/core/dev/scripts/init_git.sh +0 -0
  116. package/core/dev/scripts/init_project.sh +0 -0
  117. package/core/dev/scripts/legacy/dev_agent_scheduler.ps1.bak +536 -536
  118. package/core/dev/scripts/legacy/multi_agent_scheduler.ps1.bak +1978 -1978
  119. package/core/dev/scripts/matrix/src/cli/commands/list-tags.ts.wip +140 -140
  120. package/core/dev/scripts/matrix/src/cli/commands/manage-tags.ts.wip +173 -173
  121. package/core/dev/scripts/matrix/src/cli/commands/stats.ts.wip +168 -168
  122. package/core/dev/scripts/matrix/src/cli/commands/tokens.ts.wip +126 -126
  123. package/core/dev/scripts/multi_agent_scheduler.sh +0 -0
  124. package/core/dev/scripts/run_once.sh +0 -0
  125. package/core/dev/scripts/setup_mcp.sh +0 -0
  126. package/core/dev/scripts/setup_rag.sh +0 -0
  127. package/core/dev/scripts/start_dev_agent.sh +0 -0
  128. package/core/dev/scripts/start_multi_agent.sh +0 -0
  129. package/core/dev/scripts/sync_task_queue.sh +0 -0
  130. package/core/k-cli/README.md +1 -94
  131. package/core/k-cli/test/TEST_SPEC.md +5 -22
  132. package/core/k-cli/test/run.sh +1 -67
  133. package/core/plugins/kenaz/commands/Amelia.md +1 -1
  134. package/core/plugins/kenaz/commands/Ansuz.md +16 -3
  135. package/core/plugins/kenaz/scripts/kenaz-mode.js +11 -1
  136. package/core/plugins/kenaz/scripts/sync.js +6 -2
  137. package/core/plugins/kenaz/scripts/ygg.js +13 -1
  138. package/core/plugins/kenaz/skills/product_manager/SKILL.md +12 -12
  139. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/charts.csv +26 -26
  140. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/colors.csv +97 -97
  141. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/icons.csv +101 -101
  142. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/landing.csv +31 -31
  143. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/products.csv +96 -96
  144. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/react-performance.csv +45 -45
  145. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -54
  146. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -53
  147. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -56
  148. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -53
  149. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -53
  150. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -51
  151. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -59
  152. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -52
  153. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/react.csv +54 -54
  154. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -61
  155. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -54
  156. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -51
  157. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -50
  158. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/styles.csv +68 -68
  159. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/typography.csv +57 -57
  160. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -101
  161. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/ux-guidelines.csv +99 -99
  162. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/web-interface.csv +31 -31
  163. package/core/plugins/kenaz/skills/ui-ux-pro-max/scripts/core.py +253 -253
  164. package/core/plugins/kenaz/skills/ui-ux-pro-max/scripts/design_system.py +1067 -1067
  165. package/core/plugins/kenaz/skills/ui-ux-pro-max/scripts/search.py +106 -106
  166. package/lib/cli.js +11 -4
  167. package/package.json +11 -7
  168. package/vendor/install-kenaz-cli.js +18 -7
  169. package/core/.formation/legion/forge.yaml +0 -299
  170. package/core/.formation/legion/hive.yaml +0 -238
  171. package/core/.formation/squad/blitz.yaml +0 -192
  172. package/core/.formation/squad/strategy-tribunal.yaml +0 -354
  173. package/core/.formation/squad/strategy.yaml +0 -349
  174. package/core/.formation/squad/sweep.yaml +0 -208
  175. package/core/.formation/tools/broadcast.js +0 -103
  176. package/core/.formation/tools/dispatch-dryrun.test.js +0 -54
  177. package/core/.formation/tools/dispatch-notifier.js +0 -84
  178. package/core/.formation/tools/formations.js +0 -1203
  179. package/core/.formation/tools/headless-branch-recovery.test.js +0 -64
  180. package/core/.formation/tools/headless-dryrun.test.js +0 -79
  181. package/core/.formation/tools/headless-v2.js +0 -659
  182. package/core/.formation/tools/headless.js +0 -1071
  183. package/core/.formation/tools/legacy-scheduler.js +0 -1994
  184. package/core/.formation/tools/logger.js +0 -273
  185. package/core/.formation/tools/memory-loader.js +0 -422
  186. package/core/.formation/tools/process-state.js +0 -33
  187. package/core/.formation/tools/remote-conflict.js +0 -98
  188. package/core/.formation/tools/runner.js +0 -1332
  189. package/core/.formation/tools/task-utils.js +0 -344
  190. package/core/.formation/tools/task2028-headless-worktree.test.js +0 -112
  191. package/core/.formation/tools/tests/headless-mcp-args-plumbing.test.js +0 -123
  192. package/core/.formation/tools/worktree-leak.test.js +0 -162
  193. package/core/k-cli/dist/alaya/run.js +0 -91
  194. package/core/k-cli/dist/core/output.js +0 -24
  195. package/core/k-cli/dist/core/process.js +0 -38
  196. package/core/k-cli/dist/core/project.js +0 -80
  197. package/core/k-cli/dist/core/spawn.js +0 -268
  198. package/core/k-cli/dist/core/types.js +0 -2
  199. package/core/k-cli/dist/dashboard/update.js +0 -53
  200. package/core/k-cli/dist/dashboard/version.js +0 -20
  201. package/core/k-cli/dist/dispatch/run.js +0 -241
  202. package/core/k-cli/dist/dispatch/status.js +0 -103
  203. package/core/k-cli/dist/index.js +0 -283
  204. package/core/k-cli/dist/project/initialize.js +0 -301
  205. package/core/k-cli/dist/project/install.js +0 -89
  206. package/core/k-cli/dist/project/status.js +0 -138
  207. package/core/k-cli/dist/specialist/list.js +0 -100
  208. package/core/k-cli/dist/specialist/run.js +0 -93
  209. package/core/k-cli/dist/task/create.js +0 -194
  210. package/core/k-cli/dist/task/delete.js +0 -98
  211. package/core/k-cli/dist/task/get.js +0 -55
  212. package/core/k-cli/dist/task/list.js +0 -58
  213. package/core/k-cli/dist/task/submit.js +0 -156
  214. package/core/k-cli/dist/ygg/run.js +0 -51
  215. package/core/k-cli/node_modules/.bin/yaml +0 -16
  216. package/core/k-cli/node_modules/.bin/yaml.cmd +0 -17
  217. package/core/k-cli/node_modules/.bin/yaml.ps1 +0 -28
  218. package/core/k-cli/node_modules/.package-lock.json +0 -23
  219. package/core/k-cli/node_modules/yaml/LICENSE +0 -13
  220. package/core/k-cli/node_modules/yaml/README.md +0 -172
  221. package/core/k-cli/node_modules/yaml/bin.mjs +0 -11
  222. package/core/k-cli/node_modules/yaml/browser/dist/compose/compose-collection.js +0 -88
  223. package/core/k-cli/node_modules/yaml/browser/dist/compose/compose-doc.js +0 -43
  224. package/core/k-cli/node_modules/yaml/browser/dist/compose/compose-node.js +0 -109
  225. package/core/k-cli/node_modules/yaml/browser/dist/compose/compose-scalar.js +0 -86
  226. package/core/k-cli/node_modules/yaml/browser/dist/compose/composer.js +0 -217
  227. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-block-map.js +0 -115
  228. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-block-scalar.js +0 -198
  229. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-block-seq.js +0 -49
  230. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-end.js +0 -37
  231. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-flow-collection.js +0 -207
  232. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +0 -223
  233. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-props.js +0 -146
  234. package/core/k-cli/node_modules/yaml/browser/dist/compose/util-contains-newline.js +0 -34
  235. package/core/k-cli/node_modules/yaml/browser/dist/compose/util-empty-scalar-position.js +0 -26
  236. package/core/k-cli/node_modules/yaml/browser/dist/compose/util-flow-indent-check.js +0 -15
  237. package/core/k-cli/node_modules/yaml/browser/dist/compose/util-map-includes.js +0 -13
  238. package/core/k-cli/node_modules/yaml/browser/dist/doc/Document.js +0 -335
  239. package/core/k-cli/node_modules/yaml/browser/dist/doc/anchors.js +0 -71
  240. package/core/k-cli/node_modules/yaml/browser/dist/doc/applyReviver.js +0 -55
  241. package/core/k-cli/node_modules/yaml/browser/dist/doc/createNode.js +0 -88
  242. package/core/k-cli/node_modules/yaml/browser/dist/doc/directives.js +0 -176
  243. package/core/k-cli/node_modules/yaml/browser/dist/errors.js +0 -57
  244. package/core/k-cli/node_modules/yaml/browser/dist/index.js +0 -17
  245. package/core/k-cli/node_modules/yaml/browser/dist/log.js +0 -11
  246. package/core/k-cli/node_modules/yaml/browser/dist/nodes/Alias.js +0 -114
  247. package/core/k-cli/node_modules/yaml/browser/dist/nodes/Collection.js +0 -147
  248. package/core/k-cli/node_modules/yaml/browser/dist/nodes/Node.js +0 -38
  249. package/core/k-cli/node_modules/yaml/browser/dist/nodes/Pair.js +0 -36
  250. package/core/k-cli/node_modules/yaml/browser/dist/nodes/Scalar.js +0 -24
  251. package/core/k-cli/node_modules/yaml/browser/dist/nodes/YAMLMap.js +0 -144
  252. package/core/k-cli/node_modules/yaml/browser/dist/nodes/YAMLSeq.js +0 -113
  253. package/core/k-cli/node_modules/yaml/browser/dist/nodes/addPairToJSMap.js +0 -63
  254. package/core/k-cli/node_modules/yaml/browser/dist/nodes/identity.js +0 -36
  255. package/core/k-cli/node_modules/yaml/browser/dist/nodes/toJS.js +0 -37
  256. package/core/k-cli/node_modules/yaml/browser/dist/parse/cst-scalar.js +0 -214
  257. package/core/k-cli/node_modules/yaml/browser/dist/parse/cst-stringify.js +0 -61
  258. package/core/k-cli/node_modules/yaml/browser/dist/parse/cst-visit.js +0 -97
  259. package/core/k-cli/node_modules/yaml/browser/dist/parse/cst.js +0 -98
  260. package/core/k-cli/node_modules/yaml/browser/dist/parse/lexer.js +0 -717
  261. package/core/k-cli/node_modules/yaml/browser/dist/parse/line-counter.js +0 -39
  262. package/core/k-cli/node_modules/yaml/browser/dist/parse/parser.js +0 -967
  263. package/core/k-cli/node_modules/yaml/browser/dist/public-api.js +0 -102
  264. package/core/k-cli/node_modules/yaml/browser/dist/schema/Schema.js +0 -37
  265. package/core/k-cli/node_modules/yaml/browser/dist/schema/common/map.js +0 -17
  266. package/core/k-cli/node_modules/yaml/browser/dist/schema/common/null.js +0 -15
  267. package/core/k-cli/node_modules/yaml/browser/dist/schema/common/seq.js +0 -17
  268. package/core/k-cli/node_modules/yaml/browser/dist/schema/common/string.js +0 -14
  269. package/core/k-cli/node_modules/yaml/browser/dist/schema/core/bool.js +0 -19
  270. package/core/k-cli/node_modules/yaml/browser/dist/schema/core/float.js +0 -43
  271. package/core/k-cli/node_modules/yaml/browser/dist/schema/core/int.js +0 -38
  272. package/core/k-cli/node_modules/yaml/browser/dist/schema/core/schema.js +0 -23
  273. package/core/k-cli/node_modules/yaml/browser/dist/schema/json/schema.js +0 -62
  274. package/core/k-cli/node_modules/yaml/browser/dist/schema/tags.js +0 -96
  275. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/binary.js +0 -58
  276. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/bool.js +0 -26
  277. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/float.js +0 -46
  278. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/int.js +0 -71
  279. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/merge.js +0 -64
  280. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/omap.js +0 -74
  281. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/pairs.js +0 -78
  282. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/schema.js +0 -39
  283. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/set.js +0 -93
  284. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/timestamp.js +0 -101
  285. package/core/k-cli/node_modules/yaml/browser/dist/stringify/foldFlowLines.js +0 -146
  286. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringify.js +0 -129
  287. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyCollection.js +0 -153
  288. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyComment.js +0 -20
  289. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyDocument.js +0 -85
  290. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyNumber.js +0 -24
  291. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyPair.js +0 -150
  292. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyString.js +0 -336
  293. package/core/k-cli/node_modules/yaml/browser/dist/util.js +0 -11
  294. package/core/k-cli/node_modules/yaml/browser/dist/visit.js +0 -233
  295. package/core/k-cli/node_modules/yaml/browser/index.js +0 -5
  296. package/core/k-cli/node_modules/yaml/browser/package.json +0 -3
  297. package/core/k-cli/node_modules/yaml/dist/cli.d.ts +0 -8
  298. package/core/k-cli/node_modules/yaml/dist/cli.mjs +0 -201
  299. package/core/k-cli/node_modules/yaml/dist/compose/compose-collection.d.ts +0 -11
  300. package/core/k-cli/node_modules/yaml/dist/compose/compose-collection.js +0 -90
  301. package/core/k-cli/node_modules/yaml/dist/compose/compose-doc.d.ts +0 -7
  302. package/core/k-cli/node_modules/yaml/dist/compose/compose-doc.js +0 -45
  303. package/core/k-cli/node_modules/yaml/dist/compose/compose-node.d.ts +0 -29
  304. package/core/k-cli/node_modules/yaml/dist/compose/compose-node.js +0 -112
  305. package/core/k-cli/node_modules/yaml/dist/compose/compose-scalar.d.ts +0 -5
  306. package/core/k-cli/node_modules/yaml/dist/compose/compose-scalar.js +0 -88
  307. package/core/k-cli/node_modules/yaml/dist/compose/composer.d.ts +0 -63
  308. package/core/k-cli/node_modules/yaml/dist/compose/composer.js +0 -222
  309. package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-map.d.ts +0 -6
  310. package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-map.js +0 -117
  311. package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-scalar.d.ts +0 -11
  312. package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-scalar.js +0 -200
  313. package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-seq.d.ts +0 -6
  314. package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-seq.js +0 -51
  315. package/core/k-cli/node_modules/yaml/dist/compose/resolve-end.d.ts +0 -6
  316. package/core/k-cli/node_modules/yaml/dist/compose/resolve-end.js +0 -39
  317. package/core/k-cli/node_modules/yaml/dist/compose/resolve-flow-collection.d.ts +0 -7
  318. package/core/k-cli/node_modules/yaml/dist/compose/resolve-flow-collection.js +0 -209
  319. package/core/k-cli/node_modules/yaml/dist/compose/resolve-flow-scalar.d.ts +0 -10
  320. package/core/k-cli/node_modules/yaml/dist/compose/resolve-flow-scalar.js +0 -225
  321. package/core/k-cli/node_modules/yaml/dist/compose/resolve-props.d.ts +0 -23
  322. package/core/k-cli/node_modules/yaml/dist/compose/resolve-props.js +0 -148
  323. package/core/k-cli/node_modules/yaml/dist/compose/util-contains-newline.d.ts +0 -2
  324. package/core/k-cli/node_modules/yaml/dist/compose/util-contains-newline.js +0 -36
  325. package/core/k-cli/node_modules/yaml/dist/compose/util-empty-scalar-position.d.ts +0 -2
  326. package/core/k-cli/node_modules/yaml/dist/compose/util-empty-scalar-position.js +0 -28
  327. package/core/k-cli/node_modules/yaml/dist/compose/util-flow-indent-check.d.ts +0 -3
  328. package/core/k-cli/node_modules/yaml/dist/compose/util-flow-indent-check.js +0 -17
  329. package/core/k-cli/node_modules/yaml/dist/compose/util-map-includes.d.ts +0 -4
  330. package/core/k-cli/node_modules/yaml/dist/compose/util-map-includes.js +0 -15
  331. package/core/k-cli/node_modules/yaml/dist/doc/Document.d.ts +0 -141
  332. package/core/k-cli/node_modules/yaml/dist/doc/Document.js +0 -337
  333. package/core/k-cli/node_modules/yaml/dist/doc/anchors.d.ts +0 -24
  334. package/core/k-cli/node_modules/yaml/dist/doc/anchors.js +0 -76
  335. package/core/k-cli/node_modules/yaml/dist/doc/applyReviver.d.ts +0 -9
  336. package/core/k-cli/node_modules/yaml/dist/doc/applyReviver.js +0 -57
  337. package/core/k-cli/node_modules/yaml/dist/doc/createNode.d.ts +0 -17
  338. package/core/k-cli/node_modules/yaml/dist/doc/createNode.js +0 -90
  339. package/core/k-cli/node_modules/yaml/dist/doc/directives.d.ts +0 -49
  340. package/core/k-cli/node_modules/yaml/dist/doc/directives.js +0 -178
  341. package/core/k-cli/node_modules/yaml/dist/errors.d.ts +0 -21
  342. package/core/k-cli/node_modules/yaml/dist/errors.js +0 -62
  343. package/core/k-cli/node_modules/yaml/dist/index.d.ts +0 -25
  344. package/core/k-cli/node_modules/yaml/dist/index.js +0 -50
  345. package/core/k-cli/node_modules/yaml/dist/log.d.ts +0 -3
  346. package/core/k-cli/node_modules/yaml/dist/log.js +0 -19
  347. package/core/k-cli/node_modules/yaml/dist/nodes/Alias.d.ts +0 -29
  348. package/core/k-cli/node_modules/yaml/dist/nodes/Alias.js +0 -116
  349. package/core/k-cli/node_modules/yaml/dist/nodes/Collection.d.ts +0 -73
  350. package/core/k-cli/node_modules/yaml/dist/nodes/Collection.js +0 -151
  351. package/core/k-cli/node_modules/yaml/dist/nodes/Node.d.ts +0 -53
  352. package/core/k-cli/node_modules/yaml/dist/nodes/Node.js +0 -40
  353. package/core/k-cli/node_modules/yaml/dist/nodes/Pair.d.ts +0 -22
  354. package/core/k-cli/node_modules/yaml/dist/nodes/Pair.js +0 -39
  355. package/core/k-cli/node_modules/yaml/dist/nodes/Scalar.d.ts +0 -43
  356. package/core/k-cli/node_modules/yaml/dist/nodes/Scalar.js +0 -27
  357. package/core/k-cli/node_modules/yaml/dist/nodes/YAMLMap.d.ts +0 -53
  358. package/core/k-cli/node_modules/yaml/dist/nodes/YAMLMap.js +0 -147
  359. package/core/k-cli/node_modules/yaml/dist/nodes/YAMLSeq.d.ts +0 -60
  360. package/core/k-cli/node_modules/yaml/dist/nodes/YAMLSeq.js +0 -115
  361. package/core/k-cli/node_modules/yaml/dist/nodes/addPairToJSMap.d.ts +0 -4
  362. package/core/k-cli/node_modules/yaml/dist/nodes/addPairToJSMap.js +0 -65
  363. package/core/k-cli/node_modules/yaml/dist/nodes/identity.d.ts +0 -23
  364. package/core/k-cli/node_modules/yaml/dist/nodes/identity.js +0 -53
  365. package/core/k-cli/node_modules/yaml/dist/nodes/toJS.d.ts +0 -29
  366. package/core/k-cli/node_modules/yaml/dist/nodes/toJS.js +0 -39
  367. package/core/k-cli/node_modules/yaml/dist/options.d.ts +0 -350
  368. package/core/k-cli/node_modules/yaml/dist/parse/cst-scalar.d.ts +0 -64
  369. package/core/k-cli/node_modules/yaml/dist/parse/cst-scalar.js +0 -218
  370. package/core/k-cli/node_modules/yaml/dist/parse/cst-stringify.d.ts +0 -8
  371. package/core/k-cli/node_modules/yaml/dist/parse/cst-stringify.js +0 -63
  372. package/core/k-cli/node_modules/yaml/dist/parse/cst-visit.d.ts +0 -39
  373. package/core/k-cli/node_modules/yaml/dist/parse/cst-visit.js +0 -99
  374. package/core/k-cli/node_modules/yaml/dist/parse/cst.d.ts +0 -109
  375. package/core/k-cli/node_modules/yaml/dist/parse/cst.js +0 -112
  376. package/core/k-cli/node_modules/yaml/dist/parse/lexer.d.ts +0 -87
  377. package/core/k-cli/node_modules/yaml/dist/parse/lexer.js +0 -719
  378. package/core/k-cli/node_modules/yaml/dist/parse/line-counter.d.ts +0 -22
  379. package/core/k-cli/node_modules/yaml/dist/parse/line-counter.js +0 -41
  380. package/core/k-cli/node_modules/yaml/dist/parse/parser.d.ts +0 -84
  381. package/core/k-cli/node_modules/yaml/dist/parse/parser.js +0 -972
  382. package/core/k-cli/node_modules/yaml/dist/public-api.d.ts +0 -44
  383. package/core/k-cli/node_modules/yaml/dist/public-api.js +0 -107
  384. package/core/k-cli/node_modules/yaml/dist/schema/Schema.d.ts +0 -17
  385. package/core/k-cli/node_modules/yaml/dist/schema/Schema.js +0 -39
  386. package/core/k-cli/node_modules/yaml/dist/schema/common/map.d.ts +0 -2
  387. package/core/k-cli/node_modules/yaml/dist/schema/common/map.js +0 -19
  388. package/core/k-cli/node_modules/yaml/dist/schema/common/null.d.ts +0 -4
  389. package/core/k-cli/node_modules/yaml/dist/schema/common/null.js +0 -17
  390. package/core/k-cli/node_modules/yaml/dist/schema/common/seq.d.ts +0 -2
  391. package/core/k-cli/node_modules/yaml/dist/schema/common/seq.js +0 -19
  392. package/core/k-cli/node_modules/yaml/dist/schema/common/string.d.ts +0 -2
  393. package/core/k-cli/node_modules/yaml/dist/schema/common/string.js +0 -16
  394. package/core/k-cli/node_modules/yaml/dist/schema/core/bool.d.ts +0 -4
  395. package/core/k-cli/node_modules/yaml/dist/schema/core/bool.js +0 -21
  396. package/core/k-cli/node_modules/yaml/dist/schema/core/float.d.ts +0 -4
  397. package/core/k-cli/node_modules/yaml/dist/schema/core/float.js +0 -47
  398. package/core/k-cli/node_modules/yaml/dist/schema/core/int.d.ts +0 -4
  399. package/core/k-cli/node_modules/yaml/dist/schema/core/int.js +0 -42
  400. package/core/k-cli/node_modules/yaml/dist/schema/core/schema.d.ts +0 -1
  401. package/core/k-cli/node_modules/yaml/dist/schema/core/schema.js +0 -25
  402. package/core/k-cli/node_modules/yaml/dist/schema/json/schema.d.ts +0 -2
  403. package/core/k-cli/node_modules/yaml/dist/schema/json/schema.js +0 -64
  404. package/core/k-cli/node_modules/yaml/dist/schema/json-schema.d.ts +0 -69
  405. package/core/k-cli/node_modules/yaml/dist/schema/tags.d.ts +0 -48
  406. package/core/k-cli/node_modules/yaml/dist/schema/tags.js +0 -99
  407. package/core/k-cli/node_modules/yaml/dist/schema/types.d.ts +0 -92
  408. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/binary.d.ts +0 -2
  409. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/binary.js +0 -70
  410. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/bool.d.ts +0 -7
  411. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/bool.js +0 -29
  412. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/float.d.ts +0 -4
  413. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/float.js +0 -50
  414. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/int.d.ts +0 -5
  415. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/int.js +0 -76
  416. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/merge.d.ts +0 -9
  417. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/merge.js +0 -68
  418. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts +0 -22
  419. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/omap.js +0 -77
  420. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/pairs.d.ts +0 -10
  421. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/pairs.js +0 -82
  422. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/schema.d.ts +0 -1
  423. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/schema.js +0 -41
  424. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts +0 -28
  425. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/set.js +0 -96
  426. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/timestamp.d.ts +0 -6
  427. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +0 -105
  428. package/core/k-cli/node_modules/yaml/dist/stringify/foldFlowLines.d.ts +0 -34
  429. package/core/k-cli/node_modules/yaml/dist/stringify/foldFlowLines.js +0 -151
  430. package/core/k-cli/node_modules/yaml/dist/stringify/stringify.d.ts +0 -21
  431. package/core/k-cli/node_modules/yaml/dist/stringify/stringify.js +0 -132
  432. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyCollection.d.ts +0 -17
  433. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyCollection.js +0 -155
  434. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyComment.d.ts +0 -10
  435. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyComment.js +0 -24
  436. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyDocument.d.ts +0 -4
  437. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyDocument.js +0 -87
  438. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyNumber.d.ts +0 -2
  439. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyNumber.js +0 -26
  440. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyPair.d.ts +0 -3
  441. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyPair.js +0 -152
  442. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyString.d.ts +0 -9
  443. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyString.js +0 -338
  444. package/core/k-cli/node_modules/yaml/dist/test-events.d.ts +0 -4
  445. package/core/k-cli/node_modules/yaml/dist/test-events.js +0 -134
  446. package/core/k-cli/node_modules/yaml/dist/util.d.ts +0 -16
  447. package/core/k-cli/node_modules/yaml/dist/util.js +0 -28
  448. package/core/k-cli/node_modules/yaml/dist/visit.d.ts +0 -102
  449. package/core/k-cli/node_modules/yaml/dist/visit.js +0 -236
  450. package/core/k-cli/node_modules/yaml/package.json +0 -97
  451. package/core/k-cli/node_modules/yaml/util.js +0 -2
@@ -1,1332 +0,0 @@
1
- /**
2
- * Formation Runner — with Thalamus-aware activity timeout
3
- * 統一的 Claude subagent 進程啟動器
4
- *
5
- * 4 種啟動方式:
6
- * runSingle — 一般模式(dispatch 用,格式化輸出)
7
- * runHeadless — 無頭模式(prompt 壓縮,原始輸出)
8
- * runLegionLead — Agent Teams 模式(teammate-mode + Task 工具)
9
- * runParallel — Squad 並行啟動
10
- */
11
-
12
- const fs = require('fs');
13
- const path = require('path');
14
- const { spawn, execSync } = require('child_process');
15
- const { ROOT, getClaudeEnv, detectClaudeCmd } = require('./utils');
16
- const { createLogWriter, formatEntry } = require('./logger');
17
- // TASK_1530: shared salvage helper (also used by post-complete/git-merge.js) so the
18
- // worktree-leak detector can recover leaked main-repo changes without duplicating it.
19
- // TASK_1785: readDispatchDirtySnapshot — pre-dispatch dirty snapshot so the leak
20
- // detector only counts paths that became dirty DURING the dispatch window.
21
- const { salvageWorkingTreePaths, readDispatchDirtySnapshot } = require('./git-salvage');
22
- // TASK_1879: canonical shared CARGO_TARGET_DIR for Rust verification — single
23
- // source of truth (was: 12+ templates each inventing a unique C:/t/* dir).
24
- const { verifyTargetDir } = require(path.join(ROOT, 'dev', 'scripts', 'verify-target-dir.js'));
25
-
26
- /**
27
- * Look up which Bifröst adapter declares a given model id.
28
- * Scans .formation/bifrost/<adapter>/models.json files in priority order.
29
- * Returns the adapter name (e.g. 'openai') or null if no match.
30
- *
31
- * Used by spawnClaude to translate dashboard dispatchModel="gpt-5" into
32
- * adapter="openai" without the dashboard backend needing to round-trip.
33
- *
34
- * Empty-string `modelId` is the "adapter default model" sentinel — it
35
- * matches an adapter that explicitly declares an entry with id="" in
36
- * its manifest, so the user can pick "Codex default" without committing
37
- * to a specific gpt-5/o3/etc id.
38
- */
39
- function lookupBifrostAdapterForModel(modelId) {
40
- if (modelId == null) return null;
41
- const bifrostDir = path.join(ROOT, '.formation', 'bifrost');
42
- if (!fs.existsSync(bifrostDir)) return null;
43
- try {
44
- const entries = fs.readdirSync(bifrostDir, { withFileTypes: true });
45
- for (const entry of entries) {
46
- if (!entry.isDirectory() || entry.name === 'shared') continue;
47
- const manifestPath = path.join(bifrostDir, entry.name, 'models.json');
48
- if (!fs.existsSync(manifestPath)) continue;
49
- try {
50
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
51
- if (Array.isArray(manifest.models) && manifest.models.some((m) => m.id === modelId)) {
52
- return manifest.adapter || entry.name;
53
- }
54
- } catch {
55
- // skip malformed manifests
56
- }
57
- }
58
- } catch {
59
- // bifrost dir read failed
60
- }
61
- return null;
62
- }
63
-
64
- /**
65
- * True when a model id belongs to the Claude family — full ids (claude-*) or
66
- * the Claude CLI's bare aliases (opus/sonnet/haiku/opusplan). Anything else is
67
- * a non-claude model that must resolve to a Bifröst adapter. (TASK_1774)
68
- */
69
- function isClaudeFamilyModel(modelId) {
70
- if (!modelId) return false;
71
- return /^claude-/.test(modelId) || /^(opus|sonnet|haiku|opusplan)$/i.test(modelId);
72
- }
73
-
74
- /**
75
- * Read dispatchAdapter from dashboard-models.json — project copy first, then
76
- * core. Returns the adapter name or null. (TASK_1774, defect 1)
77
- */
78
- function readDashboardDispatchAdapter(projectPath) {
79
- const candidates = [];
80
- if (projectPath && path.resolve(projectPath) !== path.resolve(ROOT)) {
81
- candidates.push(path.join(projectPath, '.kenaz', 'dashboard-models.json'));
82
- }
83
- candidates.push(path.join(ROOT, '.kenaz', 'dashboard-models.json'));
84
- for (const p of candidates) {
85
- try {
86
- if (!fs.existsSync(p)) continue;
87
- const dm = JSON.parse(fs.readFileSync(p, 'utf8'));
88
- if (dm.dispatchAdapter) return dm.dispatchAdapter;
89
- } catch { /* malformed or unreadable — try next candidate */ }
90
- }
91
- return null;
92
- }
93
-
94
- /**
95
- * Kill a child process and its entire process tree.
96
- * On Windows with shell: true, child.kill() only kills the shell (cmd.exe),
97
- * leaving the actual claude.exe running as an orphan.
98
- * taskkill /T /F kills the entire tree.
99
- */
100
- function killProcessTree(child) {
101
- if (!child || !child.pid) return;
102
- try {
103
- if (process.platform === 'win32') {
104
- execSync(`taskkill /T /F /PID ${child.pid}`, { stdio: 'ignore', timeout: 10000, windowsHide: true });
105
- } else {
106
- child.kill('SIGTERM');
107
- }
108
- } catch (e) {
109
- // Process may have already exited
110
- try { child.kill(); } catch {}
111
- }
112
- }
113
-
114
- /**
115
- * Post-dispatch worktree leak detector.
116
- *
117
- * After a dispatch completes successfully, verifies that at least one commit
118
- * landed on the task branch. If the task branch has zero new commits AND the
119
- * main repo has uncommitted TRACKED changes, agent writes almost certainly
120
- * leaked to main (the TASK_1402 / TASK_1530 class).
121
- *
122
- * TASK_1530: this used to be WARN-ONLY (marker + console.error). That left the
123
- * leaked content sitting in the main working tree where a later branch-recovery
124
- * force-checkout could permanently discard it, AND the dispatch was still
125
- * reported as a clean success. It now ACTS:
126
- * 1. SALVAGES the leaked main-repo tracked modifications onto a recoverable
127
- * branch via the shared salvageWorkingTreePaths helper (same plumbing the
128
- * post-complete merge path uses — TASK_1529).
129
- * 2. BLOCKS: sets child._formationExitCode = 1 so downstream consumers
130
- * (formations.js onDone, headless.js, post-complete) do NOT treat the
131
- * dispatch as a clean success.
132
- *
133
- * Never throws — detection/salvage errors are caught so they never crash the
134
- * cleanup chain, but a confirmed leak ALWAYS flips the exit code.
135
- *
136
- * @param {string} worktreeDir agent's isolated worktree
137
- * @param {string} originalWorktreeHead HEAD snapshot taken pre-spawn
138
- * @param {object} [child] child process — exit code is flipped on leak
139
- * @param {string} [mainRoot] main-repo root to audit (defaults to ROOT;
140
- * parameterized for the TASK_1530 regression test)
141
- * @returns {object|null} { leaked, salvageBranch, leakedPaths } on leak, null otherwise
142
- */
143
- function detectWorktreeLeak(worktreeDir, originalWorktreeHead, child = null, mainRoot = ROOT) {
144
- if (!worktreeDir || !originalWorktreeHead) return null;
145
- try {
146
- // Use `--not <head>` rather than `^<head>`: on Windows cmd.exe `^` is the
147
- // escape char and gets stripped when execSync spawns through a shell, which
148
- // would list ALL commits and mask the leak (TASK_1530).
149
- const taskCommits = execSync(
150
- `git -C "${worktreeDir}" log --oneline HEAD --not ${originalWorktreeHead}`,
151
- { encoding: 'utf8', timeout: 10000, windowsHide: true }
152
- ).trim();
153
- if (taskCommits) return null; // at least one commit on task branch — no leak
154
-
155
- // Zero commits on task branch: check if main repo has uncommitted changes.
156
- // Use --untracked-files=no: only TRACKED modifications are the salvageable
157
- // data-loss class (untracked peer-task files are correct scope isolation).
158
- // IMPORTANT: do NOT .trim() the whole output — porcelain encodes index/worktree
159
- // status in cols 0-1, and a worktree-only modification is ' M path' (leading
160
- // space). A global trim eats the first line's leading space and shifts the
161
- // slice, corrupting the path (e.g. 'kb.js' → 'b.js'). Strip only the trailing
162
- // newline. (Same class as the raw-vs-trim note in git-merge.js.)
163
- let mainTrackedStatus = '';
164
- try {
165
- mainTrackedStatus = execSync(
166
- `git -C "${mainRoot}" status --porcelain --untracked-files=no`,
167
- { encoding: 'utf8', timeout: 10000, windowsHide: true }
168
- ).replace(/\r?\n$/, '');
169
- } catch { /* main status failed — assume clean */ }
170
-
171
- if (!mainTrackedStatus) return null; // no tracked dirt — no actionable leak
172
-
173
- // Parse the dirty tracked paths (porcelain: 'XY path' or rename 'a -> b').
174
- const _allDirtyPaths = mainTrackedStatus.split('\n')
175
- .map(l => l.replace(/\r$/, ''))
176
- .filter(l => l.length > 0)
177
- .map(l => {
178
- let p = l.slice(3).trim();
179
- const arrow = p.indexOf(' -> ');
180
- if (arrow >= 0) p = p.slice(arrow + 4).trim();
181
- return p.replace(/^"|"$/g, '');
182
- })
183
- .filter(Boolean);
184
-
185
- // TASK_1622 (Req 4): Filter known-churn session-state files from the leak set.
186
- // These files are ALWAYS dirty during a live Ansuz/Muninn session (counters,
187
- // transcripts). They look like leaks but are ambient session churn,
188
- // not agent writes that crossed the worktree boundary. Only count dirt that
189
- // is NOT in this allowlist as a genuine agent leak.
190
- const LEAK_CHURN_ALLOWLIST_PREFIXES = [
191
- '.kenaz/specialist/', // all specialist daemon state files
192
- '.kenaz/transcripts/', // transcript index + session transcripts
193
- '.kenaz/dashboard-models.', // model config updated by dashboard
194
- '.kenaz/efe_snapshots/', // EFE snapshots written by post-complete
195
- '.synapse/heat_cache.', // heat cache flushed every 5 min
196
- '.formation/.synapse/', // guardian counter etc.
197
- '.formation/tmp_', // one-off probe scripts (Muninn, episode writers)
198
- ];
199
- const _isKnownChurn = (p) => {
200
- const norm = p.replace(/[\\]/g, '/');
201
- return LEAK_CHURN_ALLOWLIST_PREFIXES.some(prefix => norm.startsWith(prefix));
202
- };
203
- let leakedPaths = _allDirtyPaths.filter(p => !_isKnownChurn(p));
204
- if (leakedPaths.length === 0) {
205
- // All dirty files are known-churn session state — not a real leak
206
- const _churnPaths = _allDirtyPaths.filter(p => _isKnownChurn(p));
207
- if (_churnPaths.length > 0) {
208
- console.log(`[runner] [TASK_1622] detectWorktreeLeak: ${_churnPaths.length} dirty file(s) are known session-churn (not a leak). First 3: ${_churnPaths.slice(0, 3).join(', ')}`);
209
- }
210
- return null; // only churn — no actionable leak
211
- }
212
-
213
- // TASK_1785 (DEFECT-D): exclude paths that were ALREADY dirty before the
214
- // dispatch started. Zero commits + only pre-existing user WIP is the DEFAULT
215
- // bifrost state (DEFECT-A), NOT an agent leak — without this exclusion the
216
- // user's own dirty files get reported as a leak AND salvaged to a branch
217
- // (TASK_1779 run 1 false positive: 5 user dirty files flagged).
218
- const _dispatchSessionId = (child && child._formation && child._formation.sessionId) || null;
219
- let _preDirtySnapshot = null;
220
- try {
221
- _preDirtySnapshot = readDispatchDirtySnapshot(mainRoot, { sessionId: _dispatchSessionId });
222
- } catch { /* snapshot read must never break leak detection */ }
223
- if (_preDirtySnapshot && Array.isArray(_preDirtySnapshot.dirtyPaths)) {
224
- const _preSet = new Set(_preDirtySnapshot.dirtyPaths.map(p => p.replace(/\\/g, '/')));
225
- const _newlyDirty = leakedPaths.filter(p => !_preSet.has(p.replace(/\\/g, '/')));
226
- const _preExistingCount = leakedPaths.length - _newlyDirty.length;
227
- if (_preExistingCount > 0) {
228
- console.log(`[runner] [TASK_1785] detectWorktreeLeak: ${_preExistingCount} dirty file(s) were already dirty BEFORE dispatch (pre-dispatch snapshot) — excluded from leak set`);
229
- }
230
- if (_newlyDirty.length === 0) {
231
- // Zero commits + only pre-existing dirt: NOT a leak. No salvage branch,
232
- // no LEAK_DETECTED marker, no exit-code flip.
233
- return null;
234
- }
235
- leakedPaths = _newlyDirty;
236
- } else {
237
- // Missing snapshot (older dispatch / capture failure) — degrade to legacy
238
- // semantics with a loud warning. NEVER crash.
239
- console.error(`[runner] [TASK_1785] WARNING: no pre-dispatch dirty snapshot for session ${_dispatchSessionId || '<unknown>'} — legacy leak semantics (pre-existing user WIP may be misattributed as leak)`);
240
- }
241
-
242
- // Leak confirmed: task branch empty + main has non-churn tracked dirt.
243
- const markerFile = path.join(worktreeDir, 'LEAK_DETECTED');
244
- const salvageId = (child && child._formation && (child._formation.sessionId || child._formation.skill)) || 'leak';
245
-
246
- // SALVAGE: preserve the leaked main-repo modifications on a recoverable branch
247
- // BEFORE anything (branch recovery, next dispatch) can force-discard them.
248
- let salvageBranch = null;
249
- try {
250
- salvageBranch = salvageWorkingTreePaths(mainRoot, salvageId, leakedPaths);
251
- } catch { /* salvage failed — fall through, files still in working tree */ }
252
-
253
- const msg = [
254
- `🚨 [runner] WORKTREE LEAK DETECTED — BLOCKING dispatch`,
255
- ` Worktree: ${worktreeDir}`,
256
- ` Task branch has ZERO commits since ${originalWorktreeHead} but main repo has ${leakedPaths.length} dirty tracked file(s).`,
257
- ` Agent writes leaked to MAIN repo. Audit: git -C "${mainRoot}" status`,
258
- salvageBranch
259
- ? ` 💾 Leaked changes salvaged → branch ${salvageBranch} (recover: git checkout ${salvageBranch} -- <file>)`
260
- : ` ⚠️ Could not salvage leaked changes to a branch — they remain in the working tree, review before any force-checkout`,
261
- ` Marker: ${markerFile}`,
262
- ].join('\n');
263
- console.error(msg);
264
-
265
- try {
266
- fs.writeFileSync(markerFile, JSON.stringify({
267
- detected_at: new Date().toISOString(),
268
- worktreeDir,
269
- originalHead: originalWorktreeHead,
270
- leakedPaths,
271
- salvageBranch,
272
- blocked: true,
273
- message: 'Agent dispatch completed with zero commits on task branch while main repo has uncommitted TRACKED changes. Writes leaked to main repo; salvaged to branch and dispatch marked failed.',
274
- }, null, 2), 'utf8');
275
- } catch { /* marker write failed — warning already emitted */ }
276
-
277
- // BLOCK: flip the exit code so the dispatch is not reported as clean success.
278
- if (child) {
279
- child._formationExitCode = 1;
280
- child._formationLeak = { worktreeDir, leakedPaths, salvageBranch };
281
- }
282
-
283
- return { leaked: true, salvageBranch, leakedPaths };
284
- } catch {
285
- return null; /* git error — skip detection silently */
286
- }
287
- }
288
-
289
- // ─────────────────────────────────────────
290
- // 內部:輸出收集
291
- // ─────────────────────────────────────────
292
-
293
- /**
294
- * 收集 claude 進程的 stdout/stderr,寫入 log
295
- * @param {ChildProcess} child
296
- * @param {object} log - createLogWriter 回傳的 log writer
297
- * @param {object} opts
298
- * @param {boolean} opts.silent - 不輸出到 console
299
- * @param {string} opts.memberId - 成員 ID(顯示用)
300
- * @param {boolean} opts.pipeRaw - true=原始 stdout 直通,false=格式化輸出
301
- * @param {boolean} opts.summaryOnly - true=收集摘要資料,結束時輸出結構化摘要到 stdout
302
- * @param {string} opts.coherenceToken - Canary coherence token(用於漂移偵測)
303
- */
304
- function collectOutput(child, log, opts = {}) {
305
- const { silent, memberId, pipeRaw = false, summaryOnly = false, onLog, coherenceToken, rigor, worktreeDir = null, originalWorktreeHead = null } = opts;
306
- let buffer = '';
307
- let resultData = null;
308
- let semanticFailureReason = null;
309
-
310
- const detectSemanticFailure = (text) => {
311
- if (!text) return null;
312
- const normalized = String(text);
313
- if (/hit your limit/i.test(normalized)) return 'usage limit reached';
314
- if (/rate limit/i.test(normalized)) return 'rate limit reached';
315
- if (/too many requests/i.test(normalized)) return 'too many requests';
316
- if (/\b429\b/.test(normalized)) return 'http 429';
317
- // TASK_1774 (defect 2): Claude CLI model-init failure — an unknown/rejected
318
- // model id surfaces as "issue with the selected model" in the result text
319
- // while the CLI still exits 0 after ~1 turn. Must never report SUCCESS.
320
- if (/issue with the selected model/i.test(normalized)) return 'model init failure (selected model rejected)';
321
- return null;
322
- };
323
-
324
- const registerSemanticFailure = (text) => {
325
- const failure = detectSemanticFailure(text);
326
- if (!failure) return null;
327
- semanticFailureReason = semanticFailureReason || failure;
328
- child._formationExitCode = 1;
329
- return semanticFailureReason;
330
- };
331
-
332
- // ── Refusal detector (TASK_1712) ──────────────────────────────────────
333
- // Scans final result text for prompt-injection refusal signatures from safety-hardened
334
- // models (e.g. claude-sonnet-5). A refusal must never log [RESULT] SUCCESS — it is a
335
- // distinct failure mode that burns cost with zero work (confirmed in esp32 heimdall logs).
336
- // Signatures are matched case-insensitively against the full result text.
337
- let refusalReason = null;
338
- const detectRefusal = (text) => {
339
- if (!text || refusalReason) return refusalReason;
340
- const t = String(text);
341
- if (/prompt.{0,10}injection/i.test(t)) return 'prompt injection refusal';
342
- if (/I(?:'m| am) not (?:going to|able to) follow/i.test(t)) return 'explicit refusal (not going to follow)';
343
- if (/(?:won'?t|will not|cannot|not able to) (?:adopt|follow|comply with|act as|pretend|roleplay)/i.test(t)) return 'behavioral refusal';
344
- if (/hallmarks of (?:a )?(?:prompt.{0,5}injection|jailbreak)/i.test(t)) return 'injection-hallmark refusal';
345
- if (/appears to be (?:a )?(?:prompt.{0,5}injection|jailbreak|attempt)/i.test(t)) return 'injection-appearance refusal';
346
- if (/attempting to (?:jailbreak|manipulate|bypass|circumvent)/i.test(t)) return 'jailbreak-attempt refusal';
347
- if (/(?:bypass|override|circumvent).{0,30}(?:guidelines|safety|values|policy|principles)/i.test(t)) return 'bypass-attempt refusal';
348
- if (/I(?:'m| am) unable to (?:assist|help|comply|process) with/i.test(t)) return 'unable-to-assist refusal';
349
- if (/(?:skip|ignore) (?:CLAUDE.md|my guidelines|my values|my training)/i.test(t)) return 'bypass-guidelines refusal';
350
- return null;
351
- };
352
- const registerRefusal = (text) => {
353
- const reason = detectRefusal(text);
354
- if (!reason) return null;
355
- refusalReason = reason;
356
- child._formationExitCode = 1;
357
- child._formationRefusal = reason;
358
- return reason;
359
- };
360
-
361
- // summaryOnly mode: 收集統計資料
362
- const stats = {
363
- toolCounts: {},
364
- errors: [],
365
- startTime: Date.now()
366
- };
367
-
368
- // Activity timeout with process-alive detection (TASK_888 fix)
369
- // Root cause: agents can silently stall (e.g., git state corruption from
370
- // parallel dispatch, context exhaustion) leaving zombie processes.
371
- // However, Opus model thinking can legitimately exceed old timeouts (7min).
372
- //
373
- // New strategy:
374
- // 1. Raised base timeouts: high=15min, medium=12min, low=8min
375
- // 2. When timeout fires, check if child PID is still alive
376
- // 3. If alive → extend (model is thinking), log and reset timer
377
- // 4. Absolute ceiling: 30min no output → kill even if alive (zombie guard)
378
- const _rigor = opts.rigor || 'low';
379
- const ACTIVITY_TIMEOUT_MS = _rigor === 'high' ? 900000 : _rigor === 'medium' ? 720000 : 480000;
380
- const ABSOLUTE_SILENCE_CEILING_MS = 30 * 60 * 1000; // 30 min hard cap
381
- let activityTimer = null;
382
- let lastOutputTime = Date.now();
383
- let extensionCount = 0;
384
- const MAX_EXTENSIONS = 3; // max 3 extensions before hard kill
385
-
386
- const isChildAlive = () => {
387
- if (!child || !child.pid) return false;
388
- try {
389
- // Sending signal 0 checks if process exists without killing it
390
- process.kill(child.pid, 0);
391
- return true;
392
- } catch {
393
- return false;
394
- }
395
- };
396
-
397
- const resetActivityTimer = () => {
398
- if (activityTimer) clearTimeout(activityTimer);
399
- lastOutputTime = Date.now();
400
- extensionCount = 0; // reset extensions on real output
401
- activityTimer = setTimeout(onActivityTimeout, ACTIVITY_TIMEOUT_MS);
402
- };
403
-
404
- const onActivityTimeout = () => {
405
- const tag = memberId ? ` [${memberId}]` : '';
406
- const silenceDuration = Date.now() - lastOutputTime;
407
-
408
- // Hard ceiling: no output for 30 min → kill regardless
409
- if (silenceDuration >= ABSOLUTE_SILENCE_CEILING_MS) {
410
- const msg = `⚠️ Activity hard ceiling${tag}: no output for ${Math.round(silenceDuration / 60000)}min — killing zombie agent`;
411
- console.log(msg);
412
- log.stream.write(`[SYSTEM] ${msg}\n`);
413
- child.emit('formation:done', 1);
414
- setTimeout(() => killProcessTree(child), 3000);
415
- return;
416
- }
417
-
418
- // Check if process is still alive
419
- if (isChildAlive() && extensionCount < MAX_EXTENSIONS) {
420
- extensionCount++;
421
- const msg = `ℹ️ Activity quiet${tag}: no output for ${Math.round(silenceDuration / 60000)}min but process alive — extending (${extensionCount}/${MAX_EXTENSIONS})`;
422
- log.stream.write(`[SYSTEM] ${msg}\n`);
423
- // Re-arm timer for another period
424
- activityTimer = setTimeout(onActivityTimeout, ACTIVITY_TIMEOUT_MS);
425
- return;
426
- }
427
-
428
- // Process dead or max extensions reached → kill
429
- const reason = !isChildAlive() ? 'process dead' : `max extensions (${MAX_EXTENSIONS}) reached`;
430
- const msg = `⚠️ Activity timeout${tag}: no output for ${Math.round(silenceDuration / 60000)}min (${reason}) — killing stalled agent`;
431
- console.log(msg);
432
- log.stream.write(`[SYSTEM] ${msg}\n`);
433
- child.emit('formation:done', 1);
434
- setTimeout(() => killProcessTree(child), 3000);
435
- };
436
-
437
- resetActivityTimer();
438
-
439
- child.stdout.on('data', (data) => {
440
- const str = data.toString();
441
- resetActivityTimer();
442
- if (!silent && pipeRaw && !summaryOnly) process.stdout.write(data);
443
- buffer += str;
444
-
445
- const lines = buffer.split('\n');
446
- buffer = lines.pop();
447
-
448
- for (const line of lines) {
449
- if (!line.trim()) continue;
450
- try {
451
- const entry = JSON.parse(line);
452
- if (entry.type === 'result') resultData = entry;
453
-
454
- // summaryOnly: 收集工具統計
455
- if (summaryOnly && entry.type === 'assistant' && entry.message?.content) {
456
- for (const block of entry.message.content) {
457
- if (block.type === 'tool_use' && block.name) {
458
- stats.toolCounts[block.name] = (stats.toolCounts[block.name] || 0) + 1;
459
- }
460
- }
461
- }
462
-
463
- // ── Canary coherence drift detection ──
464
- // NOTE: Do NOT call registerSemanticFailure on assistant text blocks.
465
- // Agent narration legitimately contains substrings like "rate limit",
466
- // "too many requests", "429" when the task is to *implement* throttling
467
- // (e.g. TASK_1114 dispatch died here while implementing pulse.rs rate_limit).
468
- // Real API termination signals arrive via entry.type === 'result'
469
- // (line ~260) and stderr (line ~280) — those paths still detect legitimately.
470
- if (entry.type === 'assistant' && entry.message?.content) {
471
- for (const block of entry.message.content) {
472
- if (block.type !== 'text' || !block.text) continue;
473
- const text = block.text;
474
- if (!coherenceToken) continue;
475
-
476
- // Agent explicitly reports drift
477
- if (text.includes('##CANARY_DRIFT')) {
478
- child.emit('formation:drift', 'agent_reported');
479
- break;
480
- }
481
-
482
- // Check ##CANARY_CHECK lines
483
- if (text.includes('##CANARY_CHECK:')) {
484
- const checkMatch = text.match(/##CANARY_CHECK:([^:\n]+):([^:\n]+):([^\n]+)/);
485
- if (checkMatch) {
486
- const [, token, taskId, scope] = checkMatch;
487
- if (token.trim() !== coherenceToken) {
488
- child.emit('formation:drift', `token_mismatch:expected=${coherenceToken},got=${token.trim()}`);
489
- }
490
- } else {
491
- // ##CANARY_CHECK present but malformed → drift
492
- child.emit('formation:drift', 'malformed_check');
493
- }
494
- }
495
- }
496
- }
497
-
498
- const formatted = formatEntry(entry);
499
- if (formatted) {
500
- if (log.stream.writable) log.stream.write(formatted + '\n');
501
- if (!silent && !pipeRaw && !summaryOnly) console.log(formatted);
502
- if (onLog) onLog(formatted);
503
- }
504
-
505
- // TASK_1816: surface the effective Bifrost reasoning effort in the
506
- // dispatch log. relay.js puts reasoning_effort on its system/init
507
- // event; logger.js formatSystem only prints model=/cwd=, so without
508
- // this line the effort would be invisible in the log — which is the
509
- // exact silent-default defect this task fixes.
510
- if (entry.type === 'system' && entry.subtype === 'init' && entry.reasoning_effort) {
511
- const effLine = `[SYSTEM][BIFROST] model=${entry.model || 'default'} reasoning_effort=${entry.reasoning_effort}`;
512
- if (log.stream.writable) log.stream.write(effLine + '\n');
513
- if (!silent && !pipeRaw && !summaryOnly) console.log(effLine);
514
- if (onLog) onLog(effLine);
515
- }
516
-
517
- // Windows fix: claude process 有時不自行 exit
518
- // 偵測到 result 就 emit 完成信號 + 延遲 kill 整個進程樹
519
- if (entry.type === 'result') {
520
- registerSemanticFailure(entry.result);
521
- registerRefusal(entry.result); // TASK_1712: detect injection-refusal before emitting done
522
- child.emit('formation:done', (semanticFailureReason || refusalReason) ? 1 : 0);
523
- setTimeout(() => {
524
- killProcessTree(child);
525
- }, 5000);
526
- }
527
- } catch (e) {
528
- if (summaryOnly && line.includes('ERROR')) {
529
- stats.errors.push(line.slice(0, 100));
530
- }
531
- }
532
- }
533
- });
534
-
535
- child.stderr.on('data', (data) => {
536
- if (!silent && !summaryOnly) process.stderr.write(data);
537
- if (summaryOnly) {
538
- const errLine = data.toString().trim();
539
- if (errLine) {
540
- stats.errors.push(errLine.slice(0, 100));
541
- semanticFailureReason = semanticFailureReason || detectSemanticFailure(errLine);
542
- }
543
- }
544
- });
545
-
546
- let closed = false;
547
- const cleanup = (code) => {
548
- if (closed) return;
549
- closed = true;
550
- if (activityTimer) clearTimeout(activityTimer);
551
-
552
- if (buffer.trim()) {
553
- try {
554
- const entry = JSON.parse(buffer);
555
- if (entry.type === 'result') resultData = entry;
556
- const formatted = formatEntry(entry);
557
- if (formatted) {
558
- log.stream.write(formatted + '\n');
559
- if (onLog) onLog(formatted);
560
- }
561
- } catch (e) {}
562
- }
563
-
564
- semanticFailureReason = semanticFailureReason || detectSemanticFailure(resultData?.result);
565
- // TASK_1712: also check for refusal in the final result (in case it wasn't caught mid-stream)
566
- registerRefusal(resultData?.result);
567
-
568
- // TASK_1774 (defect 2): a run that ended with zero turns AND zero cost did no
569
- // work at all (model failed to initialize, adapter mis-route). The CLI exits 0
570
- // in this state — force FAILED instead of a silent no-op SUCCESS.
571
- if (!semanticFailureReason && !refusalReason && resultData
572
- && (resultData.num_turns || 0) === 0 && (resultData.total_cost_usd || 0) === 0) {
573
- semanticFailureReason = 'zero-turn zero-cost run (model init failure)';
574
- child._formationExitCode = 1;
575
- }
576
-
577
- // Post-dispatch worktree integrity check: verify at least one commit landed on
578
- // the task branch. Run BEFORE computing effectiveCode/footer so a confirmed leak
579
- // (TASK_1530) salvages the leaked main-repo changes AND flips _formationExitCode,
580
- // making the footer + summary report FAILED instead of clean success.
581
- // Only checks when the dispatch otherwise looked clean (no semantic failure and
582
- // the agent exited 0) — a leak on an already-failed dispatch is moot.
583
- if (!semanticFailureReason && (code ?? 0) === 0 && worktreeDir && originalWorktreeHead) {
584
- detectWorktreeLeak(worktreeDir, originalWorktreeHead, child);
585
- }
586
-
587
- // _formationLeak is set by detectWorktreeLeak when it flips the exit code.
588
- const leaked = !!child._formationLeak;
589
- const effectiveCode = (semanticFailureReason || refusalReason || leaked) ? 1 : (code ?? 0);
590
- child._formationExitCode = effectiveCode;
591
-
592
- try { log.writeFooter(effectiveCode); } catch (e) {}
593
-
594
- // summaryOnly: 輸出結構化摘要
595
- // Wrapped in try-catch: console.log can throw EPIPE if parent (Dashboard)
596
- // closed stdout pipe. An uncaught throw here kills the EventEmitter chain,
597
- // preventing downstream listeners (scheduler's done()) from firing → hang.
598
- try {
599
- if (summaryOnly) {
600
- const duration = ((Date.now() - stats.startTime) / 1000).toFixed(1);
601
- // TASK_1712: REFUSED is a distinct status from FAILED — refusal burns cost with zero work
602
- const status = effectiveCode === 0 ? 'SUCCESS' : (refusalReason ? 'REFUSED' : 'FAILED');
603
- const formation = child._formation?.formation || 'unknown';
604
- const skill = child._formation?.skill || 'unknown';
605
- const sessionId = child._formation?.sessionId || 'unknown';
606
- const cost = resultData?.total_cost_usd?.toFixed(4) || '0.0000';
607
- const turns = resultData?.num_turns || 0;
608
-
609
- console.log('═══ Formation Result ═══');
610
- console.log(`Formation: ${formation} | Skill: ${skill} | Session: ${sessionId}`);
611
- console.log(`Status: ${status} (exit ${effectiveCode})`);
612
- console.log(`Duration: ${duration}s | Cost: $${cost} | Turns: ${turns}`);
613
- if (semanticFailureReason) {
614
- console.log(`Failure: ${semanticFailureReason}`);
615
- // TASK_1774: greppable failure marker — parity with [RESULT] REFUSED below
616
- console.log(`[RESULT] FAILED: ${semanticFailureReason}`);
617
- }
618
- if (refusalReason) {
619
- console.log(`[RESULT] REFUSED: ${refusalReason}`);
620
- }
621
- if (child._formationLeak) {
622
- const lk = child._formationLeak;
623
- console.log(`Failure: worktree leak — ${lk.leakedPaths.length} file(s) leaked to main repo${lk.salvageBranch ? `, salvaged → ${lk.salvageBranch}` : ' (salvage failed)'}`);
624
- }
625
-
626
- const toolEntries = Object.entries(stats.toolCounts);
627
- if (toolEntries.length > 0) {
628
- const toolStr = toolEntries.map(([name, count]) => `${name}×${count}`).join(', ');
629
- console.log(`Tools: ${toolStr}`);
630
- }
631
-
632
- console.log(`Log: ${log.path}`);
633
-
634
- if (stats.errors.length > 0) {
635
- console.log('Errors:');
636
- stats.errors.slice(0, 10).forEach(err => {
637
- console.log(` - ${err}`);
638
- });
639
- }
640
-
641
- console.log('═══════════════════════');
642
- } else if (!silent) {
643
- const tag = memberId ? ` [${memberId}]` : '';
644
- console.log(`\n⚔️ Done${tag} (exit ${code})`);
645
- console.log(` Log: ${log.path}`);
646
- }
647
- } catch (e) {
648
- // EPIPE or other console error — non-fatal, don't block EventEmitter chain
649
- }
650
- // NOTE: the worktree-leak check now runs ABOVE (before effectiveCode is
651
- // computed) so a confirmed leak salvages + blocks the dispatch (TASK_1530).
652
- };
653
-
654
- child.on('close', cleanup);
655
- child.on('exit', cleanup);
656
- child.on('formation:done', cleanup);
657
-
658
- child._resultData = () => resultData;
659
- }
660
-
661
- // ─────────────────────────────────────────
662
- // 內部:建立 claude 進程
663
- // ─────────────────────────────────────────
664
-
665
- /**
666
- * TASK_1841: Probe `claude --help` exactly ONCE per process and reuse the result.
667
- * Previously spawnClaude ran up to two independent execSync probes per dispatch
668
- * (--project-dir worktree branch + sub-project branch); every extra probe is a
669
- * fixed per-dispatch cost, and duplicated probes drift (TASK_1839 pattern:
670
- * "same protocol, three independent implementations").
671
- *
672
- * Returns { helpText, cliVersion }. On probe failure helpText is '' — callers
673
- * must treat unknown capability as UNSUPPORTED and fail/warn loudly, never
674
- * silently skip a flag the dispatch depends on (that silent-skip is exactly
675
- * how the dead --project-dir compensation branch masked the MCP gap).
676
- */
677
- let _claudeHelpCache = null;
678
- function probeClaudeHelp(claudeCmd) {
679
- if (_claudeHelpCache && _claudeHelpCache.cmd === claudeCmd) return _claudeHelpCache;
680
- let helpText = '';
681
- let cliVersion = 'unknown version';
682
- try {
683
- helpText = execSync(`${claudeCmd} --help 2>&1`, { encoding: 'utf8', timeout: 5000, windowsHide: true });
684
- // `--help` prints no version; `--version` prints e.g. "2.1.288 (Claude Code)".
685
- try {
686
- const verText = execSync(`${claudeCmd} --version 2>&1`, { encoding: 'utf8', timeout: 5000, windowsHide: true });
687
- const verMatch = verText.match(/(\d+\.\d+\.\d+)/);
688
- if (verMatch) cliVersion = verMatch[1];
689
- } catch {
690
- // version is informational only; capability checks use helpText
691
- }
692
- } catch {
693
- // claude --help failed — helpText stays ''; capability checks below report unsupported.
694
- // Do NOT cache the failure: a timeout/spawn error is a transient condition,
695
- // not a CLI capability fact — caching it would poison every later dispatch
696
- // in this process. Return uncached so the next call re-probes.
697
- return { cmd: claudeCmd, helpText, cliVersion };
698
- }
699
- _claudeHelpCache = { cmd: claudeCmd, helpText, cliVersion };
700
- return _claudeHelpCache;
701
- }
702
-
703
- /**
704
- * 統一的進程啟動器
705
- * 所有 run* 函數都透過此函數建立進程
706
- * Bifröst: 當 BIFROST_ADAPTER 環境變數存在時,路由到對應的 relay
707
- */
708
- function spawnClaude(opts) {
709
- // ── Adapter routing precedence (highest first) ─────────────────────────
710
- // 1. Explicit `bifrostAdapter` opt (passed by callers that already
711
- // decided which adapter to use, e.g. dispatch.js with task metadata)
712
- // 2. Dashboard dispatch model: when opts.model is set AND it does NOT
713
- // look like a Claude id (claude-*), assume it is a Bifröst model and
714
- // look up which adapter exposes it via .formation/bifrost/<name>/models.json.
715
- // Empty-string ids (the "use adapter default model" sentinel) are
716
- // matched directly via lookupBifrostAdapterForModel which scans
717
- // every manifest for an entry with id="".
718
- // 3. BIFROST_ADAPTER env var (legacy session inheritance — kept as
719
- // fallback for callers that have not been migrated to opts.model)
720
- // 4. CODEX_THREAD_ID env var (legacy auto-detect — same fallback)
721
- // 5. null → native Claude CLI
722
- const explicitAdapter = opts.bifrostAdapter?.trim();
723
- let adapter = explicitAdapter || null;
724
-
725
- // opts.model can be undefined, null, '', 'bifrost', or a real id.
726
- // Treat undefined/null as "no override" (let the adapter decide),
727
- // empty string as the "default model for the resolved adapter" sentinel,
728
- // and 'bifrost' as the dispatch-family sentinel (NOT a real model name).
729
- const rawModel = opts.model == null ? null : String(opts.model);
730
- const isBifrostSentinel = rawModel === 'bifrost';
731
- const modelStr = isBifrostSentinel ? null : rawModel;
732
- const isEmptySentinel = modelStr === '';
733
-
734
- if (!adapter && modelStr != null && !isClaudeFamilyModel(modelStr)) {
735
- adapter = lookupBifrostAdapterForModel(modelStr);
736
- // TASK_1774 (defect 1): a real non-claude model id whose manifest lookup
737
- // missed (e.g. a new Codex id not yet declared in any
738
- // .formation/bifrost/*/models.json) must still route through Bifröst —
739
- // consult dashboard-models.json dispatchAdapter (project first, then core)
740
- // before giving up. Empty-string sentinel is excluded: it is matched only
741
- // against explicit id="" manifest entries above.
742
- if (!adapter && modelStr !== '') {
743
- adapter = readDashboardDispatchAdapter(opts.projectPath);
744
- }
745
- }
746
-
747
- // 'bifrost' sentinel: resolve adapter from dashboard-models.json dispatchAdapter
748
- // (project copy first, then core — TASK_1774)
749
- if (!adapter && isBifrostSentinel) {
750
- adapter = readDashboardDispatchAdapter(opts.projectPath);
751
- }
752
-
753
- if (!adapter) {
754
- adapter = process.env.BIFROST_ADAPTER || null;
755
- }
756
- if (!adapter && process.env.CODEX_THREAD_ID) {
757
- adapter = 'openai';
758
- }
759
-
760
- // Guard: claude-family models MUST use native Claude CLI, never bifrost.
761
- // A stale .bifrost-active signal or BIFROST_ADAPTER env var can set
762
- // adapter='claude', but .formation/bifrost/claude/ has no relay.js →
763
- // falls back to openai relay → openai can't handle claude models → 0 turns.
764
- if (adapter && modelStr && isClaudeFamilyModel(modelStr)) {
765
- adapter = null;
766
- }
767
-
768
- // TASK_1774 (defect 1): NEVER silently degrade a non-claude model to the
769
- // native Claude CLI. The CLI rejects the model after burning a turn yet exits
770
- // 0, masking the mis-route as SUCCESS. Fail fast and loud instead.
771
- if (!adapter && modelStr && !isClaudeFamilyModel(modelStr)) {
772
- const msg = '[runner] ❌ [RESULT] FAILURE — no Bifröst adapter resolves model "' + modelStr + '". '
773
- + 'Declare it in .formation/bifrost/<adapter>/models.json or set dispatchAdapter in '
774
- + '.kenaz/dashboard-models.json. Refusing silent fallback to native Claude CLI.';
775
- console.error(msg);
776
- throw new Error('No Bifröst adapter for non-claude model "' + modelStr + '" — refusing silent Claude CLI fallback');
777
- }
778
-
779
- // Empty-string sentinel means "use the adapter's default model" — strip
780
- // it from opts.model so spawnBifrost / spawnClaude do not pass --model.
781
- // 'bifrost' sentinel is a dispatch-family marker, not a real model — also strip.
782
- if (isEmptySentinel || isBifrostSentinel) {
783
- opts = { ...opts, model: null };
784
- }
785
-
786
- if (adapter) {
787
- return spawnBifrost(opts, adapter);
788
- }
789
- const {
790
- prompt,
791
- formation,
792
- skill,
793
- sessionId,
794
- memberId,
795
- silent,
796
- outputPath,
797
- cleanPrompt: shouldClean = false,
798
- allowedTools = [
799
- 'Bash,Read,Edit,Write,Glob,Grep,ToolSearch',
800
- // Code navigation (LSP) + Synapse memory
801
- 'mcp__kenaz-dashboard__kenaz_lsp_callers',
802
- 'mcp__kenaz-dashboard__kenaz_lsp_references',
803
- 'mcp__kenaz-dashboard__kenaz_lsp_definition',
804
- 'mcp__kenaz-dashboard__kenaz_lsp_symbols',
805
- 'mcp__kenaz-dashboard__synapse_recall',
806
- 'mcp__kenaz-dashboard__synapse_status',
807
- 'mcp__kenaz-dashboard__synapse_sigils',
808
- 'mcp__kenaz-dashboard__synapse_health',
809
- 'mcp__kenaz-dashboard__synapse_rune_conflicts',
810
- 'mcp__kenaz-dashboard__synapse_scan_stale_runes',
811
- 'mcp__kenaz-dashboard__synapse_inscribe',
812
- 'mcp__kenaz-dashboard__synapse_forge',
813
- 'mcp__kenaz-dashboard__synapse_clustering_log',
814
- // Hippocampus — episodic memory
815
- 'mcp__kenaz-dashboard__hippocampus_episode_start',
816
- 'mcp__kenaz-dashboard__hippocampus_episode_append',
817
- 'mcp__kenaz-dashboard__hippocampus_episode_close',
818
- 'mcp__kenaz-dashboard__hippocampus_episode_link',
819
- 'mcp__kenaz-dashboard__hippocampus_episode_recall',
820
- ].join(','),
821
- extraArgs = [],
822
- pipeRaw = false,
823
- summaryOnly = false,
824
- onLog,
825
- projectPath = null,
826
- coherenceToken = null,
827
- worktreeDir = null,
828
- model = null,
829
- rigor = null,
830
- // TASK_1929 (Agent Hub M2): extra --mcp-config file(s), appended AFTER
831
- // the core .mcp.json push below (claude's --mcp-config is variadic —
832
- // `claude --help`: "Load MCP servers from JSON files or strings
833
- // (space-separated)", verified on 2.1.259). Empty array (the default)
834
- // = zero behavior change for every pre-existing call site.
835
- extraMcpConfigs = [],
836
- } = opts;
837
-
838
- const log = createLogWriter({ formation, skill, sessionId, outputPath, memberId, projectPath });
839
- log.writeHeader();
840
-
841
- if (!silent) {
842
- console.log(` Log: ${log.path}`);
843
- if (projectPath) console.log(` Project: ${projectPath}`);
844
- if (worktreeDir) console.log(` Worktree: ${worktreeDir}`);
845
- console.log(` Launching${memberId ? ` [${memberId}]` : ''}...`);
846
- }
847
-
848
- const claudeCmd = detectClaudeCmd();
849
- const args = [
850
- '-p', '-',
851
- '--dangerously-skip-permissions',
852
- '--output-format', 'stream-json',
853
- '--verbose',
854
- '--allowedTools', allowedTools,
855
- ...extraArgs
856
- ];
857
-
858
- // Model override: when provided, use the specified model instead of the CLI default.
859
- if (model) {
860
- args.push('--model', model);
861
- }
862
-
863
- // worktreeDir takes priority: Squad parallel members each work in their own isolated worktree.
864
- // Falls back to projectPath, then ROOT.
865
- const cwd = worktreeDir || projectPath || ROOT;
866
-
867
- // Single per-process capability probe — reused by every check below (TASK_1841).
868
- const claudeHelp = probeClaudeHelp(claudeCmd);
869
-
870
- // TASK_1841: the kenaz-dashboard MCP server is declared ONLY in the
871
- // project-scoped .mcp.json at the core repo root (type=http, port 18080).
872
- // Claude Code resolves project-scoped MCP config from cwd, so any dispatch
873
- // whose cwd is NOT the core root — cross-project (projectPath = sub-project,
874
- // which only carries .kenaz/) — spawns a child with ZERO kenaz-dashboard
875
- // tools, while the --allowedTools list above pretends they exist. There is
876
- // nothing to "inherit" either: ~/.claude.json has no kenaz-dashboard entry.
877
- // Passing --mcp-config with an absolute path makes MCP registration
878
- // cwd-independent for both the worktree and cross-project modes.
879
- const mcpConfigPath = path.join(ROOT, '.mcp.json');
880
- if (!fs.existsSync(mcpConfigPath)) {
881
- // Core install incomplete — fail loudly, never silently dispatch an agent
882
- // that cannot reach synapse_*/kenaz_* tools (Requirement: no silent skip).
883
- const msg = `[runner] ❌ [RESULT] FAILURE — MCP config missing: ${mcpConfigPath}. `
884
- + 'The dispatched agent would have no kenaz-dashboard MCP tools. '
885
- + 'Restore .mcp.json at the core repo root before dispatching.';
886
- console.error(msg);
887
- throw new Error(`MCP config missing: ${mcpConfigPath} — refusing to dispatch a tool-less agent`);
888
- }
889
- if (!claudeHelp.helpText.includes('--mcp-config')) {
890
- // Capability probe says the CLI cannot take an explicit MCP config. Fail
891
- // loudly instead of degrading: the previous --project-dir compensation was
892
- // wrapped in exactly this kind of probe, the probe never matched, and the
893
- // branch shipped as permanent dead code (this bug's root cause; see also
894
- // TASK_945 / TASK_1402 for prior "assumed the flag exists" incidents).
895
- const msg = `[runner] ❌ [RESULT] FAILURE — claude CLI ${claudeHelp.cliVersion} does not advertise --mcp-config; `
896
- + 'cannot register kenaz-dashboard MCP for a headless agent whose cwd is outside the core repo. '
897
- + 'Upgrade the claude CLI. Refusing silent degraded dispatch.';
898
- console.error(msg);
899
- throw new Error(`claude CLI lacks --mcp-config — refusing silent degraded dispatch (${claudeHelp.cliVersion})`);
900
- }
901
- // NOTE: deliberately NOT passing --strict-mcp-config — it would disable every
902
- // other MCP server the user configured (behavior change, out of scope).
903
- args.push('--mcp-config', mcpConfigPath);
904
- // TASK_1929: additional MCP config file(s) — appended to the SAME
905
- // --mcp-config flag (claude's --mcp-config is variadic: `<configs...>`),
906
- // never a second --mcp-config flag. No-op when extraMcpConfigs is empty
907
- // (the default for every dispatch path except agent_hub/launch.rs).
908
- if (extraMcpConfigs.length > 0) {
909
- args.push(...extraMcpConfigs);
910
- }
911
-
912
- // --project-dir controls where Claude Code resolves tool paths (Edit/Write/Read).
913
- // CRITICAL: When running in a worktree, --project-dir MUST point to the worktree,
914
- // NOT ROOT. Otherwise Claude Code's Edit/Write will write to the main repo,
915
- // causing "worktree path leakage" (TASK_945).
916
- //
917
- // TASK_1841: the former sub-project branch (`else if (cwd !== ROOT ...)` pushing
918
- // `--project-dir ROOT`) was removed — `claude --help` has NEVER advertised
919
- // --project-dir, so its helpText gate never passed and the push never executed
920
- // once. Cross-project config access is now handled by --mcp-config above.
921
- // The worktree branch below is kept (belt-and-braces for future CLI versions)
922
- // because its failure mode is a loud warn, not a silent skip.
923
- let projectDirFlagSupported = false;
924
- if (worktreeDir) {
925
- // Worktree mode: point --project-dir to the worktree so all file operations
926
- // (Edit, Write, Read, Glob, Grep) resolve paths within the worktree.
927
- projectDirFlagSupported = claudeHelp.helpText.includes('--project-dir');
928
- if (projectDirFlagSupported) {
929
- args.push('--project-dir', worktreeDir);
930
- } else {
931
- // When --project-dir is absent the flag is silently dropped (see TASK_1402).
932
- // CLAUDE_PROJECT_DIR env var (set below) is the fallback. Warn so the operator
933
- // knows isolation relies solely on the env var and can upgrade if needed.
934
- // No released CLI has advertised --project-dir (TASK_1841), so this is the normal path,
935
- // not a degraded one: an always-firing WARNING with "upgrade" advice only trained readers to ignore it.
936
- console.log(`[runner] worktree isolation via CLAUDE_PROJECT_DIR + cwd (claude CLI ${claudeHelp.cliVersion} has no --project-dir flag)`);
937
- }
938
- }
939
-
940
- const env = getClaudeEnv();
941
-
942
- // Headless 模式:告知 task-api.js 跳過 inline post-complete
943
- // (由 headless.js onDone 統一處理,避免並行 agent 的 git 衝突)
944
- env.FORMATION_HEADLESS = '1';
945
-
946
- // TASK_945: Set FORMATION_WORKTREE_PATH so downstream tools (task-api.js, prompt.js,
947
- // post-complete.js) can detect worktree mode and resolve paths correctly.
948
- if (worktreeDir) {
949
- env.FORMATION_WORKTREE_PATH = worktreeDir;
950
- // CLAUDE_PROJECT_DIR is the environment-variable counterpart to --project-dir.
951
- // It forces Claude Code's Edit/Write/Read path resolver to anchor inside the
952
- // worktree instead of walking .git upward to discover the main repo.
953
- //
954
- // WHY this is required even when --project-dir is also pushed to args:
955
- // claude CLI 2.1.x (current as of 2026-05-02) does NOT support --project-dir.
956
- // runner.js:spawnClaude checks via `claude --help` and silently omits the flag
957
- // when it is absent. Without CLAUDE_PROJECT_DIR, the fall-through behaviour is
958
- // for Claude Code to discover the main repo via .git walk-up from cwd — causing
959
- // all Edit/Write operations to mutate the main repo instead of the worktree.
960
- // This is invisible until a post-dispatch `git worktree list` audit (TASK_1402
961
- // incident: TASK_1400 dispatch wrote ALL files to main, ZERO to task branch).
962
- //
963
- // Set unconditionally when worktreeDir is set, regardless of --project-dir flag
964
- // support. Both mechanisms coexist safely — future CLI versions that support the
965
- // flag will still honour the env var as belt-and-braces isolation.
966
- env.CLAUDE_PROJECT_DIR = worktreeDir;
967
- // Windows MAX_PATH fix: worktree paths add ~40 chars, pushing KuzuDB CMake
968
- // object paths past 260 chars. TASK_1879: use the canonical shared verify
969
- // target dir (dev/scripts/verify-target-dir.js) — NOT the project root's
970
- // target/, which the live tauri dev process locks (onnxruntime.dll,
971
- // os error 32). Cargo serializes concurrent access via file locks:
972
- // "Blocking waiting for file lock" at ~0% CPU is waiting, not a hang.
973
- if (process.platform === 'win32') {
974
- env.CARGO_TARGET_DIR = verifyTargetDir();
975
- }
976
- }
977
-
978
- // 子專案 git 隔離:GIT_CEILING_DIRECTORIES 阻止 git 向上遍歷到 Core 專案
979
- // 這是硬性防禦 — 即使 subagent 直接跑 git 命令,也不會影響 KenazAI 的 repo
980
- if (projectPath && projectPath !== ROOT) {
981
- const parentDir = require('path').dirname(projectPath);
982
- env.GIT_CEILING_DIRECTORIES = parentDir;
983
- }
984
-
985
- // Windows shell rules:
986
- // .exe → shell:false (avoids cmd.exe popup windows for background agents)
987
- // .cmd → shell:true (.cmd is a batch script — requires cmd.exe to interpret)
988
- // windowsHide:true sets CREATE_NO_WINDOW flag for BOTH shell and non-shell spawn.
989
- // Non-Windows: always shell:true for PATH resolution.
990
- const useShell = process.platform !== 'win32'
991
- || claudeCmd.endsWith('.cmd');
992
- const spawnOpts = {
993
- cwd,
994
- env,
995
- stdio: ['pipe', 'pipe', 'pipe'],
996
- windowsHide: true,
997
- shell: useShell,
998
- };
999
- // Snapshot the worktree HEAD before launching so the post-dispatch leak detector
1000
- // can confirm that new commits appeared on the task branch. Captured here (pre-spawn)
1001
- // to get a clean baseline before the agent makes any changes.
1002
- let originalWorktreeHead = null;
1003
- if (worktreeDir) {
1004
- try {
1005
- originalWorktreeHead = execSync(
1006
- `git -C "${worktreeDir}" rev-parse HEAD`,
1007
- { encoding: 'utf8', timeout: 5000, windowsHide: true }
1008
- ).trim();
1009
- } catch { /* git rev-parse failed — leak detection disabled for this dispatch */ }
1010
- }
1011
-
1012
- // TASK_1929: opt-in, zero-cost args dump for proving no regression in the
1013
- // assembled command line — never set by any real dispatch path. Exits
1014
- // BEFORE spawning claude (no billed API call), so it's safe to run
1015
- // before/after a change and diff the JSON.
1016
- if (process.env.KENAZ_DEBUG_SPAWN_ARGS === '1') {
1017
- console.log(JSON.stringify({ cmd: claudeCmd, args }));
1018
- process.exit(0);
1019
- }
1020
-
1021
- const child = spawn(claudeCmd, args, spawnOpts);
1022
-
1023
- let finalPrompt = shouldClean
1024
- ? prompt.replace(/\r?\n/g, ' ').replace(/\s+/g, ' ').trim()
1025
- : prompt;
1026
-
1027
- // TASK_945: Inject explicit worktree path instruction into the prompt so the agent
1028
- // uses the worktree path for ALL file operations (Edit/Write/Read/Glob/Grep).
1029
- // This is a defense-in-depth complement to --project-dir pointing to the worktree.
1030
- if (worktreeDir) {
1031
- const worktreeInstruction = `\n\n[WORKTREE] All file operations (Edit, Write, Read, Glob, Grep) MUST use paths under: ${worktreeDir}\nDo NOT use paths under the main repo. Your working directory is the worktree.\n\n`;
1032
- finalPrompt = worktreeInstruction + finalPrompt;
1033
- }
1034
-
1035
- child.stdin.write(finalPrompt);
1036
- child.stdin.end();
1037
-
1038
- collectOutput(child, log, { silent, memberId, pipeRaw, summaryOnly, onLog, coherenceToken, rigor, worktreeDir, originalWorktreeHead });
1039
-
1040
- child._formation = { logPath: log.path, memberId, log, formation, skill, sessionId, adapter };
1041
- return child;
1042
- }
1043
-
1044
- // ─────────────────────────────────────────
1045
- // Bifröst: relay to non-Claude adapters
1046
- // ─────────────────────────────────────────
1047
-
1048
- /**
1049
- * Spawn agent via Bifröst relay (e.g. Codex).
1050
- * Same interface as spawnClaude — stdin prompt, stdout stream-json.
1051
- */
1052
- function spawnBifrost(opts, adapter) {
1053
- const {
1054
- prompt, formation, skill, sessionId, memberId, silent,
1055
- outputPath, cleanPrompt: shouldClean = false,
1056
- summaryOnly = false, onLog, projectPath = null,
1057
- coherenceToken = null, worktreeDir = null, model = null, rigor = null,
1058
- // TASK_1929 (Agent Hub M2): relay.js has no equivalent of claude's
1059
- // variadic --mcp-config — each path is passed as its own
1060
- // --mcp-config-extra <path> CLI arg and relay.js reads+translates the
1061
- // file itself (see relay.js buildCodexMcpOverrides). Empty array (the
1062
- // default) = zero behavior change for every pre-existing bifrost call.
1063
- extraMcpConfigs = [],
1064
- } = opts;
1065
-
1066
- const log = createLogWriter({ formation, skill, sessionId, outputPath, memberId, projectPath });
1067
- log.writeHeader();
1068
-
1069
- if (!silent) {
1070
- console.log(` [bifrost] Adapter: ${adapter}`);
1071
- console.log(` Log: ${log.path}`);
1072
- if (projectPath) console.log(` Project: ${projectPath}`);
1073
- console.log(` Launching${memberId ? ` [${memberId}]` : ''}...`);
1074
- }
1075
-
1076
- let relayScript = path.join(ROOT, '.formation', 'bifrost', adapter, 'relay.js');
1077
-
1078
- // Guard: if resolved adapter has no relay.js, try dashboard-models.json
1079
- // dispatchAdapter, then fall back to 'openai' which is the only adapter
1080
- // with an actual relay implementation.
1081
- if (!fs.existsSync(relayScript)) {
1082
- const origAdapter = adapter;
1083
- let fallback = null;
1084
- try {
1085
- const dmPath = path.join(ROOT, '.kenaz', 'dashboard-models.json');
1086
- if (fs.existsSync(dmPath)) {
1087
- const dm = JSON.parse(fs.readFileSync(dmPath, 'utf8'));
1088
- if (dm.dispatchAdapter && dm.dispatchAdapter !== adapter) {
1089
- fallback = dm.dispatchAdapter;
1090
- }
1091
- }
1092
- } catch { /* ignore */ }
1093
- adapter = fallback || 'openai';
1094
- relayScript = path.join(ROOT, '.formation', 'bifrost', adapter, 'relay.js');
1095
- console.warn(` [bifrost] ⚠️ No relay at ${origAdapter}/relay.js — falling back to ${adapter}`);
1096
- }
1097
-
1098
- const relayArgs = ['--cwd', worktreeDir || projectPath || ROOT];
1099
- if (model) relayArgs.push('--model', model);
1100
- // TASK_1929: see the destructured extraMcpConfigs comment above.
1101
- for (const p of extraMcpConfigs) relayArgs.push('--mcp-config-extra', p);
1102
-
1103
- const cwd = worktreeDir || projectPath || ROOT;
1104
- const env = { ...getClaudeEnv(), BIFROST_ADAPTER: adapter };
1105
- env.FORMATION_HEADLESS = '1';
1106
- if (worktreeDir) {
1107
- env.FORMATION_WORKTREE_PATH = worktreeDir;
1108
- // TASK_1530: anchor the Codex/OpenAI adapter's file writes to the worktree.
1109
- // Parity with spawnClaude (~659): without CLAUDE_PROJECT_DIR, relay.js
1110
- // resolveProjectPath() (bifrost/openai/relay.js:91-93) falls back to
1111
- // `cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd()`. relay.js already
1112
- // passes --cwd=worktreeDir as the codex spawn cwd, but setting the env var
1113
- // here is belt-and-braces isolation: any tooling that consults the env var
1114
- // (and any future relay that drops --cwd) still resolves inside the worktree
1115
- // instead of walking .git upward to the MAIN repo. The unset env var was the
1116
- // confirmed leak vector behind the TASK_1530 external-user data-loss incident.
1117
- env.CLAUDE_PROJECT_DIR = worktreeDir;
1118
- // Windows MAX_PATH fix: same as Claude native path above (TASK_1879:
1119
- // canonical shared verify target dir, see dev/scripts/verify-target-dir.js).
1120
- if (process.platform === 'win32') {
1121
- env.CARGO_TARGET_DIR = verifyTargetDir();
1122
- }
1123
- }
1124
-
1125
- // TASK_1530: snapshot the worktree HEAD before launching so the post-dispatch
1126
- // leak detector can confirm new commits landed on the task branch. Mirrors
1127
- // spawnClaude (~690-701) — without this, detectWorktreeLeak never fired for
1128
- // Bifrost dispatches (originalWorktreeHead was never captured here).
1129
- let originalWorktreeHead = null;
1130
- if (worktreeDir) {
1131
- try {
1132
- originalWorktreeHead = execSync(
1133
- `git -C "${worktreeDir}" rev-parse HEAD`,
1134
- { encoding: 'utf8', timeout: 5000, windowsHide: true }
1135
- ).trim();
1136
- } catch { /* git rev-parse failed — leak detection disabled for this dispatch */ }
1137
- }
1138
-
1139
- // TASK_1929: same opt-in args dump as spawnClaude above — exits before
1140
- // spawning the relay (no codex/API call), safe for a before/after diff.
1141
- if (process.env.KENAZ_DEBUG_SPAWN_ARGS === '1') {
1142
- console.log(JSON.stringify({ cmd: 'node', args: [relayScript, ...relayArgs] }));
1143
- process.exit(0);
1144
- }
1145
-
1146
- const child = spawn('node', [relayScript, ...relayArgs], {
1147
- cwd,
1148
- env,
1149
- stdio: ['pipe', 'pipe', 'pipe'],
1150
- windowsHide: true,
1151
- });
1152
-
1153
- let finalPrompt = shouldClean
1154
- ? prompt.replace(/\r?\n/g, ' ').replace(/\s+/g, ' ').trim()
1155
- : prompt;
1156
-
1157
- if (worktreeDir) {
1158
- finalPrompt = `\n\n[WORKTREE] All file operations MUST use paths under: ${worktreeDir}\n\n` + finalPrompt;
1159
- }
1160
-
1161
- child.stdin.write(finalPrompt);
1162
- child.stdin.end();
1163
-
1164
- collectOutput(child, log, { silent, memberId, pipeRaw: false, summaryOnly, onLog, coherenceToken, rigor, worktreeDir, originalWorktreeHead });
1165
-
1166
- child._formation = { logPath: log.path, memberId, log, formation, skill, sessionId, adapter };
1167
- return child;
1168
- }
1169
-
1170
- // ─────────────────────────────────────────
1171
- // 一般模式 (dispatch 用)
1172
- // ─────────────────────────────────────────
1173
-
1174
- /**
1175
- * 啟動單一 claude subagent(格式化輸出)
1176
- */
1177
- function runSingle(opts) {
1178
- return spawnClaude(opts);
1179
- }
1180
-
1181
- // ─────────────────────────────────────────
1182
- // Headless 模式 (Unit / Solo / Squad 成員)
1183
- // ─────────────────────────────────────────
1184
-
1185
- /**
1186
- * 無頭模式:壓縮 prompt + 結構化摘要輸出
1187
- */
1188
- function runHeadless(opts) {
1189
- return spawnClaude({ ...opts, cleanPrompt: true, summaryOnly: true });
1190
- }
1191
-
1192
- // ─────────────────────────────────────────
1193
- // Legion Lead (Agent Teams)
1194
- // ─────────────────────────────────────────
1195
-
1196
- /**
1197
- * Agent Teams 模式:啟用 teammate-mode + Task 工具
1198
- */
1199
- function runLegionLead(opts) {
1200
- return spawnClaude({
1201
- ...opts,
1202
- cleanPrompt: true,
1203
- pipeRaw: true,
1204
- allowedTools: [
1205
- 'Bash,Read,Edit,Write,Glob,Grep,Task,ToolSearch',
1206
- 'mcp__kenaz-dashboard__kenaz_lsp_callers',
1207
- 'mcp__kenaz-dashboard__kenaz_lsp_references',
1208
- 'mcp__kenaz-dashboard__kenaz_lsp_definition',
1209
- 'mcp__kenaz-dashboard__kenaz_lsp_symbols',
1210
- 'mcp__kenaz-dashboard__synapse_recall',
1211
- 'mcp__kenaz-dashboard__synapse_status',
1212
- 'mcp__kenaz-dashboard__synapse_sigils',
1213
- 'mcp__kenaz-dashboard__synapse_health',
1214
- 'mcp__kenaz-dashboard__synapse_rune_conflicts',
1215
- 'mcp__kenaz-dashboard__synapse_scan_stale_runes',
1216
- 'mcp__kenaz-dashboard__synapse_inscribe',
1217
- 'mcp__kenaz-dashboard__synapse_forge',
1218
- 'mcp__kenaz-dashboard__synapse_clustering_log',
1219
- 'mcp__kenaz-dashboard__hippocampus_episode_start',
1220
- 'mcp__kenaz-dashboard__hippocampus_episode_append',
1221
- 'mcp__kenaz-dashboard__hippocampus_episode_close',
1222
- 'mcp__kenaz-dashboard__hippocampus_episode_link',
1223
- 'mcp__kenaz-dashboard__hippocampus_episode_recall',
1224
- ].join(','),
1225
- extraArgs: ['--teammate-mode', 'in-process']
1226
- });
1227
- }
1228
-
1229
- // ─────────────────────────────────────────
1230
- // 並行模式 (Squad — dispatch 用)
1231
- // ─────────────────────────────────────────
1232
-
1233
- /**
1234
- * 並行啟動多個 claude subagent
1235
- */
1236
- function runParallel(opts) {
1237
- const { members, formation, sessionId, projectPath = null } = opts;
1238
-
1239
- console.log(`\n⚔️ Squad Parallel Launch: ${members.length} members`);
1240
-
1241
- const children = members.map(m =>
1242
- runSingle({
1243
- prompt: m.prompt,
1244
- formation,
1245
- skill: m.skill || 'do',
1246
- sessionId,
1247
- memberId: m.memberId,
1248
- silent: true,
1249
- projectPath
1250
- })
1251
- );
1252
-
1253
- return Promise.all(
1254
- children.map(child =>
1255
- new Promise((resolve) => {
1256
- child.on('close', (code) => {
1257
- resolve({
1258
- memberId: child._formation.memberId,
1259
- exitCode: code,
1260
- logPath: child._formation.logPath
1261
- });
1262
- });
1263
- })
1264
- )
1265
- ).then(results => {
1266
- console.log(`\n⚔️ Squad Complete: ${results.length}/${members.length} finished`);
1267
- for (const r of results) {
1268
- const status = r.exitCode === 0 ? '✅' : '❌';
1269
- console.log(` ${status} [${r.memberId}] exit ${r.exitCode} → ${r.logPath}`);
1270
- }
1271
- return results;
1272
- });
1273
- }
1274
-
1275
- // ─────────────────────────────────────────
1276
- // FormationRunner — single-entry facade
1277
- // ─────────────────────────────────────────
1278
-
1279
- /**
1280
- * FormationRunner: unified facade over the Formation dispatch pipeline.
1281
- *
1282
- * Reduces the caller-visible step count for the Legion pain path from ~9
1283
- * boundary crossings to 4-5:
1284
- * 1. Build prompt (caller)
1285
- * 2. FormationRunner.run(mode, opts) ← single entry
1286
- * 3. Collect/format output ← handled internally by collectOutput+formatEntry
1287
- * 4. Await child result (caller)
1288
- *
1289
- * BEHAVIOR-PRESERVING: all internal logic delegates to the same
1290
- * runSingle / runHeadless / runLegionLead / runParallel functions.
1291
- * No parameter semantics, timeout values, worktree handling, or adapter
1292
- * routing are changed.
1293
- *
1294
- * Supported modes:
1295
- * 'single' → runSingle(opts)
1296
- * 'headless' → runHeadless(opts)
1297
- * 'legion-lead' → runLegionLead(opts)
1298
- * 'parallel' → runParallel(opts) (returns Promise)
1299
- *
1300
- * Callers that previously imported runLegionLead directly continue to work
1301
- * because the original exports remain in module.exports unchanged.
1302
- */
1303
- class FormationRunner {
1304
- /**
1305
- * Execute a Formation dispatch in the specified mode.
1306
- * @param {'single'|'headless'|'legion-lead'|'parallel'} mode
1307
- * @param {object} opts - same opts accepted by the underlying run* function
1308
- * @returns {ChildProcess|Promise}
1309
- */
1310
- run(mode, opts) {
1311
- switch (mode) {
1312
- case 'single':
1313
- return runSingle(opts);
1314
- case 'headless':
1315
- return runHeadless(opts);
1316
- case 'legion-lead':
1317
- return runLegionLead(opts);
1318
- case 'parallel':
1319
- return runParallel(opts);
1320
- default:
1321
- throw new Error(`FormationRunner: unknown mode "${mode}"`);
1322
- }
1323
- }
1324
- }
1325
-
1326
- /**
1327
- * Shared singleton — callers can import and use directly without instantiation.
1328
- * @type {FormationRunner}
1329
- */
1330
- const runner = new FormationRunner();
1331
-
1332
- module.exports = { runSingle, runHeadless, runLegionLead, runParallel, killProcessTree, detectWorktreeLeak, FormationRunner, runner };