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,1994 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Formation Legacy Scheduler (= Formation Lead)
4
- *
5
- * 以 Formation Lead 身份運行的連續調度排程器。
6
- * 所有任務透過 Formation Legacy Skill 調度,不使用 /dev 或 /product-review CLI。
7
- *
8
- * 職責:
9
- * - 掃描 .collaboration + dev/pipeline 選擇 pending 任務
10
- * - 使用 Formation Skill 調度 subagent(headless 模式)
11
- * - subagent 自行完成任務移動(task-api.js complete)
12
- * - Scheduler 執行 post-complete(archive + git)— Formation Lead 職責
13
- * - 失敗時重置任務為 pending
14
- *
15
- * Usage:
16
- * node .formation/tools/legacy-scheduler.js
17
- * node .formation/tools/legacy-scheduler.js --dry-run
18
- * node .formation/tools/legacy-scheduler.js --initial-delay=0 --interval=5
19
- * node .formation/tools/legacy-scheduler.js --self-enhancement
20
- */
21
-
22
- // Signal to task-api.js: scheduler handles post-complete centrally.
23
- // Prevents double post-complete (agent + scheduler both calling it).
24
- process.env.FORMATION_SCHEDULER = '1';
25
-
26
- // Prevent EPIPE crash when parent (dashboard) disconnects stdout/stderr pipe.
27
- // Without this, any console.log after pipe break throws → uncaughtException → crash.
28
- process.stdout.on('error', (err) => { if (err.code === 'EPIPE') return; throw err; });
29
- process.stderr.on('error', (err) => { if (err.code === 'EPIPE') return; throw err; });
30
-
31
- const fs = require('fs');
32
- const path = require('path');
33
- const crypto = require('crypto');
34
- const { execSync } = require('child_process');
35
- const yaml = (() => { try { return require('js-yaml'); } catch { return require('../../.vendor/js-yaml.cjs'); } })();
36
- const { ROOT, LOG_DIR, getLogDir, generateSessionId } = require('./utils');
37
- const { loadSkill, loadTask } = require('./loader');
38
- const { buildPrompt, HEADLESS_PREFIX } = require('./prompt');
39
- const { runHeadless } = require('./runner');
40
- const { postComplete } = require('./post-complete');
41
-
42
- // Yggdrasil Layer 2: lazy-loaded modules (only init when first called)
43
- let _remoteConflict = null;
44
- let _taskSync = null;
45
- let _supabaseClient = null;
46
- function getRemoteConflict() {
47
- if (!_remoteConflict) try { _remoteConflict = require('./remote-conflict'); } catch { _remoteConflict = false; }
48
- return _remoteConflict || null;
49
- }
50
- function getTaskSync() {
51
- if (!_taskSync) try { _taskSync = require('./task-sync'); } catch { _taskSync = false; }
52
- return _taskSync || null;
53
- }
54
- function getSupabaseClient(projectPath) {
55
- if (!_supabaseClient) try { _supabaseClient = require('./supabase-client'); } catch { _supabaseClient = false; }
56
- if (!_supabaseClient) return null;
57
- return _supabaseClient.getClient(projectPath);
58
- }
59
-
60
- /** Local date string YYYY-MM-DD (matches Rust chrono::Local) */
61
- function localDateStr() {
62
- const d = new Date();
63
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
64
- }
65
-
66
- // ─────────────────────────────────────────
67
- // 常數(對應 Rust types.rs)
68
- // ─────────────────────────────────────────
69
-
70
- const Status = {
71
- STOPPED: 'stopped',
72
- WAITING: 'waiting',
73
- RUNNING: 'running',
74
- RUNNING_REVIEW: 'running_review',
75
- PAUSED: 'paused',
76
- COMPLETED: 'completed',
77
- ERROR: 'error',
78
- IDLE: 'idle',
79
- };
80
-
81
- const RunStatus = {
82
- SUCCESS: 'success',
83
- FAILED: 'failed',
84
- TIMED_OUT: 'timed_out',
85
- CANCELLED: 'cancelled',
86
- };
87
-
88
- const PRIORITY_ORDER = ['P0', 'P1', 'P2', 'P3'];
89
- const PENDING_STATUSES = ['pending', 'backlog', 'todo'];
90
-
91
- // ─────────────────────────────────────────
92
- // Scope Path Lock Manager(多實例並行協調)
93
- // ─────────────────────────────────────────
94
-
95
- const LOCKS_DIR = path.join(ROOT, '.collaboration', 'tasks', 'locks');
96
-
97
- class ScopeLockManager {
98
- constructor() {
99
- this.instanceId = `pid_${process.pid}`;
100
- if (!fs.existsSync(LOCKS_DIR)) fs.mkdirSync(LOCKS_DIR, { recursive: true });
101
- }
102
-
103
- /** Read all active lock files, prune stale ones (dead processes) */
104
- getActiveLocks() {
105
- if (!fs.existsSync(LOCKS_DIR)) return [];
106
- const locks = [];
107
- for (const file of fs.readdirSync(LOCKS_DIR)) {
108
- if (!file.endsWith('.lock')) continue;
109
- try {
110
- const data = JSON.parse(fs.readFileSync(path.join(LOCKS_DIR, file), 'utf8'));
111
- // Check if the owning process is still alive
112
- if (data.pid && !this._isProcessAlive(data.pid)) {
113
- // Stale lock — remove it
114
- try { fs.unlinkSync(path.join(LOCKS_DIR, file)); } catch {}
115
- continue;
116
- }
117
- locks.push(data);
118
- } catch {
119
- // Corrupt lock file — remove
120
- try { fs.unlinkSync(path.join(LOCKS_DIR, file)); } catch {}
121
- }
122
- }
123
- return locks;
124
- }
125
-
126
- /** Check if a task's scope_paths conflict with any existing lock */
127
- hasPathConflict(scopePaths) {
128
- if (!scopePaths || scopePaths.length === 0) return false;
129
- const locks = this.getActiveLocks();
130
- for (const lock of locks) {
131
- if (lock.instanceId === this.instanceId) continue; // own locks don't conflict
132
- for (const lockedPath of (lock.scopePaths || [])) {
133
- for (const sp of scopePaths) {
134
- if (sp.startsWith(lockedPath) || lockedPath.startsWith(sp)) {
135
- return true;
136
- }
137
- }
138
- }
139
- }
140
- return false;
141
- }
142
-
143
- /** Acquire a scope lock for a task. Returns true if acquired. */
144
- acquire(task) {
145
- if (!task || !task.taskId) return true; // no-task runs don't need locks
146
- const lockFile = path.join(LOCKS_DIR, `${task.taskId}.lock`);
147
- // If lock already exists and isn't ours, fail
148
- if (fs.existsSync(lockFile)) {
149
- try {
150
- const existing = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
151
- if (existing.instanceId !== this.instanceId && this._isProcessAlive(existing.pid)) {
152
- return false; // another live instance holds this lock
153
- }
154
- } catch {}
155
- // Stale or corrupt — overwrite
156
- }
157
- const lockData = {
158
- taskId: task.taskId,
159
- instanceId: this.instanceId,
160
- pid: process.pid,
161
- scopePaths: task.scopePaths || [],
162
- acquiredAt: new Date().toISOString(),
163
- };
164
- fs.writeFileSync(lockFile, JSON.stringify(lockData, null, 2), 'utf8');
165
- return true;
166
- }
167
-
168
- /** Release scope lock for a task */
169
- release(taskId) {
170
- if (!taskId) return;
171
- const lockFile = path.join(LOCKS_DIR, `${taskId}.lock`);
172
- try { if (fs.existsSync(lockFile)) fs.unlinkSync(lockFile); } catch {}
173
- }
174
-
175
- /** Release all locks owned by this instance (cleanup on shutdown) */
176
- releaseAll() {
177
- if (!fs.existsSync(LOCKS_DIR)) return;
178
- for (const file of fs.readdirSync(LOCKS_DIR)) {
179
- if (!file.endsWith('.lock')) continue;
180
- try {
181
- const data = JSON.parse(fs.readFileSync(path.join(LOCKS_DIR, file), 'utf8'));
182
- if (data.instanceId === this.instanceId) {
183
- fs.unlinkSync(path.join(LOCKS_DIR, file));
184
- }
185
- } catch {}
186
- }
187
- }
188
-
189
- /** Check if a process is still alive */
190
- _isProcessAlive(pid) {
191
- try {
192
- process.kill(pid, 0); // signal 0 = check existence
193
- return true;
194
- } catch {
195
- return false;
196
- }
197
- }
198
- }
199
-
200
- // ─────────────────────────────────────────
201
- // Role → Legacy Skill 對應表
202
- // ─────────────────────────────────────────
203
-
204
- const ROLE_SKILL_MAP = {
205
- developer: 'developer',
206
- frontend: 'frontend',
207
- backend: 'backend',
208
- designer: 'designer',
209
- qa: 'qa',
210
- qa_engineer: 'qa',
211
- sre: 'sre',
212
- pm: 'pm',
213
- product_manager: 'pm',
214
- };
215
-
216
- /**
217
- * 將任務的 agentType/role 對應到 .formation/legacy/ 的 skill 名稱
218
- * 未匹配的角色 fallback 到 'developer'(泛用開發者)
219
- * @returns {string} skill 名稱
220
- */
221
- function getLegacySkillName(agentType) {
222
- const normalized = (agentType || '').toLowerCase().replace(/[-\s]/g, '_');
223
- return ROLE_SKILL_MAP[normalized] || 'developer';
224
- }
225
-
226
- // ─────────────────────────────────────────
227
- // 設定載入(對應 Rust config.rs)
228
- // ─────────────────────────────────────────
229
-
230
- function loadConfig(overrides = {}) {
231
- const configPath = path.join(ROOT, '.formation', 'legacy', 'scheduler.yaml');
232
- let cfg = {};
233
- if (fs.existsSync(configPath)) {
234
- cfg = yaml.load(fs.readFileSync(configPath, 'utf8'));
235
- }
236
-
237
- const c = cfg.config || {};
238
- const p = cfg.paths || {};
239
-
240
- const rawConcurrency = overrides.concurrency ?? c.concurrency ?? 3;
241
- const maxConcurrency = c.max_concurrency ?? 5;
242
-
243
- // 子專案路徑(null = core)
244
- const projectPath = overrides.projectPath || null;
245
-
246
- // Log 目錄:所有有 projectPath 的專案統一用 getLogDir()
247
- const logDir = projectPath
248
- ? path.join(getLogDir(projectPath), 'legacy')
249
- : path.join(ROOT, p.log_dir || '.formation/logs/legacy');
250
-
251
- return {
252
- concurrency: Math.min(Math.max(1, rawConcurrency), maxConcurrency),
253
- maxConcurrency,
254
- initialDelaySecs: overrides.initialDelay ?? c.initial_delay_secs ?? 5,
255
- intervalMinutes: overrides.interval ?? c.interval_minutes ?? 20,
256
- idleCheckSecs: overrides.idleCheck ?? c.idle_check_secs ?? 5,
257
- earlyFinishDelaySecs: c.early_finish_delay_secs ?? 30,
258
- devTimeoutMinutes: overrides.devTimeout ?? c.dev_timeout_minutes ?? 60,
259
- reviewTimeoutMinutes: c.review_timeout_minutes ?? 30,
260
- enableSelfEnhancement: overrides.selfEnhancement ?? c.enable_self_enhancement ?? false,
261
- enableRemotePull: overrides.remotePull ?? c.enable_remote_pull ?? true,
262
- idleTimeoutSecs: overrides.idleTimeout ?? c.idle_timeout_secs ?? 300,
263
-
264
- // 子專案路徑(null = core)
265
- projectPath,
266
-
267
- tasksDir: path.join(ROOT, p.tasks_dir || 'dev/pipeline/tasks'),
268
- completedDir: path.join(ROOT, p.completed_dir || 'dev/pipeline/completed'),
269
- logDir,
270
- statusFile: path.join(ROOT, p.status_file || 'dev/pipeline/SELF_ENHANCEMENT_STATUS.json'),
271
- progressFile: path.join(ROOT, p.progress_file || 'dev/pipeline/PROGRESS.md'),
272
- };
273
- }
274
-
275
- // ─────────────────────────────────────────
276
- // Logger(對應 Rust setup_log_writer / write_log)
277
- // ─────────────────────────────────────────
278
-
279
- class SchedulerLogger {
280
- constructor(logDir, instanceId) {
281
- if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
282
- const date = localDateStr();
283
- this.instanceId = instanceId || crypto.randomBytes(2).toString('hex');
284
- this.logPath = path.join(logDir, `scheduler_${date}_${this.instanceId}.log`);
285
- this.stream = fs.createWriteStream(this.logPath, { flags: 'a' });
286
- }
287
-
288
- write(message) {
289
- const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
290
- const line = `[${ts}] ${message}`;
291
- try { console.log(line); } catch {} // EPIPE if parent closed pipe
292
- this.stream.write(line + '\n');
293
- }
294
-
295
- close() {
296
- this.stream.end();
297
- }
298
- }
299
-
300
- // ─────────────────────────────────────────
301
- // TaskSelector(對應 Rust task_selector.rs)
302
- // ─────────────────────────────────────────
303
-
304
- class TaskSelector {
305
- constructor(config) {
306
- this.projectPath = config.projectPath || null;
307
- const projectRoot = this.projectPath || ROOT;
308
-
309
- // Core fallback 路徑(僅供子專案 fallback 掃描用)
310
- this.coreCollabBase = path.join(ROOT, '.collaboration', 'tasks');
311
- this.coreTasksDir = config.tasksDir;
312
- this.coreCompletedDir = config.completedDir;
313
-
314
- // 統一路徑:所有有 projectPath 的專案(含 KenazAI 本體)都用 .kenaz/tasks/
315
- if (this.projectPath) {
316
- const kenazBase = path.join(projectRoot, '.kenaz', 'tasks');
317
- const collabBase = path.join(projectRoot, '.collaboration', 'tasks');
318
- // Primary base: prefer .kenaz/ if it exists
319
- this.collabBase = fs.existsSync(kenazBase) ? kenazBase : collabBase;
320
- // Pending dirs: scan BOTH .kenaz/ and .collaboration/
321
- const pendingDirs = [];
322
- for (const base of [kenazBase, collabBase]) {
323
- for (const sub of ['backlog', 'todo']) {
324
- const dir = path.join(base, sub);
325
- if (!pendingDirs.includes(dir)) pendingDirs.push(dir);
326
- }
327
- }
328
- this.collabPendingDirs = pendingDirs;
329
- this.collabDoneDir = path.join(this.collabBase, 'done');
330
- this.collabInProgressDir = path.join(this.collabBase, 'active');
331
- this.tasksDir = path.join(projectRoot, 'dev', 'pipeline', 'tasks');
332
- this.completedDir = path.join(projectRoot, 'dev', 'pipeline', 'completed');
333
- this.corePendingDirs = null;
334
- this.coreDoneDir = null;
335
- } else {
336
- // 無 projectPath(直接在 ROOT 執行):使用原有路徑
337
- this.tasksDir = config.tasksDir;
338
- this.completedDir = config.completedDir;
339
- this.collabBase = path.join(ROOT, '.collaboration', 'tasks');
340
- this.collabPendingDirs = ['backlog', 'todo'].map(d => path.join(this.collabBase, d));
341
- this.collabDoneDir = path.join(this.collabBase, 'done');
342
- this.collabInProgressDir = path.join(this.collabBase, 'active');
343
- this.corePendingDirs = null;
344
- this.coreDoneDir = null;
345
- }
346
- }
347
-
348
- /** 取得所有已完成的 task ID(新舊系統合併,含 core fallback)
349
- * done/ + archived/ 都算完成(archived 是從 done 移過去的) */
350
- getCompletedIds() {
351
- const ids = [];
352
- const scanDir = (dir) => {
353
- if (!dir || !fs.existsSync(dir)) return;
354
- ids.push(...fs.readdirSync(dir)
355
- .filter(f => f.startsWith('TASK_'))
356
- .map(f => f.split('_').slice(0, 2).join('_')));
357
- };
358
-
359
- // 當前專案(sub-project 或 core)
360
- scanDir(this.completedDir); // 舊系統 completed/
361
- scanDir(this.collabDoneDir); // 新系統 done/
362
- // archived/ 也算完成(post-complete 可能已將 done 歸檔)
363
- const archivedDir = path.join(path.dirname(this.collabDoneDir), 'archived');
364
- scanDir(archivedDir);
365
-
366
- return [...new Set(ids)];
367
- }
368
-
369
- /** 選擇最高優先級的 pending 任務(新舊系統合併,依賴已滿足)
370
- * @param {ScopeLockManager} [scopeLockMgr] - if provided, filter out tasks with path conflicts
371
- * @param {Set<string>} [excludeIds] - task IDs to skip (used for retry after claim failure)
372
- * @param {Set<string>} [remoteClaimedIds] - task IDs claimed by other nodes (Yggdrasil)
373
- * @returns {object|null} task object or null */
374
- selectNextTask(scopeLockMgr, excludeIds, remoteClaimedIds) {
375
- const completedIds = this.getCompletedIds();
376
- const candidates = [];
377
-
378
- // 1. 新系統:掃 backlog/ + todo/
379
- for (const dir of this.collabPendingDirs) {
380
- if (!fs.existsSync(dir)) continue;
381
- const files = fs.readdirSync(dir).filter(f => f.startsWith('TASK_') && f.endsWith('.yaml'));
382
- for (const file of files) {
383
- const task = this._parseTaskFrom(dir, file, 'collab');
384
- if (!task) continue;
385
- if (!PENDING_STATUSES.includes(task.status)) continue;
386
- if (task.dependencies.some(dep => !completedIds.includes(dep))) continue;
387
- if (excludeIds && excludeIds.has(task.taskId)) continue;
388
- candidates.push(task);
389
- }
390
- }
391
-
392
- // 2. 舊系統:掃 dev/pipeline/tasks/
393
- if (fs.existsSync(this.tasksDir)) {
394
- const files = fs.readdirSync(this.tasksDir).filter(f => f.startsWith('TASK_') && f.endsWith('.yaml'));
395
- for (const file of files) {
396
- const task = this._parseTaskFrom(this.tasksDir, file, 'legacy');
397
- if (!task) continue;
398
- if (task.status !== 'pending') continue;
399
- if (task.dependencies.some(dep => !completedIds.includes(dep))) continue;
400
- // 避免重複(同一 taskId 已在新系統出現)
401
- if (candidates.some(c => c.taskId === task.taskId)) continue;
402
- if (excludeIds && excludeIds.has(task.taskId)) continue;
403
- candidates.push(task);
404
- }
405
- }
406
-
407
- // 3. Core fallback 新系統(子專案模式:也掃 core 的 backlog/ + todo/)
408
- if (this.corePendingDirs) {
409
- for (const dir of this.corePendingDirs) {
410
- if (!fs.existsSync(dir)) continue;
411
- const files = fs.readdirSync(dir).filter(f => f.startsWith('TASK_') && f.endsWith('.yaml'));
412
- for (const file of files) {
413
- const task = this._parseTaskFrom(dir, file, 'core-collab');
414
- if (!task) continue;
415
- if (!PENDING_STATUSES.includes(task.status)) continue;
416
- if (task.dependencies.some(dep => !completedIds.includes(dep))) continue;
417
- if (candidates.some(c => c.taskId === task.taskId)) continue;
418
- candidates.push(task);
419
- }
420
- }
421
- }
422
-
423
- // Yggdrasil: 跳過已被其他 node claimed 的任務(避免重複 claim)
424
- if (remoteClaimedIds && remoteClaimedIds.size > 0) {
425
- for (let i = candidates.length - 1; i >= 0; i--) {
426
- if (remoteClaimedIds.has(candidates[i].taskId)) {
427
- console.log(` 🌍 Skipping ${candidates[i].taskId}: already claimed by another node`);
428
- candidates.splice(i, 1);
429
- }
430
- }
431
- }
432
-
433
- let filtered = candidates;
434
-
435
- // Scope lock filter:跳過路徑被其他實例鎖定的任務
436
- if (scopeLockMgr) {
437
- filtered = filtered.filter(t => {
438
- if (scopeLockMgr.hasPathConflict(t.scopePaths)) {
439
- console.log(` 🔒 Skipping ${t.taskId}: scope_paths locked by another instance`);
440
- return false;
441
- }
442
- return true;
443
- });
444
- }
445
-
446
- // Yggdrasil L2: Remote conflict filter — 跳過遠端節點正在處理的任務
447
- const rc = getRemoteConflict();
448
- if (rc) {
449
- filtered = filtered.filter(t => {
450
- const check = rc.hasRemoteConflict(t.scopePaths);
451
- if (check.conflict) {
452
- console.log(` 🌐 Skipping ${t.taskId}: scope_paths conflict with remote node ${check.conflictNode}`);
453
- return false;
454
- }
455
- return true;
456
- });
457
- }
458
-
459
- // 排序:priority (P0 first), then taskId
460
- filtered.sort((a, b) => {
461
- const pi = PRIORITY_ORDER.indexOf(a.priority) - PRIORITY_ORDER.indexOf(b.priority);
462
- return pi !== 0 ? pi : a.taskId.localeCompare(b.taskId);
463
- });
464
-
465
- return filtered[0] || null;
466
- }
467
-
468
- /**
469
- * 選取最多 n 個 pending 任務(並行用)
470
- * @param {number} n - 最多選幾個
471
- * @param {string[]} excludeScopes - 目前正在執行的 scope_paths(衝突排除)
472
- * @param {Set<string>} [remoteClaimedIds] - task IDs claimed by other nodes (Yggdrasil)
473
- * @returns {object[]} 任務陣列
474
- */
475
- selectNextTasks(n, excludeScopes = [], remoteClaimedIds) {
476
- const completedIds = this.getCompletedIds();
477
- const candidates = [];
478
-
479
- // 1. 新系統:掃 backlog/ + todo/
480
- for (const dir of this.collabPendingDirs) {
481
- if (!fs.existsSync(dir)) continue;
482
- const files = fs.readdirSync(dir).filter(f => f.startsWith('TASK_') && f.endsWith('.yaml'));
483
- for (const file of files) {
484
- const task = this._parseTaskFrom(dir, file, 'collab');
485
- if (!task) continue;
486
- if (!PENDING_STATUSES.includes(task.status)) continue;
487
- if (task.dependencies.some(dep => !completedIds.includes(dep))) continue;
488
- candidates.push(task);
489
- }
490
- }
491
-
492
- // 2. 舊系統:掃 dev/pipeline/tasks/
493
- if (fs.existsSync(this.tasksDir)) {
494
- const files = fs.readdirSync(this.tasksDir).filter(f => f.startsWith('TASK_') && f.endsWith('.yaml'));
495
- for (const file of files) {
496
- const task = this._parseTaskFrom(this.tasksDir, file, 'legacy');
497
- if (!task) continue;
498
- if (task.status !== 'pending') continue;
499
- if (task.dependencies.some(dep => !completedIds.includes(dep))) continue;
500
- if (candidates.some(c => c.taskId === task.taskId)) continue;
501
- candidates.push(task);
502
- }
503
- }
504
-
505
- // 3. Core fallback 新系統(子專案模式:也掃 core 的 backlog/ + todo/)
506
- if (this.corePendingDirs) {
507
- for (const dir of this.corePendingDirs) {
508
- if (!fs.existsSync(dir)) continue;
509
- const files = fs.readdirSync(dir).filter(f => f.startsWith('TASK_') && f.endsWith('.yaml'));
510
- for (const file of files) {
511
- const task = this._parseTaskFrom(dir, file, 'core-collab');
512
- if (!task) continue;
513
- if (!PENDING_STATUSES.includes(task.status)) continue;
514
- if (task.dependencies.some(dep => !completedIds.includes(dep))) continue;
515
- if (candidates.some(c => c.taskId === task.taskId)) continue;
516
- candidates.push(task);
517
- }
518
- }
519
- }
520
-
521
- // Yggdrasil: 跳過已被其他 node claimed 的任務(避免重複 claim)
522
- if (remoteClaimedIds && remoteClaimedIds.size > 0) {
523
- for (let i = candidates.length - 1; i >= 0; i--) {
524
- if (remoteClaimedIds.has(candidates[i].taskId)) {
525
- console.log(` 🌍 Skipping ${candidates[i].taskId}: already claimed by another node`);
526
- candidates.splice(i, 1);
527
- }
528
- }
529
- }
530
-
531
- let filtered = candidates;
532
-
533
- // Yggdrasil L2: Remote conflict filter — 跳過遠端節點正在處理的任務
534
- const rc = getRemoteConflict();
535
- if (rc) {
536
- filtered = filtered.filter(t => {
537
- const check = rc.hasRemoteConflict(t.scopePaths);
538
- if (check.conflict) {
539
- console.log(` 🌐 Skipping ${t.taskId}: scope_paths conflict with remote node ${check.conflictNode}`);
540
- return false;
541
- }
542
- return true;
543
- });
544
- }
545
-
546
- // 排序:priority (P0 first), then taskId
547
- filtered.sort((a, b) => {
548
- const pi = PRIORITY_ORDER.indexOf(a.priority) - PRIORITY_ORDER.indexOf(b.priority);
549
- return pi !== 0 ? pi : a.taskId.localeCompare(b.taskId);
550
- });
551
-
552
- // 選取最多 n 個,排除與 excludeScopes 衝突的任務
553
- const selected = [];
554
- const usedScopes = [...excludeScopes];
555
-
556
- for (const task of filtered) {
557
- if (selected.length >= n) break;
558
-
559
- // 衝突檢查:任務的 scope_paths 與已佔用的 scope 是否重疊
560
- const hasConflict = task.scopePaths.length > 0 && task.scopePaths.some(sp =>
561
- usedScopes.some(us => sp.startsWith(us) || us.startsWith(sp))
562
- );
563
-
564
- if (hasConflict) continue;
565
-
566
- selected.push(task);
567
- usedScopes.push(...task.scopePaths);
568
- }
569
-
570
- return selected;
571
- }
572
-
573
- /** 更新 YAML 檔案中的 status 欄位 */
574
- updateTaskStatus(fileName, newStatus, sourceDir) {
575
- const filePath = path.join(sourceDir || this.tasksDir, fileName);
576
- if (!fs.existsSync(filePath)) return;
577
- let content = fs.readFileSync(filePath, 'utf8');
578
- content = content.replace(/^status:\s*.*/m, `status: "${newStatus}"`);
579
- fs.writeFileSync(filePath, content, 'utf8');
580
- }
581
-
582
- /** 將任務標記為進行中(根據來源系統移動檔案)
583
- * @returns {boolean} true if successfully claimed, false if already taken */
584
- markInProgress(task) {
585
- const { fileName, source, sourceDir } = task;
586
-
587
- if (source === 'collab' || source === 'core-collab') {
588
- // 新系統:移到對應的 active/ 目錄
589
- const base = source === 'core-collab' ? this.coreCollabBase : this.collabBase;
590
- const activeDir = path.join(base, 'active');
591
- if (!fs.existsSync(activeDir)) fs.mkdirSync(activeDir, { recursive: true });
592
- const src = path.join(sourceDir, fileName);
593
- if (!fs.existsSync(src)) return false; // 已被其他實例移走
594
- try {
595
- this.updateTaskStatus(fileName, 'active', sourceDir);
596
- fs.renameSync(src, path.join(activeDir, fileName));
597
- task.sourceDir = activeDir;
598
- return true;
599
- } catch (err) {
600
- if (err.code === 'ENOENT') return false; // race: file moved between check and rename
601
- throw err;
602
- }
603
- } else {
604
- // 舊系統(legacy / core-legacy):原地更新
605
- this.updateTaskStatus(fileName, 'in_progress', sourceDir);
606
- return true;
607
- }
608
- }
609
-
610
- /** 將任務重置為 pending(根據來源系統移回原位) */
611
- resetToPending(task) {
612
- const { fileName, source, sourceDir } = task;
613
-
614
- if (source === 'collab' || source === 'core-collab') {
615
- // 新系統:移回對應的 backlog/
616
- const base = source === 'core-collab' ? this.coreCollabBase : this.collabBase;
617
- const backlogDir = path.join(base, 'backlog');
618
- if (!fs.existsSync(backlogDir)) fs.mkdirSync(backlogDir, { recursive: true });
619
- const src = path.join(sourceDir, fileName);
620
- if (!fs.existsSync(src)) return; // 已被移走(subagent 可能已 complete)
621
- this.updateTaskStatus(fileName, 'pending', sourceDir);
622
- fs.renameSync(src, path.join(backlogDir, fileName));
623
- task.sourceDir = backlogDir;
624
- } else {
625
- // 舊系統(legacy / core-legacy):原地更新
626
- const src = path.join(sourceDir, fileName);
627
- if (!fs.existsSync(src)) return;
628
- this.updateTaskStatus(fileName, 'pending', sourceDir);
629
- }
630
- }
631
-
632
- /** 將任務移動到 completed(根據來源系統) */
633
- moveToCompleted(task) {
634
- const { fileName, source, sourceDir } = task;
635
-
636
- if (source === 'collab' || source === 'core-collab') {
637
- // 新系統:移到對應的 done/ 目錄
638
- const doneDir = source === 'core-collab' ? this.coreDoneDir : this.collabDoneDir;
639
- if (!fs.existsSync(doneDir)) {
640
- fs.mkdirSync(doneDir, { recursive: true });
641
- }
642
- const src = path.join(sourceDir, fileName);
643
- const dst = path.join(doneDir, fileName);
644
-
645
- // subagent 可能已透過 task-api.js complete 移動,檢查是否已在 done/
646
- if (fs.existsSync(dst)) {
647
- // 已在 done/,清理 active/ 殘留(如果有)
648
- if (fs.existsSync(src)) {
649
- try { fs.unlinkSync(src); } catch {}
650
- }
651
- return;
652
- }
653
-
654
- if (!fs.existsSync(src)) {
655
- // 來源不在 active/ 也不在 done/ — 可能被其他流程移走
656
- return;
657
- }
658
-
659
- // 只有當來源檔案存在時才更新狀態並移動
660
- try {
661
- this.updateTaskStatus(fileName, 'done', sourceDir);
662
- fs.renameSync(src, dst);
663
- } catch (err) {
664
- // 處理可能的 race condition(在檢查與移動之間檔案被移走)
665
- if (err.code === 'ENOENT' && fs.existsSync(dst)) {
666
- if (fs.existsSync(src)) {
667
- try { fs.unlinkSync(src); } catch {}
668
- }
669
- } else {
670
- throw err;
671
- }
672
- }
673
- } else {
674
- // 舊系統(legacy / core-legacy):移到對應的 completed/
675
- const completedDir = source === 'core-legacy' ? this.coreCompletedDir : this.completedDir;
676
- const tasksDir = source === 'core-legacy' ? this.coreTasksDir : this.tasksDir;
677
- if (!fs.existsSync(completedDir)) {
678
- fs.mkdirSync(completedDir, { recursive: true });
679
- }
680
- const src = path.join(tasksDir, fileName);
681
- const dst = path.join(completedDir, fileName);
682
-
683
- // 同上:subagent 可能已完成移動
684
- if (fs.existsSync(dst)) {
685
- if (fs.existsSync(src)) {
686
- try { fs.unlinkSync(src); } catch {}
687
- }
688
- return;
689
- }
690
-
691
- if (!fs.existsSync(src)) {
692
- return;
693
- }
694
-
695
- // 只有當來源檔案存在時才更新狀態並移動
696
- try {
697
- this.updateTaskStatus(fileName, 'completed', tasksDir);
698
- fs.renameSync(src, dst);
699
- } catch (err) {
700
- // 處理可能的 race condition(在檢查與移動之間檔案被移走)
701
- if (err.code === 'ENOENT' && fs.existsSync(dst)) {
702
- if (fs.existsSync(src)) {
703
- try { fs.unlinkSync(src); } catch {}
704
- }
705
- } else {
706
- throw err;
707
- }
708
- }
709
- }
710
- }
711
-
712
- _parseTaskFrom(dir, fileName, source) {
713
- try {
714
- const content = fs.readFileSync(path.join(dir, fileName), 'utf8');
715
- const raw = yaml.load(content);
716
- if (!raw) return null;
717
-
718
- const taskId = raw.id || fileName.split('_').slice(0, 2).join('_');
719
- return {
720
- taskId,
721
- title: raw.title || taskId,
722
- agentType: raw.agent_type || raw.role || 'unknown',
723
- priority: (raw.priority || 'P3').toUpperCase(),
724
- status: (raw.status || 'pending').toLowerCase(),
725
- dependencies: Array.isArray(raw.dependencies) ? raw.dependencies : (raw.dependencies ? [raw.dependencies] : []),
726
- scopePaths: Array.isArray(raw.scope_paths) ? raw.scope_paths : (raw.scope_paths ? [raw.scope_paths] : []),
727
- tags: Array.isArray(raw.tags) ? raw.tags : (raw.tags ? [raw.tags] : []),
728
- keywords: raw.keywords || [],
729
- commitMessage: raw.commit_message || null,
730
- fileName,
731
- source, // 'collab' | 'legacy'
732
- sourceDir: dir, // 實際目錄路徑
733
- };
734
- } catch {
735
- return null;
736
- }
737
- }
738
- }
739
-
740
- // ─────────────────────────────────────────
741
- // ProgressInfo(對應 Rust check_pending_tasks)
742
- // ─────────────────────────────────────────
743
-
744
- function checkPendingTasks(config) {
745
- const info = {
746
- hasPendingTasks: false,
747
- pendingCount: 0,
748
- activeCount: 0,
749
- completedCount: 0,
750
- totalCount: 0,
751
- };
752
-
753
- const countYaml = (dir) => {
754
- if (!fs.existsSync(dir)) return 0;
755
- return fs.readdirSync(dir).filter(f => f.startsWith('TASK_') && f.endsWith('.yaml')).length;
756
- };
757
-
758
- const projectRoot = config.projectPath || ROOT;
759
-
760
- // 掃 .kenaz/tasks/ + .collaboration/tasks/(兩者都掃)
761
- const kenazBase = path.join(projectRoot, '.kenaz', 'tasks');
762
- const collabBase = path.join(projectRoot, '.collaboration', 'tasks');
763
- const primaryBase = (config.projectPath && fs.existsSync(kenazBase))
764
- ? kenazBase
765
- : collabBase;
766
-
767
- // pending(backlog/ + todo/)— scan primary + fallback
768
- info.pendingCount += countYaml(path.join(primaryBase, 'backlog'));
769
- info.pendingCount += countYaml(path.join(primaryBase, 'todo'));
770
- // Also scan .collaboration/ if primary was .kenaz/ (migration period)
771
- if (primaryBase === kenazBase) {
772
- info.pendingCount += countYaml(path.join(collabBase, 'backlog'));
773
- info.pendingCount += countYaml(path.join(collabBase, 'todo'));
774
- }
775
-
776
- // [merge: main] active 統計(main 新增,{git_user} 無此項)
777
- info.activeCount += countYaml(path.join(primaryBase, 'active'));
778
-
779
- // completed(done/)
780
- info.completedCount += countYaml(path.join(primaryBase, 'done'));
781
-
782
- // 舊系統 pending(dev/pipeline/tasks/)— config.tasksDir 是 core 路徑
783
- info.pendingCount += countYaml(config.tasksDir);
784
-
785
- // 舊系統 completed(dev/pipeline/completed/)— config.completedDir 是 core 路徑
786
- if (fs.existsSync(config.completedDir)) {
787
- info.completedCount += fs.readdirSync(config.completedDir)
788
- .filter(f => f.startsWith('TASK_')).length;
789
- }
790
-
791
- // 子專案模式不再 fallback 到 core(專案隔離)
792
- info.totalCount = info.pendingCount + info.activeCount + info.completedCount;
793
- info.hasPendingTasks = info.pendingCount > 0;
794
-
795
- return info;
796
- }
797
-
798
- /**
799
- * 透過 Formation Legacy Skill 執行任務
800
- * 使用 loadSkill → buildPrompt → runHeadless 完整鏈路
801
- *
802
- * 分兩階段:
803
- * 1. Loading phase(sync):載入 skill + task + 建構 prompt
804
- * 2. Spawn phase(async):啟動 child process,回傳 Promise
805
- *
806
- * Loading phase 在 Promise 外完成,確保 fillSlots 的 for 迴圈中
807
- * 多個 spawn 可以連續啟動,不被 execSync 阻塞。
808
- *
809
- * @param {object} task - taskSelector 回傳的任務物件
810
- * @param {string} skillName - .formation/legacy/ 下的 skill 名稱
811
- * @param {object} config - scheduler config
812
- * @param {object} logger - scheduler logger
813
- * @returns {Promise<{exitCode, status, stdout, timedOut}>}
814
- */
815
- function runWithFormation(task, skillName, config, logger, opts = {}) {
816
- // 使用預載資料(fillSlots 批量載入)或即時載入(fallback)
817
- const prepared = opts._prepared;
818
- const loadOpts = opts.projectPath ? { projectPath: opts.projectPath } : undefined;
819
- const skillDef = prepared?.skillDef ?? loadSkill('legacy', skillName);
820
- const taskData = prepared?.taskData ?? loadTask(task.taskId, loadOpts);
821
- const prompt = prepared?.prompt ?? buildPrompt(skillDef, task.taskId, taskData);
822
- const sessionId = prepared?.sessionId ?? generateSessionId();
823
-
824
- logger.write(`Formation: skill=${skillName}, session=${sessionId}`);
825
-
826
- // Inject canary with real task/scope binding
827
- let finalPrompt = HEADLESS_PREFIX + prompt;
828
- let coherenceToken = null;
829
- try {
830
- const canary = require('../../.specialist/tools/canary');
831
- let taskScopePaths = [];
832
- if (taskData) {
833
- try { taskScopePaths = JSON.parse(taskData).scope_paths || []; } catch { /* non-JSON */ }
834
- }
835
- const canaryResult = canary.generate({
836
- session: sessionId,
837
- specialist: 'legacy',
838
- task: task.taskId || 'none',
839
- scope: taskScopePaths.join(','),
840
- }, config.projectPath);
841
- finalPrompt = canaryResult.block + '\n\n' + finalPrompt;
842
- coherenceToken = canaryResult.coherenceToken;
843
- } catch (err) {
844
- logger.write(`Canary injection skipped: ${err.message}`);
845
- }
846
-
847
- // ── Phase 2: Spawn(async,child process 在此啟動) ──
848
- return new Promise((resolve) => {
849
- // [merge: main + {git_user}] sessionShort 標記 (main) + projectPath ({git_user})
850
- const sessionShort = sessionId.slice(-4);
851
- let lastActivityTime = Date.now();
852
-
853
- const child = runHeadless({
854
- prompt: finalPrompt,
855
- formation: 'legacy',
856
- skill: skillName,
857
- sessionId,
858
- silent: true,
859
- onLog: (line) => {
860
- logger.write(line.replace(/\[Agent\]/g, `[Agent#${sessionShort}]`));
861
- lastActivityTime = Date.now();
862
- },
863
- projectPath: config.projectPath,
864
- coherenceToken,
865
- });
866
-
867
- let timedOut = false;
868
- let drifted = false;
869
-
870
- // ── Canary drift detection: kill agent on context pollution ──
871
- child.on('formation:drift', (reason) => {
872
- if (drifted) return; // prevent double-fire
873
- drifted = true;
874
- logger.write(`🐦 CANARY DRIFT detected for ${task.taskId}: ${reason}`);
875
- logger.write(` Killing agent and scheduling retry with fresh context...`);
876
- forceKillAndResolve();
877
- });
878
-
879
- /**
880
- * Kill child process tree (Windows-safe).
881
- * On Windows, child.kill() only kills the shell (cmd.exe), not the
882
- * actual claude process underneath. Use taskkill /f /t to kill the
883
- * entire process tree. Also schedules a failsafe done() call in case
884
- * the 'close' event never fires.
885
- */
886
- const forceKillAndResolve = () => {
887
- if (!child.pid) return;
888
- try {
889
- if (process.platform === 'win32') {
890
- execSync(`taskkill /pid ${child.pid} /f /t`, { stdio: 'ignore', timeout: 5000, windowsHide: true });
891
- } else {
892
- child.kill('SIGKILL');
893
- }
894
- } catch {}
895
- // Failsafe: if 'close' event doesn't fire within 15s, force-resolve
896
- setTimeout(() => done(1), 15_000);
897
- };
898
-
899
- // Total execution timeout
900
- const timeoutMinutes = opts.timeoutMinutes || config.devTimeoutMinutes;
901
- const timeoutMs = timeoutMinutes * 60 * 1000;
902
- const timer = setTimeout(() => {
903
- timedOut = true;
904
- forceKillAndResolve();
905
- }, timeoutMs);
906
-
907
- // Idle watchdog: kill if no log output for idleTimeoutSecs (0 = disabled)
908
- const idleTimeoutSecs = config.idleTimeoutSecs || 0;
909
- const idleWatchdog = idleTimeoutSecs > 0 ? setInterval(() => {
910
- const idleSecs = Math.round((Date.now() - lastActivityTime) / 1000);
911
- if (idleSecs >= idleTimeoutSecs) {
912
- logger.write(`⚠️ IDLE WATCHDOG: Agent#${sessionShort} no output for ${idleSecs}s — killing`);
913
- timedOut = true;
914
- forceKillAndResolve();
915
- clearInterval(idleWatchdog);
916
- }
917
- }, 30_000) : null;
918
-
919
- let resolved = false;
920
- const done = (code) => {
921
- if (resolved) return;
922
- resolved = true;
923
- clearTimeout(timer);
924
- if (idleWatchdog) clearInterval(idleWatchdog);
925
- resolve({
926
- exitCode: code ?? 1,
927
- status: drifted ? RunStatus.FAILED
928
- : timedOut ? RunStatus.TIMED_OUT
929
- : (code === 0 ? RunStatus.SUCCESS : RunStatus.FAILED),
930
- stdout: '',
931
- timedOut,
932
- drifted,
933
- });
934
- };
935
-
936
- child.on('close', done);
937
- child.on('formation:done', done);
938
- });
939
- }
940
-
941
- /**
942
- * 執行單個任務(從 runDevAgent 拆出的核心邏輯)
943
- * @param {object} prepared - 預載的 Formation 資料(可選,由 fillSlots 批量載入)
944
- * @returns {Promise<{status, exitCode, durationSecs, targetTask}>}
945
- */
946
- async function executeTask(task, config, logger, prepared = null, scopeLockMgr = null) {
947
- const startTime = Date.now();
948
- let result;
949
-
950
- if (task.agentType === 'lead') {
951
- // ── Lead-only 任務:Scheduler 自己處理(不派 subagent) ──
952
- logger.write(`Lead-only task detected: ${task.taskId} — handling directly`);
953
-
954
- try {
955
- const commitMsg = task.commitMessage || `[${task.taskId}] ${task.title}`;
956
- if (!config.projectPath) {
957
- logger.write(`⚠️ Lead postComplete: projectPath is null — will use ROOT (${ROOT})`);
958
- }
959
- postComplete({ taskId: task.taskId, message: commitMsg, projectPath: config.projectPath });
960
- logger.write(`Lead task completed: post-complete done for ${task.taskId}`);
961
-
962
- try {
963
- const projectArg = config.projectPath ? ` --project-path "${config.projectPath}"` : '';
964
- execSync(`node .collaboration/core/task-api.js${projectArg} complete ${task.taskId}`, {
965
- encoding: 'utf8', cwd: config.projectPath || ROOT, timeout: 10000, windowsHide: true,
966
- });
967
- logger.write(`Task ${task.taskId} marked as done via task-api`);
968
- } catch (e) {
969
- logger.write(`Warning: task-api complete failed: ${e.message}`);
970
- }
971
-
972
- result = { exitCode: 0, status: RunStatus.SUCCESS, stdout: '' };
973
- } catch (e) {
974
- logger.write(`Lead task failed: ${e.message}`);
975
- result = { exitCode: 1, status: RunStatus.FAILED, stdout: '' };
976
- }
977
- } else {
978
- // ── 一般任務:派給 subagent 執行 ──
979
- const skillName = prepared?.skillName || getLegacySkillName(task.agentType);
980
- logger.write(`Using Formation Legacy skill: ${skillName} (from role: ${task.agentType})`);
981
- result = await runWithFormation(task, skillName, config, logger, { _prepared: prepared, projectPath: config.projectPath });
982
- }
983
-
984
- const durationSecs = Math.round((Date.now() - startTime) / 1000);
985
- logger.write(`[${task.taskId}] Execution completed: exit=${result.exitCode}, status=${result.status}, duration=${durationSecs}s`);
986
- if (result.stdout) {
987
- logger.write(`Output:\n${result.stdout}`);
988
- }
989
-
990
- // Release scope lock after task completes (success or failure)
991
- if (task) {
992
- if (scopeLockMgr) scopeLockMgr.release(task.taskId);
993
- logger.write(`Scope lock released for ${task.taskId}`);
994
-
995
- // Filesystem-level cleanup: remove active/ residuals and lock files
996
- // (git checkout during post-complete can restore committed copies)
997
- if (task.source === 'collab' && result.status === RunStatus.SUCCESS) {
998
- const taskRoot = config.projectPath || ROOT;
999
- // 子專案: .kenaz/tasks/ Core: .collaboration/tasks/
1000
- const tasksBase = fs.existsSync(path.join(taskRoot, '.kenaz', 'tasks'))
1001
- ? path.join(taskRoot, '.kenaz', 'tasks')
1002
- : path.join(taskRoot, '.collaboration', 'tasks');
1003
- const activeDir = path.join(tasksBase, 'active');
1004
- const doneDir = path.join(tasksBase, 'done');
1005
- try {
1006
- const activeFiles = fs.readdirSync(activeDir).filter(f => f.startsWith(task.taskId));
1007
- const inDone = fs.readdirSync(doneDir).some(f => f.startsWith(task.taskId));
1008
- if (inDone) {
1009
- for (const f of activeFiles) {
1010
- fs.unlinkSync(path.join(activeDir, f));
1011
- logger.write(`Cleaned residual: active/${f}`);
1012
- }
1013
- }
1014
- } catch {}
1015
- }
1016
-
1017
- // Clear task info marker so Dashboard reverts card to idle color
1018
- console.log('##TASK_CLEAR:');
1019
- }
1020
-
1021
- return {
1022
- status: result.status,
1023
- exitCode: result.exitCode,
1024
- durationSecs,
1025
- targetTask: task.taskId,
1026
- };
1027
- }
1028
-
1029
- /**
1030
- * 處理任務完成後的狀態同步
1031
- */
1032
- function handleTaskResult(task, result, taskSelector, logger, config) {
1033
- if (task.agentType === 'lead') {
1034
- if (result.status !== RunStatus.SUCCESS) {
1035
- taskSelector.resetToPending(task);
1036
- logger.write(`Lead task ${task.taskId} reset to pending for retry`);
1037
- }
1038
- return;
1039
- }
1040
-
1041
- if (result.status === RunStatus.SUCCESS) {
1042
- logger.write(`Task ${task.taskId} completed by subagent (${task.source})`);
1043
-
1044
- // Safety net: ensure task is moved to done/ (agent may have failed to call task-api complete,
1045
- // or restoreTaskFiles may have undone the move after git operations)
1046
- taskSelector.moveToCompleted(task);
1047
- logger.write(`Task ${task.taskId} ensured in done/`);
1048
-
1049
- // Yggdrasil L2: 同步 done 狀態到 Supabase
1050
- const ts = getTaskSync();
1051
- if (ts) {
1052
- ts.syncTaskStatus({
1053
- taskId: task.taskId,
1054
- status: 'done',
1055
- projectPath: config?.projectPath || null,
1056
- }).catch(() => {});
1057
- }
1058
-
1059
- // Guard: validate projectPath before post-complete to prevent cross-project git contamination
1060
- const effectiveProjectPath = config?.projectPath || null;
1061
- if (!effectiveProjectPath) {
1062
- logger.write(`⚠️ postComplete: projectPath is null/undefined — will use ROOT (${ROOT}). If this is a sub-project task, this is a bug.`);
1063
- }
1064
-
1065
- let mergeOk = false;
1066
- try {
1067
- const pcResult = postComplete({ taskId: task.taskId, projectPath: effectiveProjectPath });
1068
-
1069
- // Check if git operations succeeded
1070
- // pcResult.git = { committed, merged, noChanges, taskBranch, ... }
1071
- if (pcResult?.git && pcResult.git.committed && !pcResult.git.merged) {
1072
- logger.write(`❌ Post-complete: committed on ${pcResult.git.taskBranch} but merge FAILED for ${task.taskId}`);
1073
- logger.write(` → Resetting to pending — code is NOT on working branch`);
1074
- taskSelector.resetToPending(task);
1075
- return;
1076
- }
1077
- // Detect: had changes but commit failed (e.g., stash pop lost the changes)
1078
- if (pcResult?.git && !pcResult.git.committed && !pcResult.git.noChanges) {
1079
- logger.write(`❌ Post-complete: had changes but commit FAILED for ${task.taskId} (stash recovery failure?)`);
1080
- logger.write(` → Resetting to pending — code changes may be in git stash`);
1081
- taskSelector.resetToPending(task);
1082
- return;
1083
- }
1084
-
1085
- mergeOk = pcResult?.git?.merged ?? true; // no git result = skip-git mode, treat as ok
1086
- logger.write(`Post-complete done for ${task.taskId} (merged=${mergeOk})`);
1087
- } catch (e) {
1088
- logger.write(`❌ Post-complete failed for ${task.taskId}: ${e.message}`);
1089
- logger.write(` → Resetting to pending for retry`);
1090
- taskSelector.resetToPending(task);
1091
- return;
1092
- }
1093
-
1094
- // Post-complete git ops (stash/checkout/merge) may restore task files to old positions.
1095
- // Re-ensure the completed task is in done/ after all git operations.
1096
- taskSelector.moveToCompleted(task);
1097
- logger.write(`Task ${task.taskId} re-ensured in done/ after post-complete`);
1098
-
1099
- // ── Acceptance Criteria 驗證 ──
1100
- try {
1101
- const { verify } = require('./verify-acceptance');
1102
- const verifyResult = verify(task.taskId, config?.projectPath);
1103
-
1104
- if (verifyResult.skipped) {
1105
- logger.write(`Acceptance verify skipped: ${verifyResult.message}`);
1106
- } else if (verifyResult.pass) {
1107
- logger.write(`✅ Acceptance verify PASS for ${task.taskId}`);
1108
- } else {
1109
- logger.write(`❌ Acceptance verify FAIL for ${task.taskId}`);
1110
- const failed = (verifyResult.results || []).filter(r => !r.pass);
1111
- for (const f of failed) {
1112
- logger.write(` ❌ ${f.criterion}: ${f.reason}`);
1113
- }
1114
- if (verifyResult.fixTasks) {
1115
- for (const ft of verifyResult.fixTasks) {
1116
- logger.write(` 📋 Fix task created: ${ft}`);
1117
- }
1118
- }
1119
- }
1120
- } catch (err) {
1121
- logger.write(`Warning: Acceptance verify error: ${err.message}`);
1122
- }
1123
- } else {
1124
- taskSelector.resetToPending(task);
1125
- if (result.drifted) {
1126
- logger.write(`🐦 Task ${task.taskId} reset to pending — canary drift detected, will retry with fresh context`);
1127
- } else {
1128
- logger.write(`Task ${task.taskId} reset to pending for retry (${task.source})`);
1129
- }
1130
- }
1131
- }
1132
-
1133
- /**
1134
- * 執行 /product-review(對應 Rust run_product_review)
1135
- */
1136
- async function runProductReview(config, logger) {
1137
- logger.write('==========================================');
1138
- logger.write('Starting Product Review (Self-Enhancement)');
1139
- logger.write('==========================================');
1140
-
1141
- // 備份 status file
1142
- if (fs.existsSync(config.statusFile)) {
1143
- fs.copyFileSync(config.statusFile, config.statusFile + '.bak');
1144
- logger.write('Backed up previous status file');
1145
- }
1146
-
1147
- // 使用 Formation Legacy review skill(不再用 spawnClaudeCmd)
1148
- const result = await runWithFormation(
1149
- { taskId: null, agentType: 'pm' },
1150
- 'review',
1151
- config,
1152
- logger,
1153
- { timeoutMinutes: config.reviewTimeoutMinutes }
1154
- );
1155
-
1156
- logger.write(`Product Review completed: exit=${result.exitCode}, status=${result.status}`);
1157
-
1158
- if (result.status === RunStatus.TIMED_OUT) {
1159
- const bak = config.statusFile + '.bak';
1160
- if (fs.existsSync(bak)) {
1161
- fs.renameSync(bak, config.statusFile);
1162
- logger.write('Restored previous status file due to timeout');
1163
- }
1164
- }
1165
-
1166
- return result;
1167
- }
1168
-
1169
- /**
1170
- * 讀取 SELF_ENHANCEMENT_STATUS.json 決策
1171
- */
1172
- function readEnhancementDecision(config) {
1173
- if (!fs.existsSync(config.statusFile)) return 'continue';
1174
- try {
1175
- const data = JSON.parse(fs.readFileSync(config.statusFile, 'utf8'));
1176
- const decision = (data.decision || data.enhancement_decision || 'continue').toLowerCase();
1177
- if (decision.includes('complete_with')) return 'complete_with_suggestions';
1178
- if (decision.includes('complete')) return 'complete';
1179
- return 'continue';
1180
- } catch {
1181
- return 'continue';
1182
- }
1183
- }
1184
-
1185
- // ─────────────────────────────────────────
1186
- // Status File Writer
1187
- // ─────────────────────────────────────────
1188
-
1189
- function updateStatusFile(config, data) {
1190
- if (!fs.existsSync(config.statusFile)) return;
1191
- try {
1192
- let existing = {};
1193
- try {
1194
- existing = JSON.parse(fs.readFileSync(config.statusFile, 'utf8'));
1195
- } catch {}
1196
- const updated = { ...existing, ...data, lastUpdated: new Date().toISOString() };
1197
- fs.writeFileSync(config.statusFile, JSON.stringify(updated, null, 2), 'utf8');
1198
- } catch (err) {
1199
- console.error('Failed to update status file:', err.message);
1200
- }
1201
- }
1202
-
1203
- // ─────────────────────────────────────────
1204
- // Yggdrasil L3: Remote Task Pull Helpers
1205
- // ─────────────────────────────────────────
1206
-
1207
- /**
1208
- * 檢查本地 backlog/ 是否已有該任務(避免重複寫入)
1209
- */
1210
- function localBacklogHasTask(taskId, config) {
1211
- const projectRoot = config.projectPath || ROOT;
1212
- const kenazBase = path.join(projectRoot, '.kenaz', 'tasks');
1213
- const collabBase = (config.projectPath && fs.existsSync(kenazBase))
1214
- ? kenazBase
1215
- : path.join(projectRoot, '.collaboration', 'tasks');
1216
- const backlogDir = path.join(collabBase, 'backlog');
1217
- if (!fs.existsSync(backlogDir)) return false;
1218
- return fs.readdirSync(backlogDir).some(f => f.startsWith(taskId));
1219
- }
1220
-
1221
- /**
1222
- * 將遠端 claim 成功的任務寫入本地 backlog/ YAML
1223
- * full_yaml 存在 → 直接寫入;否則用 metadata 建立精簡 YAML
1224
- */
1225
- function writeRemoteTaskToBacklog(task, config, logger) {
1226
- const projectRoot = config.projectPath || ROOT;
1227
- const kenazBase = path.join(projectRoot, '.kenaz', 'tasks');
1228
- const collabBase = (config.projectPath && fs.existsSync(kenazBase))
1229
- ? kenazBase
1230
- : path.join(projectRoot, '.collaboration', 'tasks');
1231
- const backlogDir = path.join(collabBase, 'backlog');
1232
- if (!fs.existsSync(backlogDir)) fs.mkdirSync(backlogDir, { recursive: true });
1233
-
1234
- const scopeSuffix = (task.scope_paths || []).length > 0
1235
- ? '_' + (task.scope_paths[0] || '').replace(/[^a-zA-Z0-9]/g, '_').slice(0, 30)
1236
- : '';
1237
- const titleSlug = (task.title || '').replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '_').slice(0, 40);
1238
- const fileName = `${task.task_id}_${titleSlug}${scopeSuffix}.yaml`;
1239
- const filePath = path.join(backlogDir, fileName);
1240
-
1241
- if (task.full_yaml) {
1242
- // full_yaml 存在:直接寫入完整內容
1243
- fs.writeFileSync(filePath, task.full_yaml, 'utf8');
1244
- logger.write(` 📥 Remote task pulled: ${task.task_id} → backlog/ (full_yaml)`);
1245
- } else {
1246
- // full_yaml 為空:用 metadata 建立精簡 YAML
1247
- const minimalYaml = [
1248
- `id: "${task.task_id}"`,
1249
- `title: "${(task.title || '').replace(/"/g, '\\"')}"`,
1250
- `status: "backlog"`,
1251
- `priority: "${task.priority || 'P2'}"`,
1252
- `role: "developer"`,
1253
- `description: "${(task.description || '').replace(/"/g, '\\"')}"`,
1254
- `scope_paths:`,
1255
- ...(task.scope_paths || []).map(sp => ` - "${sp}"`),
1256
- `dependencies:`,
1257
- ...(task.dependencies || []).map(d => ` - "${d}"`),
1258
- `source: "yggdrasil"`,
1259
- ].join('\n') + '\n';
1260
- fs.writeFileSync(filePath, minimalYaml, 'utf8');
1261
- logger.write(` 📥 Remote task pulled: ${task.task_id} → backlog/ (metadata-only)`);
1262
- }
1263
- }
1264
-
1265
- /**
1266
- * 對任務池(assigned_to IS NULL)中的任務打分。
1267
- * 簡單匹配:task.scope_paths 與本 node 最近修改的檔案是否在同目錄。
1268
- * @returns {Array<{task, score}>} 按分數降序排列
1269
- */
1270
- function scorePoolTasks(poolTasks, config) {
1271
- // 取最近修改的檔案目錄(用 git 取最近 50 個 commit 的檔案)
1272
- let recentDirs = [];
1273
- try {
1274
- const projectRoot = config.projectPath || ROOT;
1275
- const output = require('child_process').execSync(
1276
- 'git log --pretty=format: --name-only -50 | head -200',
1277
- { cwd: projectRoot, encoding: 'utf8', timeout: 5000, windowsHide: true }
1278
- );
1279
- const files = output.split('\n').filter(Boolean);
1280
- recentDirs = [...new Set(files.map(f => path.dirname(f)))];
1281
- } catch {
1282
- // git 不可用 → 所有任務得 0 分
1283
- return poolTasks.map(t => ({ task: t, score: 0 }));
1284
- }
1285
-
1286
- const scored = poolTasks.map(task => {
1287
- const scopePaths = task.scope_paths || [];
1288
- if (scopePaths.length === 0) return { task, score: 0.1 }; // 無 scope 給微量分
1289
-
1290
- let overlap = 0;
1291
- for (const sp of scopePaths) {
1292
- const spDir = path.dirname(sp);
1293
- for (const rd of recentDirs) {
1294
- if (sp.startsWith(rd) || rd.startsWith(spDir)) {
1295
- overlap++;
1296
- break;
1297
- }
1298
- }
1299
- }
1300
- const score = scopePaths.length > 0 ? overlap / scopePaths.length : 0;
1301
- return { task, score };
1302
- });
1303
-
1304
- scored.sort((a, b) => b.score - a.score);
1305
- return scored;
1306
- }
1307
-
1308
- // ─────────────────────────────────────────
1309
- // 主排程迴圈(對應 Rust run_main_loop)
1310
- // ─────────────────────────────────────────
1311
-
1312
- /**
1313
- * 等待任一 Promise 完成(不是 Promise.race — 保留其他 Promise 繼續跑)
1314
- * 處理 resolve 和 reject 兩種情況,避免 hang
1315
- * @returns {Promise<{index, value}>} 第一個完成的結果 + 在 slots 中的位置
1316
- */
1317
- function raceSlots(slots) {
1318
- return new Promise((resolve, reject) => {
1319
- let settled = false;
1320
- let activeCount = 0;
1321
-
1322
- slots.forEach((slot, i) => {
1323
- if (!slot) return;
1324
- activeCount++;
1325
-
1326
- slot.promise.then(
1327
- value => {
1328
- if (!settled) { settled = true; resolve({ index: i, value }); }
1329
- },
1330
- error => {
1331
- // Promise rejected — 視為 FAILED,避免 hang
1332
- if (!settled) {
1333
- settled = true;
1334
- resolve({
1335
- index: i,
1336
- value: {
1337
- status: RunStatus.FAILED,
1338
- exitCode: 1,
1339
- durationSecs: 0,
1340
- targetTask: slot.task?.taskId || 'unknown',
1341
- },
1342
- });
1343
- }
1344
- }
1345
- );
1346
- });
1347
-
1348
- if (activeCount === 0) {
1349
- reject(new Error('raceSlots: no active slots'));
1350
- }
1351
- });
1352
- }
1353
-
1354
- async function runMainLoop(config, logger, scopeLockMgr, { writePidHeartbeat } = {}) {
1355
- const taskSelector = new TaskSelector(config);
1356
- let runCount = 0;
1357
- let status = Status.WAITING;
1358
- let firstRun = true;
1359
- let lastRunStart = Date.now();
1360
- const devHistory = [];
1361
-
1362
- // 並行 slot 管理:每個 slot = { task, promise } | null
1363
- const slots = new Array(config.concurrency).fill(null);
1364
-
1365
- /** 取得目前所有正在執行的 scope paths */
1366
- const getActiveScopes = () => {
1367
- const scopes = [];
1368
- for (const slot of slots) {
1369
- if (slot && slot.task.scopePaths) scopes.push(...slot.task.scopePaths);
1370
- }
1371
- return scopes;
1372
- };
1373
-
1374
- /** 取得空閒 slot 數量 */
1375
- const freeSlotCount = () => slots.filter(s => s === null).length;
1376
-
1377
- // Yggdrasil: 遠端節點上下文(每輪 main loop 更新,fillSlots 讀取注入 prompt)
1378
- let yggdrasilContext = null;
1379
- let remoteClaimedIds = new Set(); // Yggdrasil: task IDs claimed by other nodes
1380
-
1381
- /**
1382
- * 填充空閒 slot(兩階段:先批量載入,再快速啟動)
1383
- *
1384
- * Phase 1 — Pre-load:批量執行 loadSkill + loadTask + buildPrompt
1385
- * (execSync 在此完成,避免在 spawn 迴圈中阻塞 event loop)
1386
- * Phase 2 — Spawn:連續啟動所有 child process(毫秒級)
1387
- */
1388
- const fillSlots = () => {
1389
- const freeCount = freeSlotCount();
1390
- if (freeCount === 0) return 0;
1391
-
1392
- const activeScopes = getActiveScopes();
1393
- const tasks = taskSelector.selectNextTasks(freeCount, activeScopes, remoteClaimedIds);
1394
- if (tasks.length === 0) return 0;
1395
-
1396
- // ── Phase 1: Pre-load(所有 execSync 在此完成) ──
1397
- const preparations = [];
1398
- for (const task of tasks) {
1399
- let prepared = null;
1400
- if (task.agentType !== 'lead') {
1401
- const skillName = getLegacySkillName(task.agentType);
1402
- const skillDef = loadSkill('legacy', skillName);
1403
- const loadOpts = config.projectPath ? { projectPath: config.projectPath } : undefined;
1404
- const taskData = loadTask(task.taskId, loadOpts);
1405
- const promptOpts = config.projectPath ? { projectPath: config.projectPath } : {};
1406
- if (yggdrasilContext) promptOpts.yggdrasilContext = yggdrasilContext;
1407
- const prompt = buildPrompt(skillDef, task.taskId, taskData, promptOpts);
1408
- prepared = { skillName, skillDef, taskData, prompt, sessionId: generateSessionId() };
1409
- }
1410
- preparations.push({ task, prepared });
1411
- }
1412
-
1413
- if (tasks.length > 1) {
1414
- logger.write(`🚀 Pre-loaded ${tasks.length} tasks, spawning in parallel...`);
1415
- }
1416
-
1417
- // ── Phase 2: Spawn(不再有 execSync 阻塞,連續啟動) ──
1418
- let filled = 0;
1419
- for (const { task, prepared } of preparations) {
1420
- const slotIdx = slots.indexOf(null);
1421
- if (slotIdx === -1) break;
1422
-
1423
- runCount++;
1424
- logger.write(`[Slot ${slotIdx}] Starting: [${task.taskId}] ${task.title} (${task.priority}, ${task.agentType}, ${task.source})`);
1425
- if (prepared) {
1426
- const projArg = config.projectPath ? ` --project-path=${config.projectPath}` : '';
1427
- logger.write(`[Slot ${slotIdx}] Command: node .formation/tools/headless.js --formation=legacy --skill=${prepared.skillName} --task=${task.taskId}${projArg} (session=${prepared.sessionId})`);
1428
- logger.write(`[Slot ${slotIdx}] Task injected: ${prepared.taskData ? 'YES' : 'NO'} | Prompt length: ${(HEADLESS_PREFIX + prepared.prompt).length} chars`);
1429
- } else {
1430
- logger.write(`[Slot ${slotIdx}] Mode: lead-only (no subagent)`);
1431
- }
1432
- taskSelector.markInProgress(task);
1433
-
1434
- // Yggdrasil L2: 同步 active 狀態到 Supabase
1435
- const ts = getTaskSync();
1436
- if (ts) {
1437
- const sbClient = getSupabaseClient(config.projectPath || null);
1438
- const nodeName = sbClient?.config?.node_name || null;
1439
- ts.syncTaskStatus({
1440
- taskId: task.taskId,
1441
- status: 'active',
1442
- claimedBy: nodeName,
1443
- priority: task.priority,
1444
- scopePaths: task.scopePaths,
1445
- title: task.title,
1446
- dependencies: task.dependencies,
1447
- projectPath: config.projectPath || null,
1448
- }).catch(() => {});
1449
- }
1450
-
1451
- const promise = executeTask(task, config, logger, prepared, scopeLockMgr);
1452
- slots[slotIdx] = { task, promise };
1453
- filled++;
1454
- }
1455
-
1456
- return filled;
1457
- };
1458
-
1459
- /** 有任何 slot 在跑嗎 */
1460
- const hasActiveSlots = () => slots.some(s => s !== null);
1461
-
1462
- // 主迴圈
1463
- let lastIdleLog = 0;
1464
- while (!shuttingDown) {
1465
- // Heartbeat: update PID file so other instances can detect we're alive
1466
- if (writePidHeartbeat) writePidHeartbeat();
1467
-
1468
- // 檢查待辦
1469
- const progress = checkPendingTasks(config);
1470
- const activeLabel = progress.activeCount > 0 ? `, ${progress.activeCount} active` : '';
1471
- const isIdle = !progress.hasPendingTasks && !hasActiveSlots();
1472
-
1473
- if (!isIdle || status !== Status.WAITING) {
1474
- logger.write(`Progress: ${progress.completedCount}/${progress.totalCount} completed (${progress.pendingCount} pending${activeLabel})`);
1475
- }
1476
-
1477
- updateStatusFile(config, {
1478
- pendingCount: progress.pendingCount,
1479
- activeCount: progress.activeCount,
1480
- completedCount: progress.completedCount,
1481
- });
1482
-
1483
- if (isIdle) {
1484
- if (!config.enableSelfEnhancement) {
1485
- const now = Date.now();
1486
- if (status !== Status.WAITING || now - lastIdleLog > 300000) {
1487
- logger.write(`No pending tasks. Standing by (check every ${config.idleCheckSecs}s)...`);
1488
- lastIdleLog = now;
1489
- }
1490
- status = Status.WAITING;
1491
- updateStatusFile(config, { status: 'waiting' });
1492
- if (await waitWithShutdown(config.idleCheckSecs * 1000)) break;
1493
- continue;
1494
- }
1495
-
1496
- // Self-enhancement: 執行 product review
1497
- logger.write('No pending tasks. Running Product Review...');
1498
- status = Status.RUNNING_REVIEW;
1499
- updateStatusFile(config, { status: 'running_review' });
1500
- const reviewResult = await runProductReview(config, logger);
1501
-
1502
- if (reviewResult.status !== RunStatus.SUCCESS) {
1503
- logger.write('Product review failed. Will retry...');
1504
- if (await waitWithShutdown(config.intervalMinutes * 60 * 1000)) break;
1505
- continue;
1506
- }
1507
-
1508
- const decision = readEnhancementDecision(config);
1509
- logger.write(`Enhancement Decision: ${decision}`);
1510
-
1511
- if (decision === 'complete' || decision === 'complete_with_suggestions') {
1512
- logger.write(`PROJECT COMPLETE! (${decision})`);
1513
- status = Status.COMPLETED;
1514
- updateStatusFile(config, { status: 'completed', isRunning: false });
1515
- break;
1516
- }
1517
-
1518
- const updated = checkPendingTasks(config);
1519
- if (!updated.hasPendingTasks) {
1520
- logger.write('No new tasks found after review. Waiting...');
1521
- if (await waitWithShutdown(config.intervalMinutes * 60 * 1000)) break;
1522
- continue;
1523
- }
1524
- }
1525
-
1526
- // 間隔等待(非首次 & 無活躍 slot)
1527
- if (!firstRun && !hasActiveSlots()) {
1528
- const elapsed = (Date.now() - lastRunStart) / 1000;
1529
- const intervalSecs = config.intervalMinutes * 60;
1530
- const waitSecs = elapsed < intervalSecs ? config.earlyFinishDelaySecs : 0;
1531
-
1532
- if (waitSecs > 0) {
1533
- logger.write(`Waiting ${waitSecs} seconds before next round...`);
1534
- status = Status.PAUSED;
1535
- updateStatusFile(config, { status: 'paused' });
1536
- if (await waitWithShutdown(waitSecs * 1000)) break;
1537
- }
1538
- }
1539
- firstRun = false;
1540
- lastRunStart = Date.now();
1541
-
1542
- // Yggdrasil L2: 每輪刷新遠端衝突快取 + 查詢遠端節點(注入 agent prompt)
1543
- const rc = getRemoteConflict();
1544
- if (rc) {
1545
- try { await rc.refreshCache(config.projectPath || null); } catch {}
1546
- }
1547
- const ts = getTaskSync();
1548
- if (ts) {
1549
- try {
1550
- const [remoteNodes, remoteTasks, claimedIds] = await Promise.all([
1551
- ts.getOnlineNodes(config.projectPath || null).catch(() => []),
1552
- ts.getRemoteActiveTasks(config.projectPath || null).catch(() => []),
1553
- ts.getRemoteClaimedTaskIds(config.projectPath || null).catch(() => new Set()),
1554
- ]);
1555
- remoteClaimedIds = claimedIds;
1556
- if (remoteNodes.length > 0 || remoteTasks.length > 0) {
1557
- const sb = getSupabaseClient(config.projectPath || null);
1558
- yggdrasilContext = { onlineNodes: remoteNodes, remoteTasks, myNodeName: sb?.config?.node_name || 'unknown', myScopePaths: [] };
1559
- } else {
1560
- yggdrasilContext = null;
1561
- }
1562
- } catch { yggdrasilContext = null; remoteClaimedIds = new Set(); }
1563
- }
1564
-
1565
- // Yggdrasil L3: 拉取遠端指派/任務池任務 → 寫入本地 backlog/
1566
- if (ts && config.enableRemotePull && freeSlotCount() > 0) {
1567
- try {
1568
- const remoteCandidates = await ts.getAssignedTasks(config.projectPath || null);
1569
- if (remoteCandidates.length > 0) {
1570
- const sb = getSupabaseClient(config.projectPath || null);
1571
- const myNode = sb?.config?.node_name || 'unknown';
1572
- // 分組:直接指派 vs 任務池(未指定接收者)
1573
- const directAssigned = remoteCandidates.filter(t => t.assigned_to === myNode);
1574
- const poolTasks = remoteCandidates.filter(t => !t.assigned_to);
1575
-
1576
- // 直接指派的任務:優先 claim
1577
- for (const task of directAssigned) {
1578
- if (localBacklogHasTask(task.task_id, config)) continue;
1579
- const claimResult = await ts.tryClaimTask(task.task_id, config.projectPath || null);
1580
- if (claimResult.success) {
1581
- writeRemoteTaskToBacklog(claimResult.task, config, logger);
1582
- } else {
1583
- logger.write(` 🌍 Claim failed for ${task.task_id} (taken by another node)`);
1584
- }
1585
- }
1586
-
1587
- // 任務池:打分搶單(scope overlap)
1588
- if (poolTasks.length > 0 && freeSlotCount() > 0) {
1589
- const scored = scorePoolTasks(poolTasks, config);
1590
- for (const { task, score } of scored) {
1591
- if (freeSlotCount() === 0) break;
1592
- if (score <= 0) continue; // 無重疊不搶
1593
- if (localBacklogHasTask(task.task_id, config)) continue;
1594
- const claimResult = await ts.tryClaimTask(task.task_id, config.projectPath || null);
1595
- if (claimResult.success) {
1596
- logger.write(` 🎯 Pool claim: ${task.task_id} (score=${score.toFixed(2)})`);
1597
- writeRemoteTaskToBacklog(claimResult.task, config, logger);
1598
- }
1599
- }
1600
- }
1601
- }
1602
- } catch (err) {
1603
- logger.write(` ⚠️ Remote task pull failed (graceful skip): ${err.message}`);
1604
- }
1605
- }
1606
-
1607
- // 填充空閒 slot
1608
- const filled = fillSlots();
1609
- if (filled > 0) {
1610
- const activeCount = slots.filter(s => s !== null).length;
1611
- logger.write(`Slots: ${activeCount}/${config.concurrency} active (filled ${filled} new)`);
1612
- status = Status.RUNNING;
1613
- updateStatusFile(config, {
1614
- status: 'running',
1615
- isRunning: true,
1616
- activeSlots: activeCount,
1617
- concurrency: config.concurrency,
1618
- });
1619
- }
1620
-
1621
- // 如果沒有活躍 slot,跳回檢查
1622
- if (!hasActiveSlots()) continue;
1623
-
1624
- // 等待任一 slot 完成(滑動窗口)
1625
- const { index, value: result } = await raceSlots(slots);
1626
- const completedTask = slots[index].task;
1627
- slots[index] = null; // 釋放 slot
1628
-
1629
- logger.write(`[Slot ${index}] Completed: [${completedTask.taskId}] ${result.status} (${result.durationSecs}s)`);
1630
-
1631
- // 處理結果
1632
- handleTaskResult(completedTask, result, taskSelector, logger, config);
1633
- devHistory.push({ runNumber: runCount, ...result });
1634
- if (devHistory.length > 20) devHistory.shift();
1635
-
1636
- // 立即嘗試填補空出的 slot(滑動窗口)
1637
- const refilled = fillSlots();
1638
- if (refilled > 0) {
1639
- const activeCount = slots.filter(s => s !== null).length;
1640
- logger.write(`Slots: ${activeCount}/${config.concurrency} active (refilled ${refilled})`);
1641
- }
1642
- }
1643
-
1644
- // Shutdown: 等待所有活躍 slot 完成
1645
- if (hasActiveSlots()) {
1646
- logger.write('Shutdown requested — waiting for active slots to finish...');
1647
- const activeSlots = slots.filter(s => s !== null);
1648
- const results = await Promise.all(activeSlots.map(s => s.promise));
1649
- results.forEach((result, i) => {
1650
- const task = activeSlots[i].task;
1651
- handleTaskResult(task, result, taskSelector, logger, config);
1652
- });
1653
- }
1654
-
1655
- return { status, runCount, devHistory };
1656
- }
1657
-
1658
- // ─────────────────────────────────────────
1659
- // Shutdown 控制(對應 Rust wait_with_shutdown / is_shutdown_requested)
1660
- // ─────────────────────────────────────────
1661
-
1662
- let shuttingDown = false;
1663
-
1664
- function requestShutdown() {
1665
- shuttingDown = true;
1666
- }
1667
-
1668
- /** 等待 ms,可被 shutdown 打斷。回傳 true = 被打斷 */
1669
- function waitWithShutdown(ms) {
1670
- return new Promise((resolve) => {
1671
- if (shuttingDown) return resolve(true);
1672
-
1673
- const interval = setInterval(() => {
1674
- if (shuttingDown) {
1675
- clearInterval(interval);
1676
- clearTimeout(timer);
1677
- resolve(true);
1678
- }
1679
- }, 500);
1680
-
1681
- const timer = setTimeout(() => {
1682
- clearInterval(interval);
1683
- resolve(false);
1684
- }, ms);
1685
- });
1686
- }
1687
-
1688
- // ─────────────────────────────────────────
1689
- // 主進入點(對應 Rust start())
1690
- // ─────────────────────────────────────────
1691
-
1692
- async function start(overrides = {}) {
1693
- const config = loadConfig(overrides);
1694
- const instanceId = overrides.instanceId || crypto.randomBytes(2).toString('hex');
1695
-
1696
- // ── Singleton guard: one scheduler per project ──
1697
- // Heartbeat: PID file includes `heartbeat` timestamp updated every loop iteration.
1698
- // If the process is alive but heartbeat is stale (>5 min), treat it as a zombie.
1699
- const HEARTBEAT_STALE_MS = 5 * 60 * 1000;
1700
- if (!fs.existsSync(config.logDir)) fs.mkdirSync(config.logDir, { recursive: true });
1701
- const pidFile = path.join(config.logDir, 'scheduler.pid');
1702
- if (fs.existsSync(pidFile)) {
1703
- try {
1704
- const existing = JSON.parse(fs.readFileSync(pidFile, 'utf8'));
1705
- if (existing.pid && existing.pid !== process.pid) {
1706
- let isAlive = false;
1707
- try { process.kill(existing.pid, 0); isAlive = true; } catch {}
1708
- if (isAlive) {
1709
- const heartbeat = existing.heartbeat ? new Date(existing.heartbeat).getTime() : 0;
1710
- const stale = Date.now() - heartbeat > HEARTBEAT_STALE_MS;
1711
- if (stale) {
1712
- console.log(`⚠️ Stale scheduler detected (PID ${existing.pid}, last heartbeat: ${existing.heartbeat || 'never'}). Killing zombie...`);
1713
- try {
1714
- if (process.platform === 'win32') {
1715
- require('child_process').execSync(`taskkill /F /PID ${existing.pid}`, { stdio: 'ignore', timeout: 5000, windowsHide: true });
1716
- } else {
1717
- process.kill(existing.pid, 'SIGTERM');
1718
- }
1719
- } catch {}
1720
- } else {
1721
- console.log(`ℹ️ Scheduler already running (PID ${existing.pid}). Exiting quietly.`);
1722
- process.exit(0);
1723
- }
1724
- }
1725
- }
1726
- } catch {}
1727
- }
1728
- // Clean up old-format per-instance PID files (scheduler_{id}.pid)
1729
- try {
1730
- for (const f of fs.readdirSync(config.logDir).filter(f => /^scheduler_[0-9a-f]+\.pid$/.test(f))) {
1731
- try { fs.unlinkSync(path.join(config.logDir, f)); } catch {}
1732
- }
1733
- } catch {}
1734
-
1735
- const logger = new SchedulerLogger(config.logDir, instanceId);
1736
- const scopeLockMgr = new ScopeLockManager();
1737
-
1738
- // Write single PID file (with heartbeat for zombie detection)
1739
- const writePidHeartbeat = () => {
1740
- try {
1741
- fs.writeFileSync(pidFile, JSON.stringify({ pid: process.pid, instanceId, startedAt: new Date().toISOString(), heartbeat: new Date().toISOString() }));
1742
- } catch {}
1743
- };
1744
- writePidHeartbeat();
1745
-
1746
- // 信號處理
1747
- const cleanup = () => {
1748
- requestShutdown();
1749
- // Release all scope locks held by this instance
1750
- try { scopeLockMgr.releaseAll(); } catch {}
1751
- // Clean up PID file
1752
- try {
1753
- if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
1754
- } catch {}
1755
- // Yggdrasil L2: 下線通知 — 標記 kenaz_status is_online=false(fire-and-forget)
1756
- try {
1757
- const sb = getSupabaseClient(config.projectPath || null);
1758
- if (sb) {
1759
- const { client: supabase, config: sbCfg } = sb;
1760
- const tsm = getTaskSync();
1761
- supabase.from('kenaz_status').upsert({
1762
- node_name: sbCfg.node_name,
1763
- project: tsm?.resolveProjectName?.(config.projectPath) || 'default',
1764
- is_online: false,
1765
- }, { onConflict: 'node_name,project' }).catch(() => {});
1766
- }
1767
- } catch {}
1768
- };
1769
- process.on('SIGINT', cleanup);
1770
- process.on('SIGTERM', cleanup);
1771
- process.on('exit', cleanup);
1772
-
1773
- // Banner
1774
- console.log('');
1775
- console.log(`⚔️ Formation Legacy Scheduler [${instanceId}]`);
1776
- console.log(` PID: ${process.pid}`);
1777
- // [merge: main + {git_user}] Instance (main) + Concurrency ({git_user})
1778
- console.log(` Instance: ${instanceId}`);
1779
- console.log(` Concurrency: ${config.concurrency} (max ${config.maxConcurrency})`);
1780
- console.log(` InitialDelay: ${config.initialDelaySecs}s`);
1781
- console.log(` Interval: ${config.intervalMinutes}min`);
1782
- console.log(` IdleCheck: ${config.idleCheckSecs}s`);
1783
- console.log(` DevTimeout: ${config.devTimeoutMinutes}min`);
1784
- console.log(` SelfEnhance: ${config.enableSelfEnhancement}`);
1785
- console.log(` RemotePull: ${config.enableRemotePull}`);
1786
- if (config.projectPath) console.log(` Project: ${config.projectPath}`);
1787
- console.log(` Tasks: ${config.tasksDir}`);
1788
- console.log(` Log: ${logger.logPath}`);
1789
- console.log('');
1790
-
1791
- logger.write('Scheduler started (Legacy Formation - Continuous Mode)');
1792
- logger.write(`Config: Concurrency=${config.concurrency}, InitialDelay=${config.initialDelaySecs}s, Interval=${config.intervalMinutes}min, SelfEnhancement=${config.enableSelfEnhancement}, RemotePull=${config.enableRemotePull}`);
1793
-
1794
- // Yggdrasil L2: 上線通知 — 註冊到 kenaz_status
1795
- const sbClient = getSupabaseClient(config.projectPath || null);
1796
- if (sbClient) {
1797
- const { client: supabase, config: sbConfig } = sbClient;
1798
- supabase.from('kenaz_status').upsert({
1799
- node_name: sbConfig.node_name,
1800
- project: getTaskSync()?.resolveProjectName?.(config.projectPath) || 'default',
1801
- is_online: true,
1802
- branch: null,
1803
- current_task: null,
1804
- touching_files: [],
1805
- }, { onConflict: 'node_name,project' }).then(() => {
1806
- logger.write(`🌐 Yggdrasil: online (node=${sbConfig.node_name})`);
1807
- }).catch(() => {});
1808
- }
1809
-
1810
- // 初始延遲
1811
- if (config.initialDelaySecs > 0 && !overrides.dryRun) {
1812
- logger.write(`Waiting ${config.initialDelaySecs} seconds before first run...`);
1813
- if (await waitWithShutdown(config.initialDelaySecs * 1000)) {
1814
- logger.write('Shutdown during initial delay');
1815
- logger.close();
1816
- return;
1817
- }
1818
- }
1819
-
1820
- if (overrides.dryRun) {
1821
- // Dry run:只檢查任務狀態
1822
- const progress = checkPendingTasks(config);
1823
- logger.write(`[DRY RUN] Pending: ${progress.pendingCount}, Completed: ${progress.completedCount}`);
1824
- const ts = new TaskSelector(config);
1825
- const next = ts.selectNextTask();
1826
- if (next) {
1827
- logger.write(`[DRY RUN] Next task: [${next.taskId}] ${next.title} (${next.priority}, ${next.agentType})`);
1828
- } else {
1829
- logger.write('[DRY RUN] No pending tasks');
1830
- }
1831
- logger.close();
1832
- return;
1833
- }
1834
-
1835
- // 主迴圈
1836
- const result = await runMainLoop(config, logger, scopeLockMgr, { writePidHeartbeat });
1837
-
1838
- logger.write(`Scheduler finished. Total Dev Runs: ${result.runCount}, Final Status: ${result.status}`);
1839
- logger.close();
1840
- }
1841
-
1842
- // ─────────────────────────────────────────
1843
- // Crash Handlers(module-level,確保 headless 模式也有保護)
1844
- // ─────────────────────────────────────────
1845
-
1846
- function _writeCrashLog(msg) {
1847
- try {
1848
- const logDir = path.join(ROOT, '.formation', 'logs', 'legacy');
1849
- if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
1850
- const date = localDateStr();
1851
- fs.appendFileSync(path.join(logDir, `scheduler_${date}.log`), msg);
1852
- } catch {}
1853
- }
1854
-
1855
- process.on('uncaughtException', (err) => {
1856
- // EPIPE = parent closed stdout/stderr pipe (e.g. dashboard disconnected) — not fatal
1857
- if (err.code === 'EPIPE') return;
1858
- const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
1859
- const msg = `[${ts}] ❌ UNCAUGHT EXCEPTION: ${err.message}\n${err.stack}\n`;
1860
- try { console.error(msg); } catch {} // console may also be broken
1861
- _writeCrashLog(msg);
1862
- process.exit(1);
1863
- });
1864
-
1865
- process.on('unhandledRejection', (reason) => {
1866
- const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
1867
- const msg = `[${ts}] ❌ UNHANDLED REJECTION: ${reason}\n`;
1868
- try { console.error(msg); } catch {}
1869
- _writeCrashLog(msg);
1870
- });
1871
-
1872
- // ─────────────────────────────────────────
1873
- // CLI
1874
- // ─────────────────────────────────────────
1875
-
1876
- if (require.main === module) {
1877
- const args = process.argv.slice(2);
1878
-
1879
- if (args.includes('--help') || args.includes('-h')) {
1880
- console.log(`
1881
- ⚔️ Formation Legacy Scheduler (= Formation Lead)
1882
- 連續調度排程器 — 以 Formation Lead 身份運行
1883
-
1884
- Usage:
1885
- node .formation/tools/legacy-scheduler.js [options]
1886
-
1887
- Options:
1888
- --concurrency=<n> 並行執行數(預設 3,上限 5)
1889
- --dry-run 只檢查任務狀態,不啟動排程
1890
- --initial-delay=<sec> 首次延遲秒數(預設 30)
1891
- --interval=<min> 執行間隔分鐘(預設 20)
1892
- --idle-check=<sec> 無任務時輪詢秒數(預設 20)
1893
- --dev-timeout=<min> 任務超時分鐘(預設 60)
1894
- --self-enhancement 啟用 self-enhancement 模式
1895
- --remote-pull 啟用遠端任務拉取(預設 on,用 --no-remote-pull 關閉)
1896
- --no-remote-pull 關閉遠端任務拉取
1897
- --project-path=<path> 子專案路徑(掃描該專案的任務)
1898
-
1899
- Tasks:
1900
- Primary: .collaboration/tasks/{backlog,active,done}/
1901
- Legacy: dev/pipeline/{tasks,completed}/
1902
- Logs: .formation/logs/legacy/
1903
-
1904
- Behavior:
1905
- 1. 掃描 .collaboration + dev/pipeline → 按優先級選取最多 N 個 pending 任務
1906
- 2. 並行啟動 N 個 Formation Legacy Skill subagent(滑動窗口)
1907
- 3. subagent 自行完成任務移動(task-api.js complete)
1908
- 4. Scheduler 執行 post-complete(archive + git,Formation Lead 職責)
1909
- 5. Slot 完成即補入新任務,失敗 → 重置為 pending
1910
- 6. 衝突檢查:scope_paths 重疊的任務不並行
1911
- 7. --self-enhancement: 無任務時使用 review skill 分析專案
1912
- `);
1913
- process.exit(0);
1914
- }
1915
-
1916
- const getArg = (flag) => {
1917
- for (const a of args) {
1918
- if (a.startsWith(`${flag}=`)) return a.split('=').slice(1).join('=');
1919
- }
1920
- return null;
1921
- };
1922
-
1923
- const overrides = {
1924
- dryRun: args.includes('--dry-run'),
1925
- selfEnhancement: args.includes('--self-enhancement'),
1926
- remotePull: args.includes('--no-remote-pull') ? false : (args.includes('--remote-pull') ? true : undefined),
1927
- };
1928
-
1929
- const concurrency = getArg('--concurrency');
1930
- if (concurrency != null) overrides.concurrency = parseInt(concurrency, 10);
1931
-
1932
- const initialDelay = getArg('--initial-delay');
1933
- if (initialDelay != null) overrides.initialDelay = parseInt(initialDelay, 10);
1934
-
1935
- const interval = getArg('--interval');
1936
- if (interval != null) overrides.interval = parseInt(interval, 10);
1937
-
1938
- const idleCheck = getArg('--idle-check');
1939
- if (idleCheck != null) overrides.idleCheck = parseInt(idleCheck, 10);
1940
-
1941
- const devTimeout = getArg('--dev-timeout');
1942
- if (devTimeout != null) overrides.devTimeout = parseInt(devTimeout, 10);
1943
-
1944
- // [merge: main + {git_user}] --instance-id (main) + --project-path & crash handlers ({git_user})
1945
- const instanceId = getArg('--instance-id');
1946
- if (instanceId) overrides.instanceId = instanceId;
1947
-
1948
- const projectPath = getArg('--project-path');
1949
- if (projectPath) overrides.projectPath = projectPath;
1950
-
1951
- // Crash handlers — 捕捉未預期的崩潰
1952
- // Use project-aware log dir for crash logs
1953
- const crashLogDir = projectPath
1954
- ? path.join(getLogDir(projectPath), 'legacy')
1955
- : path.join(ROOT, '.formation', 'logs', 'legacy');
1956
-
1957
- process.on('uncaughtException', (err) => {
1958
- // EPIPE = parent closed stdout/stderr pipe (e.g. dashboard disconnected) — not fatal
1959
- if (err.code === 'EPIPE') return;
1960
- const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
1961
- const msg = `[${ts}] ❌ UNCAUGHT EXCEPTION: ${err.message}\n${err.stack}\n`;
1962
- try { console.error(msg); } catch {} // console may also be broken
1963
- try {
1964
- const date = new Date().toISOString().slice(0, 10);
1965
- fs.appendFileSync(path.join(crashLogDir, `scheduler_${date}.log`), msg);
1966
- } catch {}
1967
- process.exit(1);
1968
- });
1969
- process.on('unhandledRejection', (reason) => {
1970
- if (reason && reason.code === 'EPIPE') return;
1971
- const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
1972
- const msg = `[${ts}] ❌ UNHANDLED REJECTION: ${reason}\n`;
1973
- try { console.error(msg); } catch {}
1974
- try {
1975
- const date = new Date().toISOString().slice(0, 10);
1976
- fs.appendFileSync(path.join(crashLogDir, `scheduler_${date}.log`), msg);
1977
- } catch {}
1978
- });
1979
-
1980
- start(overrides).catch(err => {
1981
- console.error('❌ Scheduler error:', err.message);
1982
- process.exit(1);
1983
- });
1984
- }
1985
-
1986
- module.exports = {
1987
- start,
1988
- loadConfig,
1989
- checkPendingTasks,
1990
- TaskSelector,
1991
- requestShutdown,
1992
- Status,
1993
- RunStatus,
1994
- };