kenaz 0.3.2 → 0.3.3

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 (390) 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 +19 -30
  4. package/core/.claude/hooks/_ansuz-conversation.js +1 -0
  5. package/core/.claude/hooks/_ansuz-conversation.test.js +6 -0
  6. package/core/.claude/hooks/_run-user-prompt.js +0 -4
  7. package/core/.claude/hooks/_run-user-prompt.test.js +2 -3
  8. package/core/.claude/hooks/session-init.ps1 +49 -49
  9. package/core/.claude/settings.json +0 -37
  10. package/core/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -26
  11. package/core/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -97
  12. package/core/.claude/skills/ui-ux-pro-max/data/icons.csv +101 -101
  13. package/core/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -31
  14. package/core/.claude/skills/ui-ux-pro-max/data/products.csv +96 -96
  15. package/core/.claude/skills/ui-ux-pro-max/data/react-performance.csv +45 -45
  16. package/core/.claude/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -54
  17. package/core/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -53
  18. package/core/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -56
  19. package/core/.claude/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -53
  20. package/core/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -53
  21. package/core/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -51
  22. package/core/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -59
  23. package/core/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -52
  24. package/core/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -54
  25. package/core/.claude/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -61
  26. package/core/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -54
  27. package/core/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -51
  28. package/core/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -50
  29. package/core/.claude/skills/ui-ux-pro-max/data/styles.csv +68 -68
  30. package/core/.claude/skills/ui-ux-pro-max/data/typography.csv +57 -57
  31. package/core/.claude/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -101
  32. package/core/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +99 -99
  33. package/core/.claude/skills/ui-ux-pro-max/data/web-interface.csv +31 -31
  34. package/core/.claude/skills/ui-ux-pro-max/scripts/core.py +253 -253
  35. package/core/.claude/skills/ui-ux-pro-max/scripts/design_system.py +1067 -1067
  36. package/core/.claude/skills/ui-ux-pro-max/scripts/search.py +106 -106
  37. package/core/.collaboration/core/task-api.js +3 -8
  38. package/core/.formation/tests/hello.txt +2 -2
  39. package/core/.formation/tools/prompt.js +58 -5
  40. package/core/.formation/tools/prompt.test.js +29 -0
  41. package/core/.kenaz/claude-hooks-manifest.json +16 -16
  42. package/core/.kenaz/knowledge/ladybug_2c51da1e_shadow_file_frame_group.patch +163 -163
  43. package/core/.kenaz/knowledge/ladybug_30cf85690e_shadow_page_read_during_checkpoint.patch +393 -393
  44. package/core/.kenaz/knowledge/ladybug_7a17858d3a_node_group_delete_lock.patch +102 -102
  45. package/core/.kenaz/knowledge/ladybug_926a0ba1_column_checkpoint.patch +412 -412
  46. package/core/.kenaz/knowledge/ladybug_a346c4c4b6_pk_index_header_pages.patch +144 -144
  47. package/core/.kenaz/knowledge/ladybug_ba5f38815b_csr_node_group.patch +86 -86
  48. package/core/.kenaz/muninn_episode_write.ps1 +17 -17
  49. package/core/.kenaz/specialist/field_report_watermark.json +1 -1
  50. package/core/.kenaz/specialist/memory_guard_fired.json +1 -1
  51. package/core/.kenaz/specialist/memory_guard_trigger_cache.json +1 -1
  52. package/core/.kenaz/specialist/t1777_probe.txt +1 -1
  53. package/core/.matrix/index.matrix +5 -5
  54. package/core/.specialist/Ansuz/RULES.md +6 -10
  55. package/core/.specialist/tools/__tests__/task-2218-boot-session-id.test.js +17 -1
  56. package/core/.specialist/tools/__tests__/task-2222-session-identity-interleave.test.js +17 -1
  57. package/core/.specialist/tools/__tests__/task-2254-handoff-archive-contract.test.js +6 -5
  58. package/core/.specialist/tools/ansuz-boot-fast.js +44 -100
  59. package/core/.specialist/tools/ansuz-boot-fast.test.js +45 -1
  60. package/core/KENAZ_CORE_VERSION +1 -1
  61. package/core/dev/ANSUZ_STARTUP_INJECTION_REVIEW.md +355 -355
  62. package/core/dev/JEV_INTEGRATION_PLAN.md +189 -189
  63. package/core/dev/design/matrix_format_EXAMPLE_memory.matrix +13 -13
  64. package/core/dev/design/matrix_format_EXAMPLE_project_status.matrix +9 -9
  65. package/core/dev/design/matrix_format_EXAMPLE_tasks.matrix +14 -14
  66. package/core/dev/design/matrix_v2_EXAMPLE.matrix +67 -67
  67. package/core/dev/jev-eval/ACTIVE_CORRECTION.md +52 -52
  68. package/core/dev/jev-eval/CONTEXT_REVIEW.md +31 -31
  69. package/core/dev/jev-eval/EVALUATION_STATUS.md +39 -39
  70. package/core/dev/jev-eval/HISTORICAL_CORPUS.md +33 -33
  71. package/core/dev/jev-eval/README.md +99 -99
  72. package/core/dev/jev-eval/THALAMUS_RANKING.md +58 -58
  73. package/core/dev/jev-eval/reports/annotation-pilot-2026-09-20.json +32 -32
  74. package/core/dev/jev-eval/reports/context-review-2026-09-20.json +48 -48
  75. package/core/dev/jev-eval/reports/history-corpus-2026-09-20.json +52 -52
  76. package/core/dev/pipeline/SELF_ENHANCEMENT_STATUS.json.bak +47 -47
  77. package/core/dev/pipeline/tasks/.gitkeep +1 -1
  78. package/core/dev/pipeline/test_results/TASK_TEST_PARALLEL_002_result.txt +27 -27
  79. package/core/dev/plans/ANSUZ_SLIM_PLAN.md +68 -0
  80. package/core/dev/scripts/init_git.sh +0 -0
  81. package/core/dev/scripts/init_project.sh +0 -0
  82. package/core/dev/scripts/legacy/dev_agent_scheduler.ps1.bak +536 -536
  83. package/core/dev/scripts/legacy/multi_agent_scheduler.ps1.bak +1978 -1978
  84. package/core/dev/scripts/matrix/src/cli/commands/list-tags.ts.wip +140 -140
  85. package/core/dev/scripts/matrix/src/cli/commands/manage-tags.ts.wip +173 -173
  86. package/core/dev/scripts/matrix/src/cli/commands/stats.ts.wip +168 -168
  87. package/core/dev/scripts/matrix/src/cli/commands/tokens.ts.wip +126 -126
  88. package/core/dev/scripts/multi_agent_scheduler.sh +0 -0
  89. package/core/dev/scripts/run_once.sh +0 -0
  90. package/core/dev/scripts/setup_mcp.sh +0 -0
  91. package/core/dev/scripts/setup_rag.sh +0 -0
  92. package/core/dev/scripts/start_dev_agent.sh +0 -0
  93. package/core/dev/scripts/start_multi_agent.sh +0 -0
  94. package/core/dev/scripts/sync_task_queue.sh +0 -0
  95. package/core/k-cli/dist/alaya/run.js +91 -0
  96. package/core/k-cli/dist/core/output.js +24 -0
  97. package/core/k-cli/dist/core/process.js +38 -0
  98. package/core/k-cli/dist/core/project.js +80 -0
  99. package/core/k-cli/dist/core/spawn.js +268 -0
  100. package/core/k-cli/dist/core/types.js +2 -0
  101. package/core/k-cli/dist/dashboard/update.js +53 -0
  102. package/core/k-cli/dist/dashboard/version.js +20 -0
  103. package/core/k-cli/dist/dispatch/run.js +241 -0
  104. package/core/k-cli/dist/dispatch/status.js +103 -0
  105. package/core/k-cli/dist/index.js +283 -0
  106. package/core/k-cli/dist/project/initialize.js +301 -0
  107. package/core/k-cli/dist/project/install.js +89 -0
  108. package/core/k-cli/dist/project/status.js +138 -0
  109. package/core/k-cli/dist/specialist/list.js +100 -0
  110. package/core/k-cli/dist/specialist/run.js +93 -0
  111. package/core/k-cli/dist/task/create.js +194 -0
  112. package/core/k-cli/dist/task/delete.js +98 -0
  113. package/core/k-cli/dist/task/get.js +55 -0
  114. package/core/k-cli/dist/task/list.js +58 -0
  115. package/core/k-cli/dist/task/submit.js +156 -0
  116. package/core/k-cli/dist/ygg/run.js +51 -0
  117. package/core/k-cli/node_modules/.bin/yaml +16 -0
  118. package/core/k-cli/node_modules/.bin/yaml.cmd +17 -0
  119. package/core/k-cli/node_modules/.bin/yaml.ps1 +28 -0
  120. package/core/k-cli/node_modules/.package-lock.json +23 -0
  121. package/core/k-cli/node_modules/yaml/LICENSE +13 -0
  122. package/core/k-cli/node_modules/yaml/README.md +172 -0
  123. package/core/k-cli/node_modules/yaml/bin.mjs +11 -0
  124. package/core/k-cli/node_modules/yaml/browser/dist/compose/compose-collection.js +88 -0
  125. package/core/k-cli/node_modules/yaml/browser/dist/compose/compose-doc.js +43 -0
  126. package/core/k-cli/node_modules/yaml/browser/dist/compose/compose-node.js +109 -0
  127. package/core/k-cli/node_modules/yaml/browser/dist/compose/compose-scalar.js +86 -0
  128. package/core/k-cli/node_modules/yaml/browser/dist/compose/composer.js +217 -0
  129. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-block-map.js +115 -0
  130. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
  131. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-block-seq.js +49 -0
  132. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-end.js +37 -0
  133. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-flow-collection.js +207 -0
  134. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +223 -0
  135. package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-props.js +146 -0
  136. package/core/k-cli/node_modules/yaml/browser/dist/compose/util-contains-newline.js +34 -0
  137. package/core/k-cli/node_modules/yaml/browser/dist/compose/util-empty-scalar-position.js +26 -0
  138. package/core/k-cli/node_modules/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
  139. package/core/k-cli/node_modules/yaml/browser/dist/compose/util-map-includes.js +13 -0
  140. package/core/k-cli/node_modules/yaml/browser/dist/doc/Document.js +335 -0
  141. package/core/k-cli/node_modules/yaml/browser/dist/doc/anchors.js +71 -0
  142. package/core/k-cli/node_modules/yaml/browser/dist/doc/applyReviver.js +55 -0
  143. package/core/k-cli/node_modules/yaml/browser/dist/doc/createNode.js +88 -0
  144. package/core/k-cli/node_modules/yaml/browser/dist/doc/directives.js +176 -0
  145. package/core/k-cli/node_modules/yaml/browser/dist/errors.js +57 -0
  146. package/core/k-cli/node_modules/yaml/browser/dist/index.js +17 -0
  147. package/core/k-cli/node_modules/yaml/browser/dist/log.js +11 -0
  148. package/core/k-cli/node_modules/yaml/browser/dist/nodes/Alias.js +114 -0
  149. package/core/k-cli/node_modules/yaml/browser/dist/nodes/Collection.js +147 -0
  150. package/core/k-cli/node_modules/yaml/browser/dist/nodes/Node.js +38 -0
  151. package/core/k-cli/node_modules/yaml/browser/dist/nodes/Pair.js +36 -0
  152. package/core/k-cli/node_modules/yaml/browser/dist/nodes/Scalar.js +24 -0
  153. package/core/k-cli/node_modules/yaml/browser/dist/nodes/YAMLMap.js +144 -0
  154. package/core/k-cli/node_modules/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
  155. package/core/k-cli/node_modules/yaml/browser/dist/nodes/addPairToJSMap.js +63 -0
  156. package/core/k-cli/node_modules/yaml/browser/dist/nodes/identity.js +36 -0
  157. package/core/k-cli/node_modules/yaml/browser/dist/nodes/toJS.js +37 -0
  158. package/core/k-cli/node_modules/yaml/browser/dist/parse/cst-scalar.js +214 -0
  159. package/core/k-cli/node_modules/yaml/browser/dist/parse/cst-stringify.js +61 -0
  160. package/core/k-cli/node_modules/yaml/browser/dist/parse/cst-visit.js +97 -0
  161. package/core/k-cli/node_modules/yaml/browser/dist/parse/cst.js +98 -0
  162. package/core/k-cli/node_modules/yaml/browser/dist/parse/lexer.js +717 -0
  163. package/core/k-cli/node_modules/yaml/browser/dist/parse/line-counter.js +39 -0
  164. package/core/k-cli/node_modules/yaml/browser/dist/parse/parser.js +967 -0
  165. package/core/k-cli/node_modules/yaml/browser/dist/public-api.js +102 -0
  166. package/core/k-cli/node_modules/yaml/browser/dist/schema/Schema.js +37 -0
  167. package/core/k-cli/node_modules/yaml/browser/dist/schema/common/map.js +17 -0
  168. package/core/k-cli/node_modules/yaml/browser/dist/schema/common/null.js +15 -0
  169. package/core/k-cli/node_modules/yaml/browser/dist/schema/common/seq.js +17 -0
  170. package/core/k-cli/node_modules/yaml/browser/dist/schema/common/string.js +14 -0
  171. package/core/k-cli/node_modules/yaml/browser/dist/schema/core/bool.js +19 -0
  172. package/core/k-cli/node_modules/yaml/browser/dist/schema/core/float.js +43 -0
  173. package/core/k-cli/node_modules/yaml/browser/dist/schema/core/int.js +38 -0
  174. package/core/k-cli/node_modules/yaml/browser/dist/schema/core/schema.js +23 -0
  175. package/core/k-cli/node_modules/yaml/browser/dist/schema/json/schema.js +62 -0
  176. package/core/k-cli/node_modules/yaml/browser/dist/schema/tags.js +96 -0
  177. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/binary.js +58 -0
  178. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
  179. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
  180. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
  181. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/merge.js +64 -0
  182. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
  183. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
  184. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/schema.js +39 -0
  185. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
  186. package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
  187. package/core/k-cli/node_modules/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
  188. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringify.js +129 -0
  189. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyCollection.js +153 -0
  190. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyComment.js +20 -0
  191. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
  192. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyNumber.js +24 -0
  193. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyPair.js +150 -0
  194. package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyString.js +336 -0
  195. package/core/k-cli/node_modules/yaml/browser/dist/util.js +11 -0
  196. package/core/k-cli/node_modules/yaml/browser/dist/visit.js +233 -0
  197. package/core/k-cli/node_modules/yaml/browser/index.js +5 -0
  198. package/core/k-cli/node_modules/yaml/browser/package.json +3 -0
  199. package/core/k-cli/node_modules/yaml/dist/cli.d.ts +8 -0
  200. package/core/k-cli/node_modules/yaml/dist/cli.mjs +201 -0
  201. package/core/k-cli/node_modules/yaml/dist/compose/compose-collection.d.ts +11 -0
  202. package/core/k-cli/node_modules/yaml/dist/compose/compose-collection.js +90 -0
  203. package/core/k-cli/node_modules/yaml/dist/compose/compose-doc.d.ts +7 -0
  204. package/core/k-cli/node_modules/yaml/dist/compose/compose-doc.js +45 -0
  205. package/core/k-cli/node_modules/yaml/dist/compose/compose-node.d.ts +29 -0
  206. package/core/k-cli/node_modules/yaml/dist/compose/compose-node.js +112 -0
  207. package/core/k-cli/node_modules/yaml/dist/compose/compose-scalar.d.ts +5 -0
  208. package/core/k-cli/node_modules/yaml/dist/compose/compose-scalar.js +88 -0
  209. package/core/k-cli/node_modules/yaml/dist/compose/composer.d.ts +63 -0
  210. package/core/k-cli/node_modules/yaml/dist/compose/composer.js +222 -0
  211. package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-map.d.ts +6 -0
  212. package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-map.js +117 -0
  213. package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
  214. package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-scalar.js +200 -0
  215. package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
  216. package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-seq.js +51 -0
  217. package/core/k-cli/node_modules/yaml/dist/compose/resolve-end.d.ts +6 -0
  218. package/core/k-cli/node_modules/yaml/dist/compose/resolve-end.js +39 -0
  219. package/core/k-cli/node_modules/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
  220. package/core/k-cli/node_modules/yaml/dist/compose/resolve-flow-collection.js +209 -0
  221. package/core/k-cli/node_modules/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
  222. package/core/k-cli/node_modules/yaml/dist/compose/resolve-flow-scalar.js +225 -0
  223. package/core/k-cli/node_modules/yaml/dist/compose/resolve-props.d.ts +23 -0
  224. package/core/k-cli/node_modules/yaml/dist/compose/resolve-props.js +148 -0
  225. package/core/k-cli/node_modules/yaml/dist/compose/util-contains-newline.d.ts +2 -0
  226. package/core/k-cli/node_modules/yaml/dist/compose/util-contains-newline.js +36 -0
  227. package/core/k-cli/node_modules/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
  228. package/core/k-cli/node_modules/yaml/dist/compose/util-empty-scalar-position.js +28 -0
  229. package/core/k-cli/node_modules/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
  230. package/core/k-cli/node_modules/yaml/dist/compose/util-flow-indent-check.js +17 -0
  231. package/core/k-cli/node_modules/yaml/dist/compose/util-map-includes.d.ts +4 -0
  232. package/core/k-cli/node_modules/yaml/dist/compose/util-map-includes.js +15 -0
  233. package/core/k-cli/node_modules/yaml/dist/doc/Document.d.ts +141 -0
  234. package/core/k-cli/node_modules/yaml/dist/doc/Document.js +337 -0
  235. package/core/k-cli/node_modules/yaml/dist/doc/anchors.d.ts +24 -0
  236. package/core/k-cli/node_modules/yaml/dist/doc/anchors.js +76 -0
  237. package/core/k-cli/node_modules/yaml/dist/doc/applyReviver.d.ts +9 -0
  238. package/core/k-cli/node_modules/yaml/dist/doc/applyReviver.js +57 -0
  239. package/core/k-cli/node_modules/yaml/dist/doc/createNode.d.ts +17 -0
  240. package/core/k-cli/node_modules/yaml/dist/doc/createNode.js +90 -0
  241. package/core/k-cli/node_modules/yaml/dist/doc/directives.d.ts +49 -0
  242. package/core/k-cli/node_modules/yaml/dist/doc/directives.js +178 -0
  243. package/core/k-cli/node_modules/yaml/dist/errors.d.ts +21 -0
  244. package/core/k-cli/node_modules/yaml/dist/errors.js +62 -0
  245. package/core/k-cli/node_modules/yaml/dist/index.d.ts +25 -0
  246. package/core/k-cli/node_modules/yaml/dist/index.js +50 -0
  247. package/core/k-cli/node_modules/yaml/dist/log.d.ts +3 -0
  248. package/core/k-cli/node_modules/yaml/dist/log.js +19 -0
  249. package/core/k-cli/node_modules/yaml/dist/nodes/Alias.d.ts +29 -0
  250. package/core/k-cli/node_modules/yaml/dist/nodes/Alias.js +116 -0
  251. package/core/k-cli/node_modules/yaml/dist/nodes/Collection.d.ts +73 -0
  252. package/core/k-cli/node_modules/yaml/dist/nodes/Collection.js +151 -0
  253. package/core/k-cli/node_modules/yaml/dist/nodes/Node.d.ts +53 -0
  254. package/core/k-cli/node_modules/yaml/dist/nodes/Node.js +40 -0
  255. package/core/k-cli/node_modules/yaml/dist/nodes/Pair.d.ts +22 -0
  256. package/core/k-cli/node_modules/yaml/dist/nodes/Pair.js +39 -0
  257. package/core/k-cli/node_modules/yaml/dist/nodes/Scalar.d.ts +43 -0
  258. package/core/k-cli/node_modules/yaml/dist/nodes/Scalar.js +27 -0
  259. package/core/k-cli/node_modules/yaml/dist/nodes/YAMLMap.d.ts +53 -0
  260. package/core/k-cli/node_modules/yaml/dist/nodes/YAMLMap.js +147 -0
  261. package/core/k-cli/node_modules/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
  262. package/core/k-cli/node_modules/yaml/dist/nodes/YAMLSeq.js +115 -0
  263. package/core/k-cli/node_modules/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
  264. package/core/k-cli/node_modules/yaml/dist/nodes/addPairToJSMap.js +65 -0
  265. package/core/k-cli/node_modules/yaml/dist/nodes/identity.d.ts +23 -0
  266. package/core/k-cli/node_modules/yaml/dist/nodes/identity.js +53 -0
  267. package/core/k-cli/node_modules/yaml/dist/nodes/toJS.d.ts +29 -0
  268. package/core/k-cli/node_modules/yaml/dist/nodes/toJS.js +39 -0
  269. package/core/k-cli/node_modules/yaml/dist/options.d.ts +350 -0
  270. package/core/k-cli/node_modules/yaml/dist/parse/cst-scalar.d.ts +64 -0
  271. package/core/k-cli/node_modules/yaml/dist/parse/cst-scalar.js +218 -0
  272. package/core/k-cli/node_modules/yaml/dist/parse/cst-stringify.d.ts +8 -0
  273. package/core/k-cli/node_modules/yaml/dist/parse/cst-stringify.js +63 -0
  274. package/core/k-cli/node_modules/yaml/dist/parse/cst-visit.d.ts +39 -0
  275. package/core/k-cli/node_modules/yaml/dist/parse/cst-visit.js +99 -0
  276. package/core/k-cli/node_modules/yaml/dist/parse/cst.d.ts +109 -0
  277. package/core/k-cli/node_modules/yaml/dist/parse/cst.js +112 -0
  278. package/core/k-cli/node_modules/yaml/dist/parse/lexer.d.ts +87 -0
  279. package/core/k-cli/node_modules/yaml/dist/parse/lexer.js +719 -0
  280. package/core/k-cli/node_modules/yaml/dist/parse/line-counter.d.ts +22 -0
  281. package/core/k-cli/node_modules/yaml/dist/parse/line-counter.js +41 -0
  282. package/core/k-cli/node_modules/yaml/dist/parse/parser.d.ts +84 -0
  283. package/core/k-cli/node_modules/yaml/dist/parse/parser.js +972 -0
  284. package/core/k-cli/node_modules/yaml/dist/public-api.d.ts +44 -0
  285. package/core/k-cli/node_modules/yaml/dist/public-api.js +107 -0
  286. package/core/k-cli/node_modules/yaml/dist/schema/Schema.d.ts +17 -0
  287. package/core/k-cli/node_modules/yaml/dist/schema/Schema.js +39 -0
  288. package/core/k-cli/node_modules/yaml/dist/schema/common/map.d.ts +2 -0
  289. package/core/k-cli/node_modules/yaml/dist/schema/common/map.js +19 -0
  290. package/core/k-cli/node_modules/yaml/dist/schema/common/null.d.ts +4 -0
  291. package/core/k-cli/node_modules/yaml/dist/schema/common/null.js +17 -0
  292. package/core/k-cli/node_modules/yaml/dist/schema/common/seq.d.ts +2 -0
  293. package/core/k-cli/node_modules/yaml/dist/schema/common/seq.js +19 -0
  294. package/core/k-cli/node_modules/yaml/dist/schema/common/string.d.ts +2 -0
  295. package/core/k-cli/node_modules/yaml/dist/schema/common/string.js +16 -0
  296. package/core/k-cli/node_modules/yaml/dist/schema/core/bool.d.ts +4 -0
  297. package/core/k-cli/node_modules/yaml/dist/schema/core/bool.js +21 -0
  298. package/core/k-cli/node_modules/yaml/dist/schema/core/float.d.ts +4 -0
  299. package/core/k-cli/node_modules/yaml/dist/schema/core/float.js +47 -0
  300. package/core/k-cli/node_modules/yaml/dist/schema/core/int.d.ts +4 -0
  301. package/core/k-cli/node_modules/yaml/dist/schema/core/int.js +42 -0
  302. package/core/k-cli/node_modules/yaml/dist/schema/core/schema.d.ts +1 -0
  303. package/core/k-cli/node_modules/yaml/dist/schema/core/schema.js +25 -0
  304. package/core/k-cli/node_modules/yaml/dist/schema/json/schema.d.ts +2 -0
  305. package/core/k-cli/node_modules/yaml/dist/schema/json/schema.js +64 -0
  306. package/core/k-cli/node_modules/yaml/dist/schema/json-schema.d.ts +69 -0
  307. package/core/k-cli/node_modules/yaml/dist/schema/tags.d.ts +48 -0
  308. package/core/k-cli/node_modules/yaml/dist/schema/tags.js +99 -0
  309. package/core/k-cli/node_modules/yaml/dist/schema/types.d.ts +92 -0
  310. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
  311. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/binary.js +70 -0
  312. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
  313. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/bool.js +29 -0
  314. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
  315. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/float.js +50 -0
  316. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
  317. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/int.js +76 -0
  318. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/merge.d.ts +9 -0
  319. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/merge.js +68 -0
  320. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts +22 -0
  321. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/omap.js +77 -0
  322. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
  323. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
  324. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
  325. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/schema.js +41 -0
  326. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
  327. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/set.js +96 -0
  328. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
  329. package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
  330. package/core/k-cli/node_modules/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
  331. package/core/k-cli/node_modules/yaml/dist/stringify/foldFlowLines.js +151 -0
  332. package/core/k-cli/node_modules/yaml/dist/stringify/stringify.d.ts +21 -0
  333. package/core/k-cli/node_modules/yaml/dist/stringify/stringify.js +132 -0
  334. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
  335. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyCollection.js +155 -0
  336. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyComment.d.ts +10 -0
  337. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyComment.js +24 -0
  338. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
  339. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyDocument.js +87 -0
  340. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
  341. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyNumber.js +26 -0
  342. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyPair.d.ts +3 -0
  343. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyPair.js +152 -0
  344. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyString.d.ts +9 -0
  345. package/core/k-cli/node_modules/yaml/dist/stringify/stringifyString.js +338 -0
  346. package/core/k-cli/node_modules/yaml/dist/test-events.d.ts +4 -0
  347. package/core/k-cli/node_modules/yaml/dist/test-events.js +134 -0
  348. package/core/k-cli/node_modules/yaml/dist/util.d.ts +16 -0
  349. package/core/k-cli/node_modules/yaml/dist/util.js +28 -0
  350. package/core/k-cli/node_modules/yaml/dist/visit.d.ts +102 -0
  351. package/core/k-cli/node_modules/yaml/dist/visit.js +236 -0
  352. package/core/k-cli/node_modules/yaml/package.json +97 -0
  353. package/core/k-cli/node_modules/yaml/util.js +2 -0
  354. package/core/plugins/kenaz/commands/Ansuz.md +19 -30
  355. package/core/plugins/kenaz/hooks/hooks.json +1 -5
  356. package/core/plugins/kenaz/hooks/kenaz-hook.js +0 -4
  357. package/core/plugins/kenaz/scripts/kenaz.test.js +1 -1
  358. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/charts.csv +26 -26
  359. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/colors.csv +97 -97
  360. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/icons.csv +101 -101
  361. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/landing.csv +31 -31
  362. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/products.csv +96 -96
  363. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/react-performance.csv +45 -45
  364. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -54
  365. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -53
  366. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -56
  367. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -53
  368. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -53
  369. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -51
  370. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -59
  371. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -52
  372. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/react.csv +54 -54
  373. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -61
  374. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -54
  375. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -51
  376. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -50
  377. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/styles.csv +68 -68
  378. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/typography.csv +57 -57
  379. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -101
  380. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/ux-guidelines.csv +99 -99
  381. package/core/plugins/kenaz/skills/ui-ux-pro-max/data/web-interface.csv +31 -31
  382. package/core/plugins/kenaz/skills/ui-ux-pro-max/scripts/core.py +253 -253
  383. package/core/plugins/kenaz/skills/ui-ux-pro-max/scripts/design_system.py +1067 -1067
  384. package/core/plugins/kenaz/skills/ui-ux-pro-max/scripts/search.py +106 -106
  385. package/lib/cli.js +36 -2
  386. package/lib/cli.test.js +24 -0
  387. package/lib/dashboard.js +10 -0
  388. package/package.json +6 -6
  389. package/core/.specialist/tools/__tests__/task-1863-boot-lease-reuse.test.js +0 -99
  390. package/core/.specialist/tools/__tests__/task-1977-banner-cap.test.js +0 -129
@@ -1,1978 +1,1978 @@
1
- # ============================================================
2
- # Multi-Agent Parallel Scheduler
3
- # ============================================================
4
- # 啟動多個 Claude Agent 並行開發
5
- # - 自動分配無衝突的任務給多個 Agent
6
- # - 使用路徑鎖定防止衝突
7
- # - 每個 Agent 在獨立 Branch 工作
8
- # ============================================================
9
-
10
- param(
11
- [int]$MaxAgents = 3, # 最大並行 Agent 數量
12
- [int]$PollIntervalSeconds = 30, # 輪詢間隔
13
- [int]$TaskTimeoutMinutes = 60, # 單任務超時
14
- [switch]$DryRun, # 模擬運行(不實際啟動)
15
- [switch]$SingleRun, # 單次運行(不循環)
16
- [string]$WorkingDirectory, # 工作目錄
17
- [switch]$nopause, # 從 Tauri 調用時使用(跳過暫停)
18
- [switch]$Standalone # 獨立模式:忽略 Dashboard heartbeat,適合開發 Tauri 本身時使用
19
- )
20
-
21
- # Helper function to normalize Windows extended path format (\\?\)
22
- function Get-NormalizedPath {
23
- param([string]$Path)
24
- if ($Path -and $Path.StartsWith("\\?\")) {
25
- return $Path.Substring(4)
26
- }
27
- return $Path
28
- }
29
-
30
- # Use KENAZ_PROJECT_PATH env var if WorkingDirectory not specified
31
- if (-not $WorkingDirectory) {
32
- if ($env:KENAZ_PROJECT_PATH) {
33
- $WorkingDirectory = $env:KENAZ_PROJECT_PATH
34
- } else {
35
- $WorkingDirectory = (Get-Location).Path
36
- }
37
- }
38
-
39
- # Normalize WorkingDirectory to handle \\?\ prefix from Tauri
40
- $WorkingDirectory = Get-NormalizedPath $WorkingDirectory
41
-
42
- # === STARTUP DEBUG LOG ===
43
- # Write startup info to a debug file for troubleshooting Tauri execution issues
44
- $StartupDebugFile = Join-Path $WorkingDirectory "multi_agent_startup.log"
45
-
46
- # Helper function to append to debug log
47
- function Write-DebugLog {
48
- param([string]$Message, [string]$Level = "INFO")
49
- $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
50
- $logLine = "[$timestamp] [$Level] $Message"
51
- try {
52
- Add-Content -Path $StartupDebugFile -Value $logLine -Encoding UTF8 -ErrorAction SilentlyContinue
53
- } catch { }
54
- if ($Level -eq "ERROR") {
55
- Write-Host $logLine -ForegroundColor Red
56
- }
57
- }
58
-
59
- try {
60
- $startupInfo = @"
61
- === Multi-Agent Scheduler Startup ===
62
- Time: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
63
- WorkingDirectory (raw param): $($PSBoundParameters['WorkingDirectory'])
64
- WorkingDirectory (normalized): $WorkingDirectory
65
- KENAZ_PROJECT_PATH env: $($env:KENAZ_PROJECT_PATH)
66
- Script Path: $($MyInvocation.MyCommand.Path)
67
- Script Directory: $(Split-Path -Parent $MyInvocation.MyCommand.Path)
68
- PowerShell Version: $($PSVersionTable.PSVersion)
69
- OS: $($PSVersionTable.OS)
70
- Platform: $($PSVersionTable.Platform)
71
- Arguments: $($args -join ' ')
72
- nopause: $nopause
73
- DryRun: $DryRun
74
- SingleRun: $SingleRun
75
- Standalone: $Standalone
76
- MaxAgents: $MaxAgents
77
- PollIntervalSeconds: $PollIntervalSeconds
78
- TaskTimeoutMinutes: $TaskTimeoutMinutes
79
-
80
- === Path Checks ===
81
- WorkingDirectory exists: $(Test-Path $WorkingDirectory)
82
- dev folder exists: $(Test-Path (Join-Path $WorkingDirectory 'dev'))
83
- dev/pipeline exists: $(Test-Path (Join-Path $WorkingDirectory 'dev\pipeline'))
84
- dev/scripts exists: $(Test-Path (Join-Path $WorkingDirectory 'dev\scripts'))
85
- TASK_QUEUE.json exists: $(Test-Path (Join-Path $WorkingDirectory 'dev\pipeline\TASK_QUEUE.json'))
86
-
87
- === Environment Variables ===
88
- PATH (first 500 chars): $($env:PATH.Substring(0, [Math]::Min(500, $env:PATH.Length)))
89
-
90
- === Execution Log ===
91
- "@
92
- # Ensure directory exists
93
- $debugDir = Split-Path $StartupDebugFile -Parent
94
- if (-not (Test-Path $debugDir)) {
95
- New-Item -ItemType Directory -Path $debugDir -Force | Out-Null
96
- }
97
- Set-Content -Path $StartupDebugFile -Value $startupInfo -Encoding UTF8
98
- Write-Host "[DEBUG] Startup log written to: $StartupDebugFile" -ForegroundColor DarkGray
99
- } catch {
100
- Write-Host "[DEBUG] Failed to write startup log: $_" -ForegroundColor Yellow
101
- }
102
- # === END STARTUP DEBUG LOG ===
103
-
104
- # 檢測是否在交互式控制台
105
- $script:IsInteractiveConsole = $false
106
- try {
107
- $null = [Console]::CursorTop
108
- $script:IsInteractiveConsole = $true
109
- } catch {
110
- $script:IsInteractiveConsole = $false
111
- }
112
-
113
- # Standalone 模式(忽略 Dashboard heartbeat)
114
- $script:StandaloneMode = $Standalone
115
-
116
- # 在 Standalone 模式下,刪除舊的 heartbeat 文件避免混淆
117
- if ($Standalone) {
118
- $heartbeatPath = Join-Path $WorkingDirectory "dev\pipeline\DASHBOARD_HEARTBEAT.json"
119
- if (Test-Path $heartbeatPath) {
120
- Remove-Item $heartbeatPath -Force -ErrorAction SilentlyContinue
121
- Write-Host "[STANDALONE] Removed old heartbeat file" -ForegroundColor Magenta
122
- }
123
- }
124
-
125
- # 配置
126
- $LogFile = Join-Path $WorkingDirectory "dev\pipeline\logs\multi_agent_$(Get-Date -Format 'yyyy-MM-dd').log"
127
- $TaskQueueFile = Join-Path $WorkingDirectory "dev\pipeline\TASK_QUEUE.json"
128
- $LockStateFile = Join-Path $WorkingDirectory "dev\pipeline\LOCK_STATE.json"
129
- $AgentStateFile = Join-Path $WorkingDirectory "dev\pipeline\AGENT_STATE.json"
130
- $HeartbeatFile = Join-Path $WorkingDirectory "dev\pipeline\DASHBOARD_HEARTBEAT.json"
131
-
132
- # Heartbeat timeout disabled - only stop on explicit "closing" signal
133
-
134
- # 確保目錄存在
135
- $LogDir = Split-Path $LogFile -Parent
136
- if (-not (Test-Path $LogDir)) {
137
- New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
138
- }
139
-
140
- # ============================================================
141
- # 工具函數
142
- # ============================================================
143
-
144
- function Write-Log {
145
- param([string]$Message, [string]$Level = "INFO")
146
- $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
147
- $LogMessage = "[$Timestamp] [$Level] $Message"
148
-
149
- switch ($Level) {
150
- "ERROR" { Write-Host $LogMessage -ForegroundColor Red }
151
- "WARN" { Write-Host $LogMessage -ForegroundColor Yellow }
152
- "SUCCESS" { Write-Host $LogMessage -ForegroundColor Green }
153
- default { Write-Host $LogMessage }
154
- }
155
-
156
- Add-Content -Path $LogFile -Value $LogMessage -Encoding UTF8
157
- }
158
-
159
- function Write-FileWithRetry {
160
- param(
161
- [string]$Path,
162
- [string]$Content,
163
- [int]$MaxRetries = 5,
164
- [int]$RetryDelayMs = 200
165
- )
166
-
167
- $attempt = 0
168
- while ($attempt -lt $MaxRetries) {
169
- try {
170
- $Content | Set-Content $Path -Encoding UTF8 -ErrorAction Stop
171
- return $true
172
- } catch [System.IO.IOException] {
173
- $attempt++
174
- if ($attempt -lt $MaxRetries) {
175
- Start-Sleep -Milliseconds $RetryDelayMs
176
- $RetryDelayMs = $RetryDelayMs * 2 # Exponential backoff
177
- } else {
178
- Write-Log "Failed to write to $Path after $MaxRetries retries: $_" "ERROR"
179
- throw $_
180
- }
181
- }
182
- }
183
- return $false
184
- }
185
-
186
- function Stop-ProcessTree {
187
- param([int]$ProcessId)
188
-
189
- # Use taskkill with /T flag to kill the process tree
190
- # This ensures all child processes are also terminated
191
- try {
192
- $null = taskkill /PID $ProcessId /T /F 2>&1
193
- Write-Log "Terminated process tree for PID $ProcessId" "INFO"
194
- return $true
195
- } catch {
196
- Write-Log "Failed to terminate process tree for PID $ProcessId : $_" "WARN"
197
- return $false
198
- }
199
- }
200
-
201
- function Stop-OrphanClaudeProcesses {
202
- # Find and terminate orphan Claude processes that are not tracked by this scheduler
203
- # SAFETY: Only terminates processes that:
204
- # 1. Are NOT in TrackedProcessIds
205
- # 2. Have a command line containing "--dangerously-skip-permissions" (scheduler-launched agents)
206
- # This prevents accidentally killing user's IDE Claude sessions
207
- param(
208
- [hashtable]$TrackedProcessIds = @{},
209
- [switch]$Force # Force cleanup even without command line check (dangerous!)
210
- )
211
-
212
- $claudeProcesses = Get-Process -Name "claude" -ErrorAction SilentlyContinue
213
-
214
- if (-not $claudeProcesses) {
215
- return 0
216
- }
217
-
218
- $orphanCount = 0
219
- foreach ($proc in $claudeProcesses) {
220
- # Skip if this process is being tracked
221
- if ($TrackedProcessIds.ContainsKey($proc.Id)) {
222
- continue
223
- }
224
-
225
- # Safety check: only kill processes launched by scheduler (have --dangerously-skip-permissions flag)
226
- if (-not $Force) {
227
- try {
228
- $cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($proc.Id)" -ErrorAction SilentlyContinue).CommandLine
229
- if (-not $cmdLine -or $cmdLine -notmatch "--dangerously-skip-permissions") {
230
- # This is likely a user's IDE session, skip it
231
- continue
232
- }
233
- } catch {
234
- # If we can't check command line, don't kill to be safe
235
- continue
236
- }
237
- }
238
-
239
- try {
240
- Write-Log "Found orphan Claude agent process (PID: $($proc.Id), Started: $($proc.StartTime))" "WARN"
241
- Stop-ProcessTree -ProcessId $proc.Id
242
- $orphanCount++
243
- } catch {
244
- Write-Log "Failed to stop orphan process $($proc.Id): $_" "ERROR"
245
- }
246
- }
247
-
248
- return $orphanCount
249
- }
250
-
251
- # ============================================================
252
- # Smart Agent Dispatch Functions
253
- # ============================================================
254
-
255
- function Get-AgentType {
256
- param(
257
- [string]$TaskId,
258
- [string]$TaskTitle,
259
- [string[]]$ScopePaths,
260
- [string]$ExplicitType = $null
261
- )
262
-
263
- # If explicit type is specified, use it
264
- if ($ExplicitType) {
265
- return $ExplicitType
266
- }
267
-
268
- $titleLower = $TaskTitle.ToLower()
269
-
270
- # Keyword matching for agent type (English only for PowerShell compatibility)
271
- $designerKeywords = @('ui', 'ux', 'layout', 'style', 'design', 'css', 'theme', 'color', 'visual', 'icon')
272
- $backendKeywords = @('api', 'database', 'backend', 'endpoint', 'server', 'sql', 'migration', 'supabase', 'postgres')
273
- $qaKeywords = @('test', 'qa', 'quality', 'unit test', 'e2e', 'spec', 'coverage', 'assert')
274
- $sreKeywords = @('deploy', 'ci/cd', 'monitoring', 'infrastructure', 'docker', 'kubernetes', 'devops', 'pipeline')
275
- $frontendKeywords = @('component', 'react', 'frontend', 'tsx', 'jsx', 'hook', 'state', 'page', 'dashboard')
276
-
277
- # Check keywords in title
278
- foreach ($kw in $designerKeywords) {
279
- if ($titleLower.Contains($kw)) { return "designer" }
280
- }
281
- foreach ($kw in $backendKeywords) {
282
- if ($titleLower.Contains($kw)) { return "backend" }
283
- }
284
- foreach ($kw in $qaKeywords) {
285
- if ($titleLower.Contains($kw)) { return "qa" }
286
- }
287
- foreach ($kw in $sreKeywords) {
288
- if ($titleLower.Contains($kw)) { return "sre" }
289
- }
290
- foreach ($kw in $frontendKeywords) {
291
- if ($titleLower.Contains($kw)) { return "developer" }
292
- }
293
-
294
- # Scope path matching
295
- foreach ($path in $ScopePaths) {
296
- $pathLower = $path.ToLower()
297
- if ($pathLower -match "api/" -or $pathLower -match "backend/") {
298
- return "backend"
299
- }
300
- if ($pathLower -match "components/" -or $pathLower -match "src/" -or $pathLower -match "dashboard/") {
301
- return "developer"
302
- }
303
- if ($pathLower -match "tests/" -or $pathLower -match "spec/") {
304
- return "qa"
305
- }
306
- if ($pathLower -match "deploy/" -or $pathLower -match "infra/") {
307
- return "sre"
308
- }
309
- }
310
-
311
- # Default to dev_agent
312
- return "dev"
313
- }
314
-
315
- function Get-SkillPath {
316
- param([string]$AgentType)
317
-
318
- $skillPaths = @{
319
- "developer" = ".claude/skills/developer/SKILL.md"
320
- "backend" = ".claude/skills/backend_engineer/SKILL.md"
321
- "designer" = ".claude/skills/designer/SKILL.md"
322
- "qa" = ".claude/skills/qa_engineer/SKILL.md"
323
- "sre" = ".claude/skills/sre_agent/SKILL.md"
324
- "dev" = ".claude/skills/dev_agent/SKILL.md"
325
- "frontend" = ".claude/skills/frontend_agent/SKILL.md"
326
- "architect" = ".claude/skills/architect_agent/SKILL.md"
327
- }
328
-
329
- if ($skillPaths.ContainsKey($AgentType)) {
330
- return $skillPaths[$AgentType]
331
- }
332
-
333
- # Default to dev_agent
334
- return ".claude/skills/dev_agent/SKILL.md"
335
- }
336
-
337
- function Get-AgentDisplayName {
338
- param([string]$AgentType)
339
-
340
- $displayNames = @{
341
- "developer" = "Frontend Developer"
342
- "backend" = "Backend Engineer"
343
- "designer" = "UI/UX Designer"
344
- "qa" = "QA Engineer"
345
- "sre" = "SRE Agent"
346
- "dev" = "Dev Agent"
347
- "frontend" = "Frontend Agent"
348
- "architect" = "Architect"
349
- }
350
-
351
- if ($displayNames.ContainsKey($AgentType)) {
352
- return $displayNames[$AgentType]
353
- }
354
-
355
- return "Dev Agent"
356
- }
357
-
358
- function Get-TaskQueue {
359
- if (-not (Test-Path $TaskQueueFile)) {
360
- return $null
361
- }
362
-
363
- try {
364
- $content = Get-Content $TaskQueueFile -Raw -Encoding UTF8
365
- return $content | ConvertFrom-Json
366
- } catch {
367
- Write-Log "Failed to read task queue: $_" "ERROR"
368
- return $null
369
- }
370
- }
371
-
372
- function Get-LockState {
373
- if (-not (Test-Path $LockStateFile)) {
374
- return @{ locks = @() }
375
- }
376
-
377
- try {
378
- $content = Get-Content $LockStateFile -Raw -Encoding UTF8
379
- return $content | ConvertFrom-Json
380
- } catch {
381
- return @{ locks = @() }
382
- }
383
- }
384
-
385
- function Save-LockState {
386
- param($State)
387
-
388
- try {
389
- $State | Add-Member -NotePropertyName "last_updated" -NotePropertyValue (Get-Date -Format "o") -Force
390
- $jsonContent = $State | ConvertTo-Json -Depth 10
391
- Write-FileWithRetry -Path $LockStateFile -Content $jsonContent
392
- } catch {
393
- Write-Log "Failed to save lock state: $_" "WARN"
394
- # Don't propagate - allow scheduler to continue
395
- }
396
- }
397
-
398
- function Get-AgentState {
399
- if (-not (Test-Path $AgentStateFile)) {
400
- return @{ agents = @() }
401
- }
402
-
403
- try {
404
- $content = Get-Content $AgentStateFile -Raw -Encoding UTF8
405
- return $content | ConvertFrom-Json
406
- } catch {
407
- return @{ agents = @() }
408
- }
409
- }
410
-
411
- function Save-AgentState {
412
- param($State)
413
-
414
- try {
415
- $State | Add-Member -NotePropertyName "last_updated" -NotePropertyValue (Get-Date -Format "o") -Force
416
- $jsonContent = $State | ConvertTo-Json -Depth 10
417
- Write-FileWithRetry -Path $AgentStateFile -Content $jsonContent
418
- } catch {
419
- Write-Log "Failed to save agent state: $_" "WARN"
420
- # Don't propagate - allow scheduler to continue
421
- }
422
- }
423
-
424
- function Test-DashboardHeartbeat {
425
- # Check if dashboard is still alive via heartbeat file
426
- # Stop agents when:
427
- # 1. Dashboard explicitly sends "closing" signal
428
- # 2. Heartbeat file hasn't been updated for too long (crash detection)
429
- #
430
- # NOTE: In Standalone mode, heartbeat is ignored entirely
431
-
432
- # Standalone mode - always return alive
433
- if ($script:StandaloneMode) {
434
- return @{ Alive = $true; Reason = "Standalone mode (heartbeat ignored)" }
435
- }
436
-
437
- $HeartbeatTimeoutSeconds = 120 # 2 minutes - if no heartbeat update, assume crashed
438
-
439
- if (-not (Test-Path $HeartbeatFile)) {
440
- # No heartbeat file - assume running without dashboard (CLI mode)
441
- return @{ Alive = $true; Reason = "No heartbeat file (CLI mode)" }
442
- }
443
-
444
- try {
445
- $content = Get-Content $HeartbeatFile -Raw -Encoding UTF8
446
- $heartbeat = $content | ConvertFrom-Json
447
-
448
- # Check if dashboard is explicitly closing
449
- if ($heartbeat.status -eq "closing") {
450
- return @{ Alive = $false; Reason = "Dashboard closing" }
451
- }
452
-
453
- # Check heartbeat timeout (crash detection)
454
- if ($heartbeat.timestamp) {
455
- $lastHeartbeat = [DateTime]::Parse($heartbeat.timestamp)
456
- $elapsed = (Get-Date) - $lastHeartbeat
457
- if ($elapsed.TotalSeconds -gt $HeartbeatTimeoutSeconds) {
458
- return @{
459
- Alive = $false
460
- Reason = "Dashboard heartbeat timeout ($([int]$elapsed.TotalSeconds)s > ${HeartbeatTimeoutSeconds}s)"
461
- }
462
- }
463
- }
464
-
465
- return @{ Alive = $true; SessionId = $heartbeat.session_id }
466
- } catch {
467
- # Failed to read/parse - check file modification time as fallback
468
- try {
469
- $fileInfo = Get-Item $HeartbeatFile
470
- $elapsed = (Get-Date) - $fileInfo.LastWriteTime
471
- if ($elapsed.TotalSeconds -gt $HeartbeatTimeoutSeconds) {
472
- return @{
473
- Alive = $false
474
- Reason = "Dashboard heartbeat file stale ($([int]$elapsed.TotalSeconds)s)"
475
- }
476
- }
477
- } catch { }
478
- return @{ Alive = $true; Reason = "Heartbeat check fallback" }
479
- }
480
- }
481
-
482
- function Add-AgentToState {
483
- param(
484
- [string]$AgentId,
485
- [string]$TaskId,
486
- [string]$TaskTitle,
487
- [string]$Branch,
488
- [string]$Status = "working",
489
- [string]$AgentType = "dev_agent"
490
- )
491
-
492
- $agentState = Get-AgentState
493
- $agents = [System.Collections.ArrayList]@()
494
- if ($agentState.agents) {
495
- $agents = [System.Collections.ArrayList]@($agentState.agents)
496
- }
497
-
498
- # Remove existing entry if any
499
- $agents = [System.Collections.ArrayList]@($agents | Where-Object { $_.id -ne $AgentId })
500
-
501
- # Map internal agent type to dashboard-compatible type
502
- $dashboardType = switch ($AgentType) {
503
- "developer" { "developer" }
504
- "backend" { "backend_engineer" }
505
- "designer" { "designer" }
506
- "qa" { "qa_engineer" }
507
- "sre" { "sre_agent" }
508
- "frontend" { "frontend_agent" }
509
- "architect" { "architect_agent" }
510
- default { "dev_agent" }
511
- }
512
-
513
- # Debug: Log type mapping for troubleshooting
514
- Write-Log "Type mapping: AgentType='$AgentType' -> dashboardType='$dashboardType'" "INFO"
515
-
516
- # Add new agent entry (format compatible with dashboard)
517
- $agents.Add(@{
518
- id = $AgentId
519
- type = $dashboardType
520
- status = $Status
521
- current_task = $TaskId
522
- current_operation = $TaskTitle
523
- branch = $Branch
524
- started_at = (Get-Date -Format "o")
525
- task_id = $TaskId
526
- task_title = $TaskTitle
527
- }) | Out-Null
528
-
529
- $agentState.agents = $agents
530
- Save-AgentState $agentState
531
- $displayName = Get-AgentDisplayName -AgentType $AgentType
532
- Write-Log "Added agent $AgentId ($displayName) to AGENT_STATE.json (task: $TaskId)"
533
- }
534
-
535
- function Remove-AgentFromState {
536
- param([string]$AgentId)
537
-
538
- $agentState = Get-AgentState
539
- if ($agentState.agents) {
540
- $newAgents = @($agentState.agents | Where-Object { $_.id -ne $AgentId })
541
- $agentState.agents = $newAgents
542
- Save-AgentState $agentState
543
- Write-Log "Removed agent $AgentId from AGENT_STATE.json"
544
- }
545
- }
546
-
547
- # Clean exit function - ensures AGENT_STATE.json is updated before exit
548
- # PowerShell's `exit` doesn't trigger `finally` blocks, so we need this
549
- function Stop-SchedulerCleanly {
550
- param(
551
- [int]$ExitCode = 0,
552
- [string]$Reason = "Normal exit"
553
- )
554
-
555
- Write-Log "Scheduler stopping: $Reason"
556
-
557
- # Terminate all active agents if any
558
- if ($script:activeAgents -and $script:activeAgents.Count -gt 0) {
559
- foreach ($agentId in @($script:activeAgents.Keys)) {
560
- $agent = $script:activeAgents[$agentId]
561
- if ($agent.Process -and -not $agent.Process.HasExited) {
562
- Write-Log "Terminating agent $agentId (PID: $($agent.Process.Id))"
563
- Stop-ProcessTree -ProcessId $agent.Process.Id
564
- }
565
- Remove-AgentLocks -AgentId $agentId
566
- Remove-AgentFromState -AgentId $agentId
567
- }
568
- $script:activeAgents.Clear()
569
- }
570
-
571
- # Update AGENT_STATE.json to show scheduler is stopped
572
- $agentState = @{
573
- agents = @()
574
- scheduler = @{
575
- status = "stopped"
576
- stopped_at = (Get-Date -Format "o")
577
- }
578
- last_updated = (Get-Date -Format "o")
579
- }
580
- Save-AgentState $agentState
581
- Write-Log "AGENT_STATE.json updated: scheduler stopped"
582
-
583
- exit $ExitCode
584
- }
585
-
586
- function Test-PathConflict {
587
- param(
588
- [string[]]$RequestedPaths,
589
- $LockState
590
- )
591
-
592
- foreach ($reqPath in $RequestedPaths) {
593
- foreach ($lock in $LockState.locks) {
594
- $lockPath = $lock.path
595
-
596
- # 檢查路徑衝突
597
- if ($reqPath.StartsWith($lockPath) -or $lockPath.StartsWith($reqPath) -or $reqPath -eq $lockPath) {
598
- return @{
599
- Conflict = $true
600
- ConflictingPath = $lockPath
601
- HeldBy = $lock.agent_id
602
- Task = $lock.task_id
603
- }
604
- }
605
- }
606
- }
607
-
608
- return @{ Conflict = $false }
609
- }
610
-
611
- function Get-ReadyTasks {
612
- param($Queue)
613
-
614
- $readyTasks = @()
615
- $completedIds = @()
616
-
617
- # 收集已完成任務 ID
618
- foreach ($task in $Queue.tasks) {
619
- if ($task.status -eq "completed") {
620
- $completedIds += $task.id
621
- }
622
- }
623
-
624
- # 找出可執行的任務
625
- foreach ($task in $Queue.tasks) {
626
- if ($task.status -ne "pending") {
627
- continue
628
- }
629
-
630
- # 檢查依賴
631
- $depsOk = $true
632
- if ($task.dependencies) {
633
- foreach ($dep in $task.dependencies) {
634
- if ($dep -notin $completedIds) {
635
- $depsOk = $false
636
- break
637
- }
638
- }
639
- }
640
-
641
- if ($depsOk) {
642
- $readyTasks += $task
643
- }
644
- }
645
-
646
- # 按優先級排序 (P0 > P1 > P2 > P3)
647
- $priorityOrder = @{ "P0" = 0; "P1" = 1; "P2" = 2; "P3" = 3 }
648
- $readyTasks = $readyTasks | Sort-Object { $priorityOrder[$_.priority] }
649
-
650
- return $readyTasks
651
- }
652
-
653
- function New-AgentId {
654
- return "agent_$(Get-Date -Format 'HHmmss')_$([guid]::NewGuid().ToString().Substring(0,4))"
655
- }
656
-
657
- function Start-AgentWorker {
658
- param(
659
- [string]$AgentId,
660
- [string]$TaskId,
661
- [string]$TaskTitle,
662
- [string[]]$ScopePaths,
663
- [string]$AgentType = "dev"
664
- )
665
-
666
- $displayName = Get-AgentDisplayName -AgentType $AgentType
667
- $skillPath = Get-SkillPath -AgentType $AgentType
668
- Write-Log "Starting $displayName ($AgentId) for task $TaskId" "SUCCESS"
669
- Write-Log " Skill: $skillPath" "INFO"
670
-
671
- if ($DryRun) {
672
- Write-Log "[DRY RUN] Would start: claude -p '/agent-worker --task $TaskId --agent $AgentId'" "WARN"
673
- return $null
674
- }
675
-
676
- # Check if task has a resumable branch (from previous interrupted session)
677
- $queue = Get-TaskQueue
678
- $resumeBranch = $null
679
- if ($queue) {
680
- $taskObj = $queue.tasks | Where-Object { $_.id -eq $TaskId } | Select-Object -First 1
681
- if ($taskObj -and $taskObj.resume_branch) {
682
- $resumeBranch = $taskObj.resume_branch
683
- }
684
- }
685
-
686
- # 決定使用的 Branch
687
- $branchName = if ($resumeBranch) {
688
- Write-Log "Resuming task from existing branch: $resumeBranch" "INFO"
689
- $resumeBranch
690
- } else {
691
- "task-$($TaskId.ToLower())-$AgentId"
692
- }
693
-
694
- try {
695
- Push-Location $WorkingDirectory
696
-
697
- if ($resumeBranch) {
698
- # Resume: checkout existing branch
699
- $null = git checkout $resumeBranch 2>$null
700
- if ($LASTEXITCODE -ne 0) {
701
- Write-Log "Failed to checkout resume branch, creating new one" "WARN"
702
- $branchName = "task-$($TaskId.ToLower())-$AgentId"
703
- $null = git checkout main 2>$null
704
- if ($LASTEXITCODE -ne 0) { $null = git checkout master 2>$null }
705
- $null = git checkout -b $branchName 2>$null
706
- }
707
- } else {
708
- # New task: create new branch from main
709
- $currentBranch = git rev-parse --abbrev-ref HEAD 2>$null
710
- if ($currentBranch -ne "main" -and $currentBranch -ne "master") {
711
- $null = git checkout main 2>$null
712
- if ($LASTEXITCODE -ne 0) {
713
- $null = git checkout master 2>$null
714
- }
715
- }
716
- # 創建任務分支
717
- $null = git checkout -b $branchName 2>$null
718
- }
719
-
720
- Pop-Location
721
- } catch {
722
- Write-Log "Failed to setup branch: $_" "ERROR"
723
- Pop-Location
724
- return $null
725
- }
726
-
727
- # 啟動 Claude Agent Worker
728
- $prompt = "/agent-worker --task $TaskId --agent $AgentId --branch $branchName"
729
-
730
- # Create log file for this agent's output
731
- $logDir = Join-Path $WorkingDirectory "dev\pipeline\logs"
732
- if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
733
- $agentLogFile = Join-Path $logDir "agent_${AgentId}.log"
734
-
735
- try {
736
- # Use cmd.exe /c to invoke claude (npm global installs create .cmd scripts on Windows)
737
- # This ensures proper execution regardless of how claude was installed
738
- $process = Start-Process -FilePath "cmd.exe" `
739
- -ArgumentList "/c", "claude", "--dangerously-skip-permissions", "-p", "`"$prompt`"" `
740
- -WorkingDirectory $WorkingDirectory `
741
- -PassThru `
742
- -NoNewWindow `
743
- -RedirectStandardOutput $agentLogFile `
744
- -RedirectStandardError "${agentLogFile}.err"
745
-
746
- Write-Log "Agent $AgentId started (PID: $($process.Id))"
747
- Write-Log "Agent log: $agentLogFile"
748
-
749
- $result = @{
750
- Process = $process
751
- AgentId = $AgentId
752
- TaskId = $TaskId
753
- Branch = $branchName
754
- StartTime = Get-Date
755
- LogFile = $agentLogFile
756
- }
757
- return $result
758
- } catch {
759
- Write-Log "Failed to start agent: $_" "ERROR"
760
- return $null
761
- }
762
- }
763
-
764
- function Update-TaskStatus {
765
- param(
766
- [string]$TaskId,
767
- [string]$Status,
768
- [string]$AgentId = $null,
769
- [string]$Branch = $null
770
- )
771
-
772
- $queue = Get-TaskQueue
773
- if (-not $queue) { return }
774
-
775
- foreach ($task in $queue.tasks) {
776
- if ($task.id -eq $TaskId) {
777
- $task.status = $Status
778
-
779
- if ($Status -eq "in_progress") {
780
- $task | Add-Member -NotePropertyName "started_at" -NotePropertyValue (Get-Date -Format "o") -Force
781
- if ($AgentId) {
782
- $task | Add-Member -NotePropertyName "assigned_agent" -NotePropertyValue $AgentId -Force
783
- }
784
- if ($Branch) {
785
- $task | Add-Member -NotePropertyName "branch" -NotePropertyValue $Branch -Force
786
- }
787
- # Clear resume_branch marker now that agent has started
788
- if ($task.PSObject.Properties["resume_branch"]) {
789
- $task.PSObject.Properties.Remove("resume_branch")
790
- }
791
- } elseif ($Status -eq "completed") {
792
- $task | Add-Member -NotePropertyName "completed_at" -NotePropertyValue (Get-Date -Format "o") -Force
793
- }
794
-
795
- break
796
- }
797
- }
798
-
799
- try {
800
- $queue | Add-Member -NotePropertyName "last_updated" -NotePropertyValue (Get-Date -Format "o") -Force
801
- $jsonContent = $queue | ConvertTo-Json -Depth 10
802
- Write-FileWithRetry -Path $TaskQueueFile -Content $jsonContent
803
- } catch {
804
- Write-Log "Failed to update task queue for ${TaskId}: $_" "WARN"
805
- # Don't propagate - allow scheduler to continue
806
- }
807
- }
808
-
809
- function Move-TaskToCompleted {
810
- param([string]$TaskId)
811
-
812
- $tasksDir = Join-Path $WorkingDirectory "dev\pipeline\tasks"
813
- $completedDir = Join-Path $WorkingDirectory "dev\pipeline\completed"
814
-
815
- # Ensure completed directory exists
816
- if (-not (Test-Path $completedDir)) {
817
- New-Item -ItemType Directory -Path $completedDir -Force | Out-Null
818
- }
819
-
820
- # Find and move task file (support both .md and .yaml formats)
821
- $taskFile = Get-ChildItem -Path $tasksDir -Filter "${TaskId}_*" -ErrorAction SilentlyContinue |
822
- Where-Object { $_.Extension -in @('.md', '.yaml', '.yml') } |
823
- Select-Object -First 1
824
- if ($taskFile) {
825
- $destPath = Join-Path $completedDir $taskFile.Name
826
- Move-Item -Path $taskFile.FullName -Destination $destPath -Force
827
- Write-Log "Moved task file to completed: $($taskFile.Name)" "INFO"
828
- }
829
- }
830
-
831
- # ============================================================
832
- # 任務檔案備份(啟動時執行)
833
- # ============================================================
834
-
835
- function Backup-TaskFiles {
836
- # Backup tasks/ folder before starting scheduler
837
- # This prevents loss of task files if scheduler crashes or misbehaves
838
-
839
- $tasksDir = Join-Path $WorkingDirectory "dev\pipeline\tasks"
840
- $backupDir = Join-Path $WorkingDirectory "dev\pipeline\backups"
841
-
842
- if (-not (Test-Path $tasksDir)) {
843
- Write-Log "Tasks directory not found, skipping backup" "WARN"
844
- return $null
845
- }
846
-
847
- $taskFiles = Get-ChildItem -Path $tasksDir -File -ErrorAction SilentlyContinue
848
- if (-not $taskFiles -or $taskFiles.Count -eq 0) {
849
- Write-Log "No task files to backup" "INFO"
850
- return $null
851
- }
852
-
853
- # Create backup directory with timestamp
854
- $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
855
- $backupPath = Join-Path $backupDir "tasks_$timestamp"
856
-
857
- try {
858
- if (-not (Test-Path $backupDir)) {
859
- New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
860
- }
861
-
862
- # Copy entire tasks folder
863
- Copy-Item -Path $tasksDir -Destination $backupPath -Recurse -Force
864
-
865
- $backupCount = (Get-ChildItem -Path $backupPath -File -ErrorAction SilentlyContinue).Count
866
- Write-Log "Backed up $backupCount task file(s) to: $backupPath" "SUCCESS"
867
-
868
- # Clean up old backups (keep last 5)
869
- $oldBackups = Get-ChildItem -Path $backupDir -Directory -Filter "tasks_*" |
870
- Sort-Object CreationTime -Descending |
871
- Select-Object -Skip 5
872
-
873
- foreach ($old in $oldBackups) {
874
- Remove-Item -Path $old.FullName -Recurse -Force -ErrorAction SilentlyContinue
875
- Write-Log "Removed old backup: $($old.Name)" "INFO"
876
- }
877
-
878
- return $backupPath
879
- } catch {
880
- Write-Log "Failed to backup task files: $_" "ERROR"
881
- return $null
882
- }
883
- }
884
-
885
- # ============================================================
886
- # 改進的 Exit Code 偵測
887
- # ============================================================
888
-
889
- function Test-AgentSuccess {
890
- param(
891
- [int]$ExitCode,
892
- [string]$LogFile,
893
- [string]$Branch,
894
- [string]$TaskId
895
- )
896
-
897
- # Priority 1: Exit code 0 is always success
898
- if ($ExitCode -eq 0) {
899
- return @{ Success = $true; Reason = "Exit code 0" }
900
- }
901
-
902
- # Priority 2: Check for commits in branch (actual work done)
903
- if ($Branch) {
904
- try {
905
- Push-Location $WorkingDirectory
906
-
907
- # Check if branch exists
908
- $null = git rev-parse --verify $Branch 2>$null
909
- if ($LASTEXITCODE -eq 0) {
910
- # Determine main branch
911
- $mainBranch = "main"
912
- $null = git rev-parse --verify main 2>$null
913
- if ($LASTEXITCODE -ne 0) { $mainBranch = "master" }
914
-
915
- # Count commits ahead of main
916
- $commitsAhead = git rev-list --count "${mainBranch}..${Branch}" 2>$null
917
- if ($LASTEXITCODE -eq 0 -and [int]$commitsAhead -gt 0) {
918
- # Check if any commit message contains the task ID
919
- $commitMessages = git log --oneline "${mainBranch}..${Branch}" 2>$null
920
- if ($commitMessages -match $TaskId) {
921
- Pop-Location
922
- return @{
923
- Success = $true
924
- Reason = "Branch has $commitsAhead commit(s) with $TaskId"
925
- }
926
- }
927
- }
928
- }
929
-
930
- Pop-Location
931
- } catch {
932
- Pop-Location
933
- }
934
- }
935
-
936
- # Priority 3: Check log file for success indicators
937
- if ($LogFile -and (Test-Path $LogFile)) {
938
- try {
939
- $logContent = Get-Content $LogFile -Raw -ErrorAction SilentlyContinue
940
- if ($logContent) {
941
- # Multiple success patterns
942
- $successPatterns = @(
943
- "Status:\s*SUCCESS",
944
- "TASK_\d+\s+is\s+now\s+complete",
945
- "Task.*completed\s+successfully",
946
- "^\s*##\s+TASK_\d+\s+Summary",
947
- "\[SUCCESS\].*completed",
948
- "All\s+\d+\s+tests\s+pass"
949
- )
950
-
951
- foreach ($pattern in $successPatterns) {
952
- if ($logContent -match $pattern) {
953
- return @{
954
- Success = $true
955
- Reason = "Log matches success pattern: $pattern"
956
- }
957
- }
958
- }
959
-
960
- # Check for failure patterns (explicit failures)
961
- $failurePatterns = @(
962
- "API Error:\s*5\d\d",
963
- "FATAL\s+ERROR",
964
- "Build\s+failed",
965
- "Compilation\s+error"
966
- )
967
-
968
- foreach ($pattern in $failurePatterns) {
969
- if ($logContent -match $pattern) {
970
- return @{
971
- Success = $false
972
- Reason = "Log matches failure pattern: $pattern"
973
- }
974
- }
975
- }
976
- }
977
- } catch {
978
- # Ignore log read errors
979
- }
980
- }
981
-
982
- # Default: If exit code is null/unknown and no definitive indicators, check branch again
983
- if ($null -eq $ExitCode -or $ExitCode -eq "") {
984
- # One more check: if branch exists with commits, consider it partial success
985
- if ($Branch) {
986
- try {
987
- Push-Location $WorkingDirectory
988
- $null = git rev-parse --verify $Branch 2>$null
989
- if ($LASTEXITCODE -eq 0) {
990
- $commitsAhead = git rev-list --count "main..${Branch}" 2>$null
991
- if ($LASTEXITCODE -ne 0) {
992
- $commitsAhead = git rev-list --count "master..${Branch}" 2>$null
993
- }
994
- if ($LASTEXITCODE -eq 0 -and [int]$commitsAhead -gt 0) {
995
- Pop-Location
996
- return @{
997
- Success = $true
998
- Reason = "Exit null but branch has $commitsAhead commit(s) - assuming success"
999
- }
1000
- }
1001
- }
1002
- Pop-Location
1003
- } catch {
1004
- Pop-Location
1005
- }
1006
- }
1007
-
1008
- return @{
1009
- Success = $false
1010
- Reason = "Exit code null with no success indicators"
1011
- }
1012
- }
1013
-
1014
- return @{
1015
- Success = $false
1016
- Reason = "Exit code $ExitCode (non-zero)"
1017
- }
1018
- }
1019
-
1020
- function Update-ProgressFile {
1021
- param(
1022
- [string]$TaskId,
1023
- [string]$TaskTitle,
1024
- [string]$Action
1025
- )
1026
-
1027
- $progressFile = Join-Path $WorkingDirectory "dev\pipeline\PROGRESS.md"
1028
- if (-not (Test-Path $progressFile)) { return }
1029
-
1030
- $content = Get-Content $progressFile -Raw -Encoding UTF8
1031
- $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm"
1032
-
1033
- # Update Last Update section
1034
- $content = $content -replace '(?ms)(## Last Update\r?\n-.*?Time.*?:)[^\r\n]*', "`$1 $timestamp"
1035
- $content = $content -replace '(?ms)(## Last Update\r?\n.*?Agent.*?:)[^\r\n]*', "`$1 Multi-Agent Scheduler"
1036
- $content = $content -replace '(?ms)(## Last Update\r?\n.*?Session.*?:)[^\r\n]*', "`$1 $TaskId $Action"
1037
-
1038
- # Update statistics based on current queue
1039
- $queue = Get-TaskQueue
1040
- if ($queue) {
1041
- $total = $queue.tasks.Count
1042
- $completed = @($queue.tasks | Where-Object { $_.status -eq "completed" }).Count
1043
- $inProgress = @($queue.tasks | Where-Object { $_.status -eq "in_progress" }).Count
1044
- $pending = @($queue.tasks | Where-Object { $_.status -eq "pending" }).Count
1045
- $blocked = @($queue.tasks | Where-Object { $_.status -eq "blocked" }).Count
1046
-
1047
- $completedPct = if ($total -gt 0) { [math]::Round($completed / $total * 100) } else { 0 }
1048
- $inProgressPct = if ($total -gt 0) { [math]::Round($inProgress / $total * 100) } else { 0 }
1049
- $pendingPct = if ($total -gt 0) { [math]::Round($pending / $total * 100) } else { 0 }
1050
- $blockedPct = if ($total -gt 0) { [math]::Round($blocked / $total * 100) } else { 0 }
1051
-
1052
- $statsBlock = @"
1053
- Total Tasks: $total
1054
- Completed: $completed ($completedPct%)
1055
- In Progress: $inProgress ($inProgressPct%)
1056
- Pending: $pending ($pendingPct%)
1057
- Blocked: $blocked ($blockedPct%)
1058
- "@
1059
- $content = $content -replace '(?ms)(### Overall Progress\r?\n```\r?\n).*?(```)', "`$1$statsBlock`n`$2"
1060
- }
1061
-
1062
- # Add to change history
1063
- $dateStr = Get-Date -Format "yyyy-MM-dd"
1064
- $historyEntry = "| $dateStr | $Action | $TaskId - $TaskTitle |"
1065
- $content = $content -replace '(\| 2026-01-29 \| Tasks Created \|[^\r\n]*)', "`$1`n$historyEntry"
1066
-
1067
- Set-Content $progressFile -Value $content -Encoding UTF8
1068
- Write-Log "Updated PROGRESS.md" "INFO"
1069
- }
1070
-
1071
- function Add-PathLocks {
1072
- param(
1073
- [string]$AgentId,
1074
- [string]$TaskId,
1075
- [string[]]$Paths
1076
- )
1077
-
1078
- $lockState = Get-LockState
1079
- $locks = [System.Collections.ArrayList]@($lockState.locks)
1080
-
1081
- foreach ($path in $Paths) {
1082
- $locks.Add(@{
1083
- path = $path
1084
- agent_id = $AgentId
1085
- task_id = $TaskId
1086
- lock_type = if ($path.EndsWith("/")) { "directory" } else { "file" }
1087
- acquired_at = (Get-Date -Format "o")
1088
- }) | Out-Null
1089
- }
1090
-
1091
- $lockState.locks = $locks
1092
- Save-LockState $lockState
1093
-
1094
- Write-Log "Acquired locks for $AgentId on $($Paths.Count) paths"
1095
- }
1096
-
1097
- function Remove-AgentLocks {
1098
- param([string]$AgentId)
1099
-
1100
- $lockState = Get-LockState
1101
- $newLocks = $lockState.locks | Where-Object { $_.agent_id -ne $AgentId }
1102
- $lockState.locks = @($newLocks)
1103
- Save-LockState $lockState
1104
-
1105
- Write-Log "Released locks for $AgentId"
1106
- }
1107
-
1108
- # ============================================================
1109
- # 狀態一致性檢查與清理(啟動時執行)
1110
- # ============================================================
1111
-
1112
- function Clear-StaleState {
1113
- # CRITICAL: Clear all stale state from previous sessions at startup
1114
- # This function runs BEFORE consistency check to ensure clean slate
1115
- #
1116
- # Why: When scheduler restarts, any existing locks/agents from previous
1117
- # crashed sessions are invalid and must be cleared immediately
1118
-
1119
- Write-Log "Cleaning stale state from previous sessions..." "INFO"
1120
- $cleaned = @()
1121
-
1122
- # 1. Clear ALL locks - new scheduler session means fresh state
1123
- try {
1124
- $lockState = Get-LockState
1125
- if ($lockState.locks -and $lockState.locks.Count -gt 0) {
1126
- $lockCount = $lockState.locks.Count
1127
- $lockDetails = @($lockState.locks | ForEach-Object { "$($_.task_id):$($_.path)" })
1128
- Write-Log "Found $lockCount stale lock(s) from previous session:" "WARN"
1129
- foreach ($detail in $lockDetails) {
1130
- Write-Log " - $detail" "WARN"
1131
- }
1132
-
1133
- # Clear all locks
1134
- Save-LockState @{ locks = @() }
1135
- $cleaned += "Cleared $lockCount stale lock(s)"
1136
- }
1137
- } catch {
1138
- Write-Log "Error clearing locks: $_" "ERROR"
1139
- # Force reset
1140
- try {
1141
- Save-LockState @{ locks = @() }
1142
- $cleaned += "Force reset LOCK_STATE.json"
1143
- } catch { }
1144
- }
1145
-
1146
- # 2. Clear stale agents from AGENT_STATE.json
1147
- try {
1148
- $agentState = Get-AgentState
1149
- if ($agentState.agents -and $agentState.agents.Count -gt 0) {
1150
- $agentCount = $agentState.agents.Count
1151
- $agentDetails = @($agentState.agents | ForEach-Object { "$($_.id):$($_.current_task)" })
1152
- Write-Log "Found $agentCount stale agent(s) from previous session:" "WARN"
1153
- foreach ($detail in $agentDetails) {
1154
- Write-Log " - $detail" "WARN"
1155
- }
1156
-
1157
- # Clear agents but preserve scheduler info
1158
- $agentState.agents = @()
1159
- Save-AgentState $agentState
1160
- $cleaned += "Cleared $agentCount stale agent(s)"
1161
- }
1162
- } catch {
1163
- Write-Log "Error clearing agents: $_" "ERROR"
1164
- }
1165
-
1166
- # 3. Reset in_progress tasks to pending (check branch progress first)
1167
- try {
1168
- $queue = Get-TaskQueue
1169
- if ($queue -and $queue.tasks) {
1170
- $inProgressTasks = @($queue.tasks | Where-Object { $_.status -eq "in_progress" })
1171
- if ($inProgressTasks.Count -gt 0) {
1172
- Write-Log "Found $($inProgressTasks.Count) in_progress task(s) to check:" "WARN"
1173
- $resetCount = 0
1174
- $resumeCount = 0
1175
-
1176
- foreach ($task in $queue.tasks) {
1177
- if ($task.status -eq "in_progress") {
1178
- $hasProgress = $false
1179
- $branchName = $task.branch
1180
-
1181
- # Check if branch exists and has commits ahead of main
1182
- if ($branchName) {
1183
- try {
1184
- Push-Location $WorkingDirectory
1185
-
1186
- # Check if branch exists
1187
- $null = git rev-parse --verify $branchName 2>$null
1188
- if ($LASTEXITCODE -eq 0) {
1189
- # Check for commits ahead of main/master
1190
- $mainBranch = "main"
1191
- $null = git rev-parse --verify main 2>$null
1192
- if ($LASTEXITCODE -ne 0) { $mainBranch = "master" }
1193
-
1194
- $commitsAhead = git rev-list --count "${mainBranch}..${branchName}" 2>$null
1195
- if ($LASTEXITCODE -eq 0 -and [int]$commitsAhead -gt 0) {
1196
- $hasProgress = $true
1197
- Write-Log " - $($task.id): $($task.title) - branch has $commitsAhead commit(s), will RESUME" "SUCCESS"
1198
- }
1199
- }
1200
-
1201
- Pop-Location
1202
- } catch {
1203
- Pop-Location
1204
- }
1205
- }
1206
-
1207
- # Reset task to pending
1208
- $task.status = "pending"
1209
-
1210
- # Clear stale assignment info
1211
- if ($task.PSObject.Properties["assigned_agent"]) {
1212
- $task.PSObject.Properties.Remove("assigned_agent")
1213
- }
1214
- if ($task.PSObject.Properties["started_at"]) {
1215
- $task.PSObject.Properties.Remove("started_at")
1216
- }
1217
-
1218
- if ($hasProgress) {
1219
- # Keep branch for resume
1220
- $task | Add-Member -NotePropertyName "resume_branch" -NotePropertyValue $branchName -Force
1221
- if ($task.PSObject.Properties["branch"]) {
1222
- $task.PSObject.Properties.Remove("branch")
1223
- }
1224
- $resumeCount++
1225
- } else {
1226
- # Full reset - no progress to preserve
1227
- if (-not $hasProgress) {
1228
- Write-Log " - $($task.id): $($task.title) - no progress, will RESET" "WARN"
1229
- }
1230
- if ($task.PSObject.Properties["branch"]) {
1231
- $task.PSObject.Properties.Remove("branch")
1232
- }
1233
- if ($task.PSObject.Properties["resume_branch"]) {
1234
- $task.PSObject.Properties.Remove("resume_branch")
1235
- }
1236
- $resetCount++
1237
- }
1238
- }
1239
- }
1240
-
1241
- # Save updated queue
1242
- $queue.last_updated = (Get-Date -Format "o")
1243
- $jsonContent = $queue | ConvertTo-Json -Depth 10
1244
- Write-FileWithRetry -Path $TaskQueueFile -Content $jsonContent
1245
-
1246
- if ($resetCount -gt 0) {
1247
- $cleaned += "Reset $resetCount task(s) with no progress"
1248
- }
1249
- if ($resumeCount -gt 0) {
1250
- $cleaned += "Will resume $resumeCount task(s) with branch progress"
1251
- }
1252
- }
1253
- }
1254
- } catch {
1255
- Write-Log "Error resetting tasks: $_" "ERROR"
1256
- }
1257
-
1258
- # Report
1259
- if ($cleaned.Count -gt 0) {
1260
- Write-Log "Stale state cleanup completed:" "SUCCESS"
1261
- foreach ($item in $cleaned) {
1262
- Write-Log " - $item" "SUCCESS"
1263
- }
1264
- } else {
1265
- Write-Log "No stale state found - clean startup" "SUCCESS"
1266
- }
1267
-
1268
- return $cleaned
1269
- }
1270
-
1271
- function Test-StateConsistency {
1272
- # Comprehensive state consistency check at scheduler startup
1273
- # This prevents issues from previous crashed sessions
1274
- # NOTE: Clear-StaleState should be called BEFORE this function
1275
-
1276
- $issues = @()
1277
- $fixed = @()
1278
-
1279
- # 1. Validate TASK_QUEUE.json
1280
- Write-Log "Checking state consistency..." "INFO"
1281
-
1282
- try {
1283
- $queue = Get-TaskQueue
1284
- if (-not $queue) {
1285
- $issues += "TASK_QUEUE.json is missing or invalid"
1286
- # Will be recreated by sync script
1287
- }
1288
- } catch {
1289
- $issues += "TASK_QUEUE.json parse error: $_"
1290
- }
1291
-
1292
- # 2. Validate LOCK_STATE.json (should be empty after Clear-StaleState)
1293
- try {
1294
- $lockState = Get-LockState
1295
- if ($lockState.locks -and $lockState.locks.Count -gt 0) {
1296
- # This shouldn't happen after Clear-StaleState, but handle it
1297
- $issues += "Unexpected locks remain after cleanup: $($lockState.locks.Count)"
1298
- Save-LockState @{ locks = @() }
1299
- $fixed += "Force cleared remaining locks"
1300
- }
1301
- } catch {
1302
- $issues += "LOCK_STATE.json error: $_"
1303
- try {
1304
- Save-LockState @{ locks = @() }
1305
- $fixed += "Reset LOCK_STATE.json"
1306
- } catch { }
1307
- }
1308
-
1309
- # 3. Validate AGENT_STATE.json (should have no agents after Clear-StaleState)
1310
- try {
1311
- $agentState = Get-AgentState
1312
- if ($agentState.agents -and $agentState.agents.Count -gt 0) {
1313
- $issues += "Unexpected agents remain after cleanup: $($agentState.agents.Count)"
1314
- }
1315
- } catch {
1316
- $issues += "AGENT_STATE.json error: $_"
1317
- }
1318
-
1319
- # 4. Check for tasks stuck in in_progress (should be none after Clear-StaleState)
1320
- if ($queue -and $queue.tasks) {
1321
- $stuckTasks = @($queue.tasks | Where-Object { $_.status -eq "in_progress" })
1322
- if ($stuckTasks.Count -gt 0) {
1323
- $issues += "Unexpected in_progress tasks after cleanup: $($stuckTasks.Count)"
1324
- }
1325
- }
1326
-
1327
- # Report
1328
- if ($issues.Count -gt 0) {
1329
- Write-Log "State consistency issues found:" "WARN"
1330
- foreach ($issue in $issues) {
1331
- Write-Log " - $issue" "WARN"
1332
- }
1333
- }
1334
-
1335
- if ($fixed.Count -gt 0) {
1336
- Write-Log "Auto-fixed issues:" "SUCCESS"
1337
- foreach ($fix in $fixed) {
1338
- Write-Log " - $fix" "SUCCESS"
1339
- }
1340
- }
1341
-
1342
- if ($issues.Count -eq 0 -and $fixed.Count -eq 0) {
1343
- Write-Log "State consistency check passed" "SUCCESS"
1344
- }
1345
-
1346
- return @{
1347
- Issues = $issues
1348
- Fixed = $fixed
1349
- HasCriticalIssues = ($issues | Where-Object { $_ -match "parse error|invalid" }).Count -gt 0
1350
- }
1351
- }
1352
-
1353
- # ============================================================
1354
- # 主循環
1355
- # ============================================================
1356
-
1357
- Clear-Host
1358
- Write-Host ""
1359
- Write-Host "============================================================" -ForegroundColor Cyan
1360
- Write-Host " Multi-Agent Parallel Scheduler" -ForegroundColor Cyan
1361
- Write-Host "============================================================" -ForegroundColor Cyan
1362
- Write-Host " Max Parallel Agents: $MaxAgents"
1363
- Write-Host " Poll Interval: ${PollIntervalSeconds}s"
1364
- Write-Host " Task Timeout: ${TaskTimeoutMinutes}min"
1365
- Write-Host " Working Dir: $WorkingDirectory"
1366
- if ($DryRun) { Write-Host " Mode: DRY RUN" -ForegroundColor Yellow }
1367
- if ($Standalone) { Write-Host " Mode: STANDALONE (heartbeat ignored)" -ForegroundColor Magenta }
1368
- Write-Host "============================================================" -ForegroundColor Cyan
1369
- Write-Host ""
1370
-
1371
- # ============================================================
1372
- # Prerequisite Checks
1373
- # ============================================================
1374
- Write-Host "[CHECK] Running prerequisite checks..." -ForegroundColor Yellow
1375
- Write-DebugLog "Starting prerequisite checks"
1376
-
1377
- # Check 1: Claude CLI installed
1378
- try {
1379
- $claudeCmd = Get-Command "claude" -ErrorAction SilentlyContinue
1380
- $claudeInstalled = $null -ne $claudeCmd
1381
- if ($claudeCmd) {
1382
- Write-DebugLog "Claude CLI found at: $($claudeCmd.Source)"
1383
- }
1384
- } catch {
1385
- $claudeInstalled = $false
1386
- Write-DebugLog "Error checking Claude CLI: $_" "ERROR"
1387
- }
1388
-
1389
- if (-not $claudeInstalled) {
1390
- $errorMsg = "Claude CLI not found in PATH"
1391
- Write-DebugLog $errorMsg "ERROR"
1392
- Write-Host ""
1393
- Write-Host "============================================================" -ForegroundColor Red
1394
- Write-Host " ERROR: Claude CLI not found!" -ForegroundColor Red
1395
- Write-Host "============================================================" -ForegroundColor Red
1396
- Write-Host ""
1397
- Write-Host " Multi-Agent mode requires Claude CLI to be installed."
1398
- Write-Host ""
1399
- Write-Host " To install:" -ForegroundColor White
1400
- Write-Host " npm install -g @anthropic-ai/claude-code" -ForegroundColor Cyan
1401
- Write-Host ""
1402
- Write-Host " After installation, restart your terminal and try again."
1403
- Write-Host ""
1404
- Write-Host "============================================================" -ForegroundColor Red
1405
- Write-DebugLog "Exiting with code 1 - Claude CLI not found"
1406
- exit 1
1407
- }
1408
- Write-Host " [OK] Claude CLI found" -ForegroundColor Green
1409
- Write-DebugLog "Claude CLI check passed"
1410
-
1411
- # Check 2: Working directory exists
1412
- if (-not (Test-Path $WorkingDirectory)) {
1413
- $errorMsg = "Working directory not found: $WorkingDirectory"
1414
- Write-DebugLog $errorMsg "ERROR"
1415
- Write-DebugLog "Exiting with code 1 - working directory not found"
1416
- Write-Host " [ERROR] $errorMsg" -ForegroundColor Red
1417
- exit 1
1418
- }
1419
- Write-Host " [OK] Working directory exists" -ForegroundColor Green
1420
- Write-DebugLog "Working directory check passed: $WorkingDirectory"
1421
-
1422
- # Check 3: dev/pipeline directory exists
1423
- $pipelineDir = Join-Path $WorkingDirectory "dev\pipeline"
1424
- if (-not (Test-Path $pipelineDir)) {
1425
- $errorMsg = "Pipeline directory not found: $pipelineDir - Run Initialize Project first"
1426
- Write-DebugLog $errorMsg "ERROR"
1427
- Write-DebugLog "Exiting with code 1 - pipeline directory not found"
1428
- Write-Host " [ERROR] $errorMsg" -ForegroundColor Red
1429
- exit 1
1430
- }
1431
- Write-Host " [OK] Pipeline directory exists" -ForegroundColor Green
1432
- Write-DebugLog "Pipeline directory check passed"
1433
-
1434
- # Check 4: Task queue file exists (will be created by sync if needed)
1435
- $TaskQueueFile = Join-Path $WorkingDirectory "dev\pipeline\TASK_QUEUE.json"
1436
- if (-not (Test-Path $TaskQueueFile)) {
1437
- Write-Host " [WARN] Task queue not found, will be created by sync" -ForegroundColor Yellow
1438
- Write-DebugLog "Task queue file not found, will attempt sync" "WARN"
1439
- }
1440
-
1441
- Write-Host "[CHECK] Prerequisites passed" -ForegroundColor Green
1442
- Write-DebugLog "All prerequisite checks passed"
1443
- Write-Host ""
1444
-
1445
- # Auto-sync task queue from markdown files
1446
- Write-Host "[SYNC] Syncing task queue from markdown files..." -ForegroundColor Yellow
1447
- Write-DebugLog "Starting task queue sync"
1448
-
1449
- # Look for sync script in the same directory as this script (not in user's project)
1450
- $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
1451
- Write-DebugLog "Script directory (raw): $scriptDir"
1452
-
1453
- # Normalize path to handle \\?\ prefix from Tauri
1454
- if ($scriptDir -and $scriptDir.StartsWith("\\?\")) {
1455
- $scriptDir = $scriptDir.Substring(4)
1456
- Write-DebugLog "Script directory (normalized): $scriptDir"
1457
- }
1458
-
1459
- $syncScript = Join-Path $scriptDir "sync_task_queue.ps1"
1460
- Write-DebugLog "Looking for sync script at: $syncScript"
1461
-
1462
- if (Test-Path $syncScript) {
1463
- Write-DebugLog "Found sync script, executing..."
1464
- try {
1465
- & $syncScript -WorkingDirectory $WorkingDirectory -Quiet | Out-Null
1466
- Write-Host "[SYNC] Task queue synced successfully" -ForegroundColor Green
1467
- Write-DebugLog "Task queue sync completed successfully"
1468
- } catch {
1469
- Write-DebugLog "Sync script execution failed: $_" "ERROR"
1470
- Write-Host "[SYNC] Sync failed: $_" -ForegroundColor Red
1471
- }
1472
- } else {
1473
- Write-DebugLog "Sync script not found at primary location, trying fallback..."
1474
- # Fallback: check in user's working directory (for manual runs)
1475
- $syncScriptFallback = Join-Path $WorkingDirectory "dev\scripts\sync_task_queue.ps1"
1476
- Write-DebugLog "Fallback sync script path: $syncScriptFallback"
1477
-
1478
- if (Test-Path $syncScriptFallback) {
1479
- Write-DebugLog "Found fallback sync script, executing..."
1480
- try {
1481
- & $syncScriptFallback -WorkingDirectory $WorkingDirectory -Quiet | Out-Null
1482
- Write-Host "[SYNC] Task queue synced successfully" -ForegroundColor Green
1483
- Write-DebugLog "Task queue sync completed successfully (fallback)"
1484
- } catch {
1485
- Write-DebugLog "Fallback sync script execution failed: $_" "ERROR"
1486
- Write-Host "[SYNC] Sync failed: $_" -ForegroundColor Red
1487
- }
1488
- } else {
1489
- Write-DebugLog "No sync script found at either location" "WARN"
1490
- Write-Host "[SYNC] sync_task_queue.ps1 not found, skipping auto-sync" -ForegroundColor Yellow
1491
- }
1492
- }
1493
- Write-Host ""
1494
-
1495
- Write-Log "Multi-Agent Scheduler started (MaxAgents=$MaxAgents)"
1496
-
1497
- # Backup task files before starting (prevent data loss)
1498
- $backupResult = Backup-TaskFiles
1499
- if ($backupResult) {
1500
- Write-DebugLog "Task files backed up to: $backupResult"
1501
- }
1502
-
1503
- # CRITICAL: Clean stale state from previous sessions FIRST
1504
- # This ensures locks, agents, and tasks are reset before any checks
1505
- $null = Clear-StaleState
1506
-
1507
- # Run state consistency check (should find no issues after cleanup)
1508
- $consistencyResult = Test-StateConsistency
1509
- if ($consistencyResult.HasCriticalIssues) {
1510
- Write-Log "Critical state issues detected. Running sync to repair..." "WARN"
1511
- if (Test-Path $syncScript) {
1512
- & $syncScript -WorkingDirectory $WorkingDirectory -Force -Quiet | Out-Null
1513
- Write-Log "Forced sync completed" "SUCCESS"
1514
- }
1515
- }
1516
-
1517
- # NOTE: Removed orphan Claude process cleanup
1518
- # The previous logic was dangerous - it would terminate ALL Claude processes
1519
- # including user's active VSCode Claude extension sessions.
1520
- # If cleanup is needed, user should manually terminate processes before starting scheduler.
1521
- Write-Log "Scheduler ready (stale state cleaned)"
1522
-
1523
- # Check if there are any pending tasks before starting the main loop
1524
- $queue = Get-TaskQueue
1525
- if ($queue) {
1526
- $pendingTasks = @($queue.tasks | Where-Object { $_.status -eq "pending" })
1527
- $completedTasks = @($queue.tasks | Where-Object { $_.status -eq "completed" })
1528
- $totalTasks = $queue.tasks.Count
1529
-
1530
- if ($pendingTasks.Count -eq 0) {
1531
- Write-Host ""
1532
- Write-Host "============================================================" -ForegroundColor Yellow
1533
- Write-Host " No pending tasks found!" -ForegroundColor Yellow
1534
- Write-Host "============================================================" -ForegroundColor Yellow
1535
- Write-Host ""
1536
- Write-Host " Task Queue Status:"
1537
- Write-Host " Total: $totalTasks"
1538
- Write-Host " Completed: $($completedTasks.Count)"
1539
- Write-Host " Pending: 0"
1540
- Write-Host ""
1541
- Write-Host " To add tasks, you can:" -ForegroundColor White
1542
- Write-Host " 1. Run /product to generate PRD and tasks" -ForegroundColor Cyan
1543
- Write-Host " 2. Add task files to dev/pipeline/tasks/" -ForegroundColor Cyan
1544
- Write-Host " 3. Use /dev to start development manually" -ForegroundColor Cyan
1545
- Write-Host ""
1546
- Write-Host "============================================================" -ForegroundColor Yellow
1547
-
1548
- if ($SingleRun) {
1549
- Stop-SchedulerCleanly -ExitCode 0 -Reason "No pending tasks (SingleRun mode)"
1550
- }
1551
-
1552
- Write-Log "No pending tasks. Scheduler will wait for new tasks..."
1553
- } else {
1554
- Write-Log "Found $($pendingTasks.Count) pending task(s) out of $totalTasks total" "SUCCESS"
1555
- }
1556
- } else {
1557
- Write-Host ""
1558
- Write-Host "============================================================" -ForegroundColor Red
1559
- Write-Host " ERROR: Task queue not found or invalid!" -ForegroundColor Red
1560
- Write-Host "============================================================" -ForegroundColor Red
1561
- Write-Host ""
1562
- Write-Host " Expected file: $TaskQueueFile"
1563
- Write-Host ""
1564
- Write-Host " Run Initialize Project first to create the task queue."
1565
- Write-Host ""
1566
- Write-DebugLog "Task queue not found or invalid: $TaskQueueFile" "ERROR"
1567
- Write-DebugLog "Exiting with code 1 - task queue missing"
1568
- exit 1
1569
- }
1570
-
1571
- # Clear stale agents and set scheduler status
1572
- $schedulerState = @{
1573
- agents = @()
1574
- scheduler = @{
1575
- status = "running"
1576
- max_agents = $MaxAgents
1577
- started_at = (Get-Date -Format "o")
1578
- }
1579
- last_updated = (Get-Date -Format "o")
1580
- }
1581
- Save-AgentState $schedulerState
1582
- Write-Log "Scheduler status set to 'running' in AGENT_STATE.json"
1583
-
1584
- # 活躍 Agent 追蹤
1585
- $activeAgents = @{}
1586
-
1587
- # 同步計數器(每 5 次輪詢同步一次新任務)
1588
- $syncCounter = 0
1589
- $SyncIntervalCycles = 5
1590
-
1591
- # 保存狀態行位置(用於同一行更新,僅在交互式控制台)
1592
- $statusLineRow = 0
1593
- if ($script:IsInteractiveConsole) {
1594
- $statusLineRow = [Console]::CursorTop
1595
- }
1596
-
1597
- Write-DebugLog "Entering main scheduler loop"
1598
-
1599
- try {
1600
- while ($true) {
1601
- # Check if scheduler was stopped via API (AGENT_STATE.json status)
1602
- $agentState = Get-AgentState
1603
- if ($agentState.scheduler.status -eq "stopped") {
1604
- Write-Log "Scheduler stop requested via API - shutting down..." "WARN"
1605
-
1606
- # Terminate all active agents
1607
- foreach ($agentId in $activeAgents.Keys) {
1608
- $agent = $activeAgents[$agentId]
1609
- if (-not $agent.Process.HasExited) {
1610
- Write-Log "Terminating agent $agentId (stop requested, PID: $($agent.Process.Id))"
1611
- Stop-ProcessTree -ProcessId $agent.Process.Id
1612
- }
1613
- Remove-AgentLocks -AgentId $agentId
1614
- Remove-AgentFromState -AgentId $agentId
1615
- }
1616
- $activeAgents.Clear()
1617
-
1618
- Write-Log "All agents terminated. Scheduler exiting." "SUCCESS"
1619
- Stop-SchedulerCleanly -ExitCode 0 -Reason "Scheduler stop requested via API"
1620
- }
1621
-
1622
- # Check dashboard heartbeat - stop all agents if dashboard is closed
1623
- $heartbeatStatus = Test-DashboardHeartbeat
1624
- if (-not $heartbeatStatus.Alive) {
1625
- Write-Log "Dashboard disconnected: $($heartbeatStatus.Reason)" "WARN"
1626
- Write-Log "Stopping all agents due to dashboard disconnection..." "WARN"
1627
-
1628
- # Terminate all active agents
1629
- foreach ($agentId in $activeAgents.Keys) {
1630
- $agent = $activeAgents[$agentId]
1631
- if (-not $agent.Process.HasExited) {
1632
- Write-Log "Terminating agent $agentId (dashboard closed, PID: $($agent.Process.Id))"
1633
- # Use process tree termination to ensure all child processes are killed
1634
- Stop-ProcessTree -ProcessId $agent.Process.Id
1635
- }
1636
- Remove-AgentLocks -AgentId $agentId
1637
- Remove-AgentFromState -AgentId $agentId
1638
-
1639
- # Reset task to pending for retry
1640
- Update-TaskStatus -TaskId $agent.TaskId -Status "pending"
1641
- }
1642
- $activeAgents.Clear()
1643
-
1644
- # If dashboard explicitly closed, exit immediately
1645
- if ($heartbeatStatus.Reason -match "closing") {
1646
- Write-Log "Dashboard explicitly closed. Scheduler exiting." "SUCCESS"
1647
- Stop-SchedulerCleanly -ExitCode 0 -Reason "Dashboard explicitly closed"
1648
- }
1649
-
1650
- # Wait for dashboard to reconnect (max 60 seconds)
1651
- $reconnectTimeout = 60
1652
- $waited = 0
1653
- Write-Log "Waiting up to ${reconnectTimeout}s for dashboard to reconnect..." "INFO"
1654
- while (-not (Test-DashboardHeartbeat).Alive -and $waited -lt $reconnectTimeout) {
1655
- Start-Sleep -Seconds 5
1656
- $waited += 5
1657
- Write-Host "." -NoNewline
1658
- }
1659
- Write-Host ""
1660
-
1661
- if (-not (Test-DashboardHeartbeat).Alive) {
1662
- Write-Log "Dashboard did not reconnect within ${reconnectTimeout}s. Scheduler exiting." "WARN"
1663
- Stop-SchedulerCleanly -ExitCode 0 -Reason "Dashboard did not reconnect within timeout"
1664
- }
1665
-
1666
- Write-Log "Dashboard reconnected! Resuming scheduler..." "SUCCESS"
1667
- }
1668
-
1669
- # 定期同步新任務(每 5 個輪詢週期,或 Scheduler 空閒時)
1670
- $syncCounter++
1671
- $shouldSync = ($syncCounter -ge $SyncIntervalCycles) -or ($activeAgents.Count -eq 0)
1672
- if ($shouldSync) {
1673
- $syncCounter = 0
1674
- if (Test-Path $syncScript) {
1675
- & $syncScript -WorkingDirectory $WorkingDirectory -Quiet | Out-Null
1676
- # 不輸出日誌,避免洗版
1677
- }
1678
- }
1679
-
1680
- # 檢查已完成的 Agent
1681
- $completedAgents = @()
1682
- foreach ($agentId in $activeAgents.Keys) {
1683
- $agent = $activeAgents[$agentId]
1684
-
1685
- if ($agent.Process.HasExited) {
1686
- $exitCode = $agent.Process.ExitCode
1687
- $durationMin = [math]::Round(((Get-Date) - $agent.StartTime).TotalMinutes, 1)
1688
-
1689
- # Use improved success detection
1690
- $successResult = Test-AgentSuccess -ExitCode $exitCode -LogFile $agent.LogFile -Branch $agent.Branch -TaskId $agent.TaskId
1691
- $isSuccess = $successResult.Success
1692
-
1693
- if ($isSuccess -and $successResult.Reason -ne "Exit code 0") {
1694
- Write-Log "Success detected via: $($successResult.Reason)" "INFO"
1695
- }
1696
-
1697
- if ($isSuccess) {
1698
- Write-Log "Agent $agentId completed task $($agent.TaskId) successfully ($durationMin min)" "SUCCESS"
1699
-
1700
- # Get task title before updating status
1701
- $queue = Get-TaskQueue
1702
- $taskTitle = ""
1703
- foreach ($t in $queue.tasks) {
1704
- if ($t.id -eq $agent.TaskId) {
1705
- $taskTitle = $t.title
1706
- break
1707
- }
1708
- }
1709
-
1710
- Update-TaskStatus -TaskId $agent.TaskId -Status "completed"
1711
-
1712
- # Move task file to completed folder
1713
- Move-TaskToCompleted -TaskId $agent.TaskId
1714
-
1715
- # Update PROGRESS.md
1716
- Update-ProgressFile -TaskId $agent.TaskId -TaskTitle $taskTitle -Action "Completed"
1717
-
1718
- # Merge task branch to main
1719
- try {
1720
- Push-Location $WorkingDirectory
1721
- git checkout main 2>$null
1722
- git merge $agent.Branch -m "Merge $($agent.TaskId): Completed by $agentId" 2>$null
1723
- git branch -d $agent.Branch 2>$null
1724
- Pop-Location
1725
- Write-Log "Merged branch $($agent.Branch) to main" "SUCCESS"
1726
- } catch {
1727
- Pop-Location
1728
- Write-Log "Failed to merge branch: $_" "WARN"
1729
- }
1730
- } else {
1731
- $exitCodeStr = if ($null -eq $exitCode -or $exitCode -eq "") { "null" } else { $exitCode }
1732
- Write-Log "Agent $agentId failed task $($agent.TaskId) after $durationMin min (exit: $exitCodeStr)" "ERROR"
1733
- Write-Log "Failure reason: $($successResult.Reason)" "ERROR"
1734
-
1735
- # Read error log for debugging
1736
- if ($agent.LogFile -and (Test-Path "$($agent.LogFile).err")) {
1737
- $errContent = Get-Content "$($agent.LogFile).err" -Raw -ErrorAction SilentlyContinue
1738
- if ($errContent -and $errContent.Trim().Length -gt 0) {
1739
- Write-Log "Agent error: $($errContent.Substring(0, [Math]::Min(500, $errContent.Length)))" "ERROR"
1740
- }
1741
- }
1742
-
1743
- # Check if there's partial work done (branch with commits) - mark for resume instead of full reset
1744
- $hasPartialWork = $false
1745
- if ($agent.Branch) {
1746
- try {
1747
- Push-Location $WorkingDirectory
1748
- $null = git rev-parse --verify $agent.Branch 2>$null
1749
- if ($LASTEXITCODE -eq 0) {
1750
- $mainBranch = "main"
1751
- $null = git rev-parse --verify main 2>$null
1752
- if ($LASTEXITCODE -ne 0) { $mainBranch = "master" }
1753
- $commitsAhead = git rev-list --count "${mainBranch}..$($agent.Branch)" 2>$null
1754
- if ($LASTEXITCODE -eq 0 -and [int]$commitsAhead -gt 0) {
1755
- $hasPartialWork = $true
1756
- Write-Log "Branch $($agent.Branch) has $commitsAhead commit(s) - marking for resume" "INFO"
1757
- }
1758
- }
1759
- Pop-Location
1760
- } catch {
1761
- Pop-Location
1762
- }
1763
- }
1764
-
1765
- # Reset task to pending for retry, potentially with resume info
1766
- if ($hasPartialWork) {
1767
- $queue = Get-TaskQueue
1768
- if ($queue) {
1769
- foreach ($t in $queue.tasks) {
1770
- if ($t.id -eq $agent.TaskId) {
1771
- $t.status = "pending"
1772
- $t | Add-Member -NotePropertyName "resume_branch" -NotePropertyValue $agent.Branch -Force
1773
- if ($t.PSObject.Properties["assigned_agent"]) {
1774
- $t.PSObject.Properties.Remove("assigned_agent")
1775
- }
1776
- break
1777
- }
1778
- }
1779
- $queue.last_updated = (Get-Date -Format "o")
1780
- $jsonContent = $queue | ConvertTo-Json -Depth 10
1781
- Write-FileWithRetry -Path $TaskQueueFile -Content $jsonContent
1782
- }
1783
- } else {
1784
- Update-TaskStatus -TaskId $agent.TaskId -Status "pending"
1785
- }
1786
- }
1787
-
1788
- # 釋放鎖
1789
- Remove-AgentLocks -AgentId $agentId
1790
- $completedAgents += $agentId
1791
- } else {
1792
- # 檢查超時
1793
- $elapsed = ((Get-Date) - $agent.StartTime).TotalMinutes
1794
- if ($elapsed -gt $TaskTimeoutMinutes) {
1795
- Write-Log "Agent $agentId timed out on task $($agent.TaskId) (PID: $($agent.Process.Id))" "WARN"
1796
- # Use process tree termination to ensure all child processes are killed
1797
- Stop-ProcessTree -ProcessId $agent.Process.Id
1798
- Remove-AgentLocks -AgentId $agentId
1799
- $completedAgents += $agentId
1800
- }
1801
- }
1802
- }
1803
-
1804
- # 清理已完成的 Agent
1805
- foreach ($agentId in $completedAgents) {
1806
- $activeAgents.Remove($agentId)
1807
- # 從 AGENT_STATE.json 移除 (for dashboard)
1808
- Remove-AgentFromState -AgentId $agentId
1809
- }
1810
-
1811
- # 檢查是否可以啟動新 Agent
1812
- $availableSlots = $MaxAgents - $activeAgents.Count
1813
-
1814
- if ($availableSlots -gt 0) {
1815
- $queue = Get-TaskQueue
1816
-
1817
- if ($queue) {
1818
- $readyTasks = Get-ReadyTasks -Queue $queue
1819
- $lockState = Get-LockState
1820
-
1821
- # Only log when there are ready tasks to potentially launch
1822
- if ($readyTasks.Count -gt 0) {
1823
- Write-Host "" # New line before log message
1824
- Write-Log "Found $($readyTasks.Count) ready task(s), checking availability..."
1825
- }
1826
-
1827
- foreach ($task in $readyTasks) {
1828
- if ($availableSlots -le 0) { break }
1829
-
1830
- # 跳過已經在執行的任務
1831
- $alreadyRunning = $false
1832
- foreach ($agent in $activeAgents.Values) {
1833
- if ($agent.TaskId -eq $task.id) {
1834
- $alreadyRunning = $true
1835
- break
1836
- }
1837
- }
1838
- if ($alreadyRunning) { continue }
1839
-
1840
- # 取得任務 scope
1841
- $scopePaths = @()
1842
- # 優先使用 scope_paths(新格式)
1843
- if ($task.scope_paths) {
1844
- $scopePaths += $task.scope_paths
1845
- }
1846
- # 向下相容:檢查舊格式 scope.paths / scope.files
1847
- elseif ($task.scope) {
1848
- if ($task.scope.paths) { $scopePaths += $task.scope.paths }
1849
- if ($task.scope.files) { $scopePaths += $task.scope.files }
1850
- }
1851
-
1852
- # 如果沒有定義 scope,使用預設路徑
1853
- if ($scopePaths.Count -eq 0) {
1854
- $scopePaths = @("src/")
1855
- }
1856
-
1857
- # 檢查路徑衝突
1858
- $conflict = Test-PathConflict -RequestedPaths $scopePaths -LockState $lockState
1859
-
1860
- if ($conflict.Conflict) {
1861
- Write-Log "Task $($task.id) conflicts with $($conflict.HeldBy) on $($conflict.ConflictingPath)" "WARN"
1862
- continue
1863
- }
1864
-
1865
- # Determine agent type (smart dispatch)
1866
- $explicitType = $null
1867
- if ($task.agent_type) {
1868
- $explicitType = $task.agent_type
1869
- }
1870
- $agentType = Get-AgentType -TaskId $task.id -TaskTitle $task.title -ScopePaths $scopePaths -ExplicitType $explicitType
1871
-
1872
- # 啟動 Agent
1873
- $agentId = New-AgentId
1874
- $agentInfo = Start-AgentWorker -AgentId $agentId -TaskId $task.id -TaskTitle $task.title -ScopePaths $scopePaths -AgentType $agentType
1875
-
1876
- if ($agentInfo -and $agentInfo -is [hashtable]) {
1877
- try {
1878
- # Store agent type in agent info for later reference
1879
- $agentInfo.AgentType = $agentType
1880
-
1881
- # 加入鎖
1882
- Add-PathLocks -AgentId $agentId -TaskId $task.id -Paths $scopePaths
1883
-
1884
- # 更新任務狀態
1885
- Update-TaskStatus -TaskId $task.id -Status "in_progress" -AgentId $agentId -Branch $agentInfo.Branch
1886
-
1887
- # 更新 AGENT_STATE.json (for dashboard) with agent type
1888
- Add-AgentToState -AgentId $agentId -TaskId $task.id -TaskTitle $task.title -Branch $agentInfo.Branch -Status "working" -AgentType $agentType
1889
-
1890
- # 追蹤 Agent (CRITICAL: must happen for cleanup to work)
1891
- $activeAgents[$agentId] = $agentInfo
1892
- $availableSlots--
1893
-
1894
- $displayName = Get-AgentDisplayName -AgentType $agentType
1895
- Write-Log "Launched $displayName ($agentId) for $($task.id): $($task.title)"
1896
- } catch {
1897
- Write-Log "Error setting up agent $agentId for task $($task.id): $_" "ERROR"
1898
- # Agent process was started but setup failed - must still track it for cleanup
1899
- if ($agentInfo.Process -and -not $agentInfo.Process.HasExited) {
1900
- $activeAgents[$agentId] = $agentInfo
1901
- Write-Log "Agent $agentId still tracked for cleanup despite setup error" "WARN"
1902
- }
1903
- # Release any locks that were acquired
1904
- Remove-AgentLocks -AgentId $agentId
1905
- }
1906
- }
1907
- }
1908
- }
1909
- }
1910
-
1911
- # 顯示狀態 (更新同一行,避免洗版)
1912
- $timestamp = Get-Date -Format "HH:mm:ss"
1913
- $statusLine = "[$timestamp] Agents: $($activeAgents.Count)/$MaxAgents"
1914
- if ($activeAgents.Count -gt 0) {
1915
- $agentInfo = @()
1916
- foreach ($agent in $activeAgents.Values) {
1917
- $elapsedMin = [math]::Round(((Get-Date) - $agent.StartTime).TotalMinutes, 1)
1918
- $agentInfo += "$($agent.TaskId)($elapsedMin min)"
1919
- }
1920
- $statusLine += " | " + ($agentInfo -join ", ")
1921
- } else {
1922
- $statusLine += " | Waiting for tasks..."
1923
- }
1924
- # 顯示狀態(交互式用同行更新,非交互式用普通輸出)
1925
- if ($script:IsInteractiveConsole) {
1926
- [Console]::SetCursorPosition(0, $statusLineRow)
1927
- [Console]::ForegroundColor = [ConsoleColor]::Cyan
1928
- [Console]::Write($statusLine.PadRight(100))
1929
- [Console]::ResetColor()
1930
- } else {
1931
- Write-Host $statusLine -ForegroundColor Cyan
1932
- }
1933
-
1934
- if ($SingleRun -and $activeAgents.Count -eq 0) {
1935
- Write-Log "Single run completed, no active agents"
1936
- break
1937
- }
1938
-
1939
- # 等待下一個輪詢週期
1940
- Start-Sleep -Seconds $PollIntervalSeconds
1941
- }
1942
- } catch {
1943
- $errorMsg = "FATAL ERROR in main loop: $_"
1944
- Write-DebugLog $errorMsg "ERROR"
1945
- Write-DebugLog "Stack trace: $($_.ScriptStackTrace)" "ERROR"
1946
- Write-Log $errorMsg "ERROR"
1947
- throw
1948
- } finally {
1949
- # 清理:終止所有 Agent 並釋放鎖
1950
- Write-Log "Shutting down scheduler..."
1951
- Write-DebugLog "Entering finally block - cleanup starting"
1952
-
1953
- foreach ($agentId in $activeAgents.Keys) {
1954
- $agent = $activeAgents[$agentId]
1955
- if (-not $agent.Process.HasExited) {
1956
- Write-Log "Terminating agent $agentId (PID: $($agent.Process.Id))"
1957
- Write-DebugLog "Terminating agent $agentId (PID: $($agent.Process.Id))"
1958
- # Use process tree termination to ensure all child processes are killed
1959
- Stop-ProcessTree -ProcessId $agent.Process.Id
1960
- }
1961
- Remove-AgentLocks -AgentId $agentId
1962
- }
1963
-
1964
- # 清理 AGENT_STATE.json - 移除所有 agents 並設置 scheduler 為 stopped
1965
- $agentState = @{
1966
- agents = @()
1967
- scheduler = @{
1968
- status = "stopped"
1969
- stopped_at = (Get-Date -Format "o")
1970
- }
1971
- last_updated = (Get-Date -Format "o")
1972
- }
1973
- Save-AgentState $agentState
1974
- Write-Log "Cleared AGENT_STATE.json and set scheduler to 'stopped'"
1975
- Write-DebugLog "Cleanup completed, scheduler stopped"
1976
-
1977
- Write-Log "Scheduler stopped"
1978
- }
1
+ # ============================================================
2
+ # Multi-Agent Parallel Scheduler
3
+ # ============================================================
4
+ # 啟動多個 Claude Agent 並行開發
5
+ # - 自動分配無衝突的任務給多個 Agent
6
+ # - 使用路徑鎖定防止衝突
7
+ # - 每個 Agent 在獨立 Branch 工作
8
+ # ============================================================
9
+
10
+ param(
11
+ [int]$MaxAgents = 3, # 最大並行 Agent 數量
12
+ [int]$PollIntervalSeconds = 30, # 輪詢間隔
13
+ [int]$TaskTimeoutMinutes = 60, # 單任務超時
14
+ [switch]$DryRun, # 模擬運行(不實際啟動)
15
+ [switch]$SingleRun, # 單次運行(不循環)
16
+ [string]$WorkingDirectory, # 工作目錄
17
+ [switch]$nopause, # 從 Tauri 調用時使用(跳過暫停)
18
+ [switch]$Standalone # 獨立模式:忽略 Dashboard heartbeat,適合開發 Tauri 本身時使用
19
+ )
20
+
21
+ # Helper function to normalize Windows extended path format (\\?\)
22
+ function Get-NormalizedPath {
23
+ param([string]$Path)
24
+ if ($Path -and $Path.StartsWith("\\?\")) {
25
+ return $Path.Substring(4)
26
+ }
27
+ return $Path
28
+ }
29
+
30
+ # Use KENAZ_PROJECT_PATH env var if WorkingDirectory not specified
31
+ if (-not $WorkingDirectory) {
32
+ if ($env:KENAZ_PROJECT_PATH) {
33
+ $WorkingDirectory = $env:KENAZ_PROJECT_PATH
34
+ } else {
35
+ $WorkingDirectory = (Get-Location).Path
36
+ }
37
+ }
38
+
39
+ # Normalize WorkingDirectory to handle \\?\ prefix from Tauri
40
+ $WorkingDirectory = Get-NormalizedPath $WorkingDirectory
41
+
42
+ # === STARTUP DEBUG LOG ===
43
+ # Write startup info to a debug file for troubleshooting Tauri execution issues
44
+ $StartupDebugFile = Join-Path $WorkingDirectory "multi_agent_startup.log"
45
+
46
+ # Helper function to append to debug log
47
+ function Write-DebugLog {
48
+ param([string]$Message, [string]$Level = "INFO")
49
+ $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
50
+ $logLine = "[$timestamp] [$Level] $Message"
51
+ try {
52
+ Add-Content -Path $StartupDebugFile -Value $logLine -Encoding UTF8 -ErrorAction SilentlyContinue
53
+ } catch { }
54
+ if ($Level -eq "ERROR") {
55
+ Write-Host $logLine -ForegroundColor Red
56
+ }
57
+ }
58
+
59
+ try {
60
+ $startupInfo = @"
61
+ === Multi-Agent Scheduler Startup ===
62
+ Time: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
63
+ WorkingDirectory (raw param): $($PSBoundParameters['WorkingDirectory'])
64
+ WorkingDirectory (normalized): $WorkingDirectory
65
+ KENAZ_PROJECT_PATH env: $($env:KENAZ_PROJECT_PATH)
66
+ Script Path: $($MyInvocation.MyCommand.Path)
67
+ Script Directory: $(Split-Path -Parent $MyInvocation.MyCommand.Path)
68
+ PowerShell Version: $($PSVersionTable.PSVersion)
69
+ OS: $($PSVersionTable.OS)
70
+ Platform: $($PSVersionTable.Platform)
71
+ Arguments: $($args -join ' ')
72
+ nopause: $nopause
73
+ DryRun: $DryRun
74
+ SingleRun: $SingleRun
75
+ Standalone: $Standalone
76
+ MaxAgents: $MaxAgents
77
+ PollIntervalSeconds: $PollIntervalSeconds
78
+ TaskTimeoutMinutes: $TaskTimeoutMinutes
79
+
80
+ === Path Checks ===
81
+ WorkingDirectory exists: $(Test-Path $WorkingDirectory)
82
+ dev folder exists: $(Test-Path (Join-Path $WorkingDirectory 'dev'))
83
+ dev/pipeline exists: $(Test-Path (Join-Path $WorkingDirectory 'dev\pipeline'))
84
+ dev/scripts exists: $(Test-Path (Join-Path $WorkingDirectory 'dev\scripts'))
85
+ TASK_QUEUE.json exists: $(Test-Path (Join-Path $WorkingDirectory 'dev\pipeline\TASK_QUEUE.json'))
86
+
87
+ === Environment Variables ===
88
+ PATH (first 500 chars): $($env:PATH.Substring(0, [Math]::Min(500, $env:PATH.Length)))
89
+
90
+ === Execution Log ===
91
+ "@
92
+ # Ensure directory exists
93
+ $debugDir = Split-Path $StartupDebugFile -Parent
94
+ if (-not (Test-Path $debugDir)) {
95
+ New-Item -ItemType Directory -Path $debugDir -Force | Out-Null
96
+ }
97
+ Set-Content -Path $StartupDebugFile -Value $startupInfo -Encoding UTF8
98
+ Write-Host "[DEBUG] Startup log written to: $StartupDebugFile" -ForegroundColor DarkGray
99
+ } catch {
100
+ Write-Host "[DEBUG] Failed to write startup log: $_" -ForegroundColor Yellow
101
+ }
102
+ # === END STARTUP DEBUG LOG ===
103
+
104
+ # 檢測是否在交互式控制台
105
+ $script:IsInteractiveConsole = $false
106
+ try {
107
+ $null = [Console]::CursorTop
108
+ $script:IsInteractiveConsole = $true
109
+ } catch {
110
+ $script:IsInteractiveConsole = $false
111
+ }
112
+
113
+ # Standalone 模式(忽略 Dashboard heartbeat)
114
+ $script:StandaloneMode = $Standalone
115
+
116
+ # 在 Standalone 模式下,刪除舊的 heartbeat 文件避免混淆
117
+ if ($Standalone) {
118
+ $heartbeatPath = Join-Path $WorkingDirectory "dev\pipeline\DASHBOARD_HEARTBEAT.json"
119
+ if (Test-Path $heartbeatPath) {
120
+ Remove-Item $heartbeatPath -Force -ErrorAction SilentlyContinue
121
+ Write-Host "[STANDALONE] Removed old heartbeat file" -ForegroundColor Magenta
122
+ }
123
+ }
124
+
125
+ # 配置
126
+ $LogFile = Join-Path $WorkingDirectory "dev\pipeline\logs\multi_agent_$(Get-Date -Format 'yyyy-MM-dd').log"
127
+ $TaskQueueFile = Join-Path $WorkingDirectory "dev\pipeline\TASK_QUEUE.json"
128
+ $LockStateFile = Join-Path $WorkingDirectory "dev\pipeline\LOCK_STATE.json"
129
+ $AgentStateFile = Join-Path $WorkingDirectory "dev\pipeline\AGENT_STATE.json"
130
+ $HeartbeatFile = Join-Path $WorkingDirectory "dev\pipeline\DASHBOARD_HEARTBEAT.json"
131
+
132
+ # Heartbeat timeout disabled - only stop on explicit "closing" signal
133
+
134
+ # 確保目錄存在
135
+ $LogDir = Split-Path $LogFile -Parent
136
+ if (-not (Test-Path $LogDir)) {
137
+ New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
138
+ }
139
+
140
+ # ============================================================
141
+ # 工具函數
142
+ # ============================================================
143
+
144
+ function Write-Log {
145
+ param([string]$Message, [string]$Level = "INFO")
146
+ $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
147
+ $LogMessage = "[$Timestamp] [$Level] $Message"
148
+
149
+ switch ($Level) {
150
+ "ERROR" { Write-Host $LogMessage -ForegroundColor Red }
151
+ "WARN" { Write-Host $LogMessage -ForegroundColor Yellow }
152
+ "SUCCESS" { Write-Host $LogMessage -ForegroundColor Green }
153
+ default { Write-Host $LogMessage }
154
+ }
155
+
156
+ Add-Content -Path $LogFile -Value $LogMessage -Encoding UTF8
157
+ }
158
+
159
+ function Write-FileWithRetry {
160
+ param(
161
+ [string]$Path,
162
+ [string]$Content,
163
+ [int]$MaxRetries = 5,
164
+ [int]$RetryDelayMs = 200
165
+ )
166
+
167
+ $attempt = 0
168
+ while ($attempt -lt $MaxRetries) {
169
+ try {
170
+ $Content | Set-Content $Path -Encoding UTF8 -ErrorAction Stop
171
+ return $true
172
+ } catch [System.IO.IOException] {
173
+ $attempt++
174
+ if ($attempt -lt $MaxRetries) {
175
+ Start-Sleep -Milliseconds $RetryDelayMs
176
+ $RetryDelayMs = $RetryDelayMs * 2 # Exponential backoff
177
+ } else {
178
+ Write-Log "Failed to write to $Path after $MaxRetries retries: $_" "ERROR"
179
+ throw $_
180
+ }
181
+ }
182
+ }
183
+ return $false
184
+ }
185
+
186
+ function Stop-ProcessTree {
187
+ param([int]$ProcessId)
188
+
189
+ # Use taskkill with /T flag to kill the process tree
190
+ # This ensures all child processes are also terminated
191
+ try {
192
+ $null = taskkill /PID $ProcessId /T /F 2>&1
193
+ Write-Log "Terminated process tree for PID $ProcessId" "INFO"
194
+ return $true
195
+ } catch {
196
+ Write-Log "Failed to terminate process tree for PID $ProcessId : $_" "WARN"
197
+ return $false
198
+ }
199
+ }
200
+
201
+ function Stop-OrphanClaudeProcesses {
202
+ # Find and terminate orphan Claude processes that are not tracked by this scheduler
203
+ # SAFETY: Only terminates processes that:
204
+ # 1. Are NOT in TrackedProcessIds
205
+ # 2. Have a command line containing "--dangerously-skip-permissions" (scheduler-launched agents)
206
+ # This prevents accidentally killing user's IDE Claude sessions
207
+ param(
208
+ [hashtable]$TrackedProcessIds = @{},
209
+ [switch]$Force # Force cleanup even without command line check (dangerous!)
210
+ )
211
+
212
+ $claudeProcesses = Get-Process -Name "claude" -ErrorAction SilentlyContinue
213
+
214
+ if (-not $claudeProcesses) {
215
+ return 0
216
+ }
217
+
218
+ $orphanCount = 0
219
+ foreach ($proc in $claudeProcesses) {
220
+ # Skip if this process is being tracked
221
+ if ($TrackedProcessIds.ContainsKey($proc.Id)) {
222
+ continue
223
+ }
224
+
225
+ # Safety check: only kill processes launched by scheduler (have --dangerously-skip-permissions flag)
226
+ if (-not $Force) {
227
+ try {
228
+ $cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($proc.Id)" -ErrorAction SilentlyContinue).CommandLine
229
+ if (-not $cmdLine -or $cmdLine -notmatch "--dangerously-skip-permissions") {
230
+ # This is likely a user's IDE session, skip it
231
+ continue
232
+ }
233
+ } catch {
234
+ # If we can't check command line, don't kill to be safe
235
+ continue
236
+ }
237
+ }
238
+
239
+ try {
240
+ Write-Log "Found orphan Claude agent process (PID: $($proc.Id), Started: $($proc.StartTime))" "WARN"
241
+ Stop-ProcessTree -ProcessId $proc.Id
242
+ $orphanCount++
243
+ } catch {
244
+ Write-Log "Failed to stop orphan process $($proc.Id): $_" "ERROR"
245
+ }
246
+ }
247
+
248
+ return $orphanCount
249
+ }
250
+
251
+ # ============================================================
252
+ # Smart Agent Dispatch Functions
253
+ # ============================================================
254
+
255
+ function Get-AgentType {
256
+ param(
257
+ [string]$TaskId,
258
+ [string]$TaskTitle,
259
+ [string[]]$ScopePaths,
260
+ [string]$ExplicitType = $null
261
+ )
262
+
263
+ # If explicit type is specified, use it
264
+ if ($ExplicitType) {
265
+ return $ExplicitType
266
+ }
267
+
268
+ $titleLower = $TaskTitle.ToLower()
269
+
270
+ # Keyword matching for agent type (English only for PowerShell compatibility)
271
+ $designerKeywords = @('ui', 'ux', 'layout', 'style', 'design', 'css', 'theme', 'color', 'visual', 'icon')
272
+ $backendKeywords = @('api', 'database', 'backend', 'endpoint', 'server', 'sql', 'migration', 'supabase', 'postgres')
273
+ $qaKeywords = @('test', 'qa', 'quality', 'unit test', 'e2e', 'spec', 'coverage', 'assert')
274
+ $sreKeywords = @('deploy', 'ci/cd', 'monitoring', 'infrastructure', 'docker', 'kubernetes', 'devops', 'pipeline')
275
+ $frontendKeywords = @('component', 'react', 'frontend', 'tsx', 'jsx', 'hook', 'state', 'page', 'dashboard')
276
+
277
+ # Check keywords in title
278
+ foreach ($kw in $designerKeywords) {
279
+ if ($titleLower.Contains($kw)) { return "designer" }
280
+ }
281
+ foreach ($kw in $backendKeywords) {
282
+ if ($titleLower.Contains($kw)) { return "backend" }
283
+ }
284
+ foreach ($kw in $qaKeywords) {
285
+ if ($titleLower.Contains($kw)) { return "qa" }
286
+ }
287
+ foreach ($kw in $sreKeywords) {
288
+ if ($titleLower.Contains($kw)) { return "sre" }
289
+ }
290
+ foreach ($kw in $frontendKeywords) {
291
+ if ($titleLower.Contains($kw)) { return "developer" }
292
+ }
293
+
294
+ # Scope path matching
295
+ foreach ($path in $ScopePaths) {
296
+ $pathLower = $path.ToLower()
297
+ if ($pathLower -match "api/" -or $pathLower -match "backend/") {
298
+ return "backend"
299
+ }
300
+ if ($pathLower -match "components/" -or $pathLower -match "src/" -or $pathLower -match "dashboard/") {
301
+ return "developer"
302
+ }
303
+ if ($pathLower -match "tests/" -or $pathLower -match "spec/") {
304
+ return "qa"
305
+ }
306
+ if ($pathLower -match "deploy/" -or $pathLower -match "infra/") {
307
+ return "sre"
308
+ }
309
+ }
310
+
311
+ # Default to dev_agent
312
+ return "dev"
313
+ }
314
+
315
+ function Get-SkillPath {
316
+ param([string]$AgentType)
317
+
318
+ $skillPaths = @{
319
+ "developer" = ".claude/skills/developer/SKILL.md"
320
+ "backend" = ".claude/skills/backend_engineer/SKILL.md"
321
+ "designer" = ".claude/skills/designer/SKILL.md"
322
+ "qa" = ".claude/skills/qa_engineer/SKILL.md"
323
+ "sre" = ".claude/skills/sre_agent/SKILL.md"
324
+ "dev" = ".claude/skills/dev_agent/SKILL.md"
325
+ "frontend" = ".claude/skills/frontend_agent/SKILL.md"
326
+ "architect" = ".claude/skills/architect_agent/SKILL.md"
327
+ }
328
+
329
+ if ($skillPaths.ContainsKey($AgentType)) {
330
+ return $skillPaths[$AgentType]
331
+ }
332
+
333
+ # Default to dev_agent
334
+ return ".claude/skills/dev_agent/SKILL.md"
335
+ }
336
+
337
+ function Get-AgentDisplayName {
338
+ param([string]$AgentType)
339
+
340
+ $displayNames = @{
341
+ "developer" = "Frontend Developer"
342
+ "backend" = "Backend Engineer"
343
+ "designer" = "UI/UX Designer"
344
+ "qa" = "QA Engineer"
345
+ "sre" = "SRE Agent"
346
+ "dev" = "Dev Agent"
347
+ "frontend" = "Frontend Agent"
348
+ "architect" = "Architect"
349
+ }
350
+
351
+ if ($displayNames.ContainsKey($AgentType)) {
352
+ return $displayNames[$AgentType]
353
+ }
354
+
355
+ return "Dev Agent"
356
+ }
357
+
358
+ function Get-TaskQueue {
359
+ if (-not (Test-Path $TaskQueueFile)) {
360
+ return $null
361
+ }
362
+
363
+ try {
364
+ $content = Get-Content $TaskQueueFile -Raw -Encoding UTF8
365
+ return $content | ConvertFrom-Json
366
+ } catch {
367
+ Write-Log "Failed to read task queue: $_" "ERROR"
368
+ return $null
369
+ }
370
+ }
371
+
372
+ function Get-LockState {
373
+ if (-not (Test-Path $LockStateFile)) {
374
+ return @{ locks = @() }
375
+ }
376
+
377
+ try {
378
+ $content = Get-Content $LockStateFile -Raw -Encoding UTF8
379
+ return $content | ConvertFrom-Json
380
+ } catch {
381
+ return @{ locks = @() }
382
+ }
383
+ }
384
+
385
+ function Save-LockState {
386
+ param($State)
387
+
388
+ try {
389
+ $State | Add-Member -NotePropertyName "last_updated" -NotePropertyValue (Get-Date -Format "o") -Force
390
+ $jsonContent = $State | ConvertTo-Json -Depth 10
391
+ Write-FileWithRetry -Path $LockStateFile -Content $jsonContent
392
+ } catch {
393
+ Write-Log "Failed to save lock state: $_" "WARN"
394
+ # Don't propagate - allow scheduler to continue
395
+ }
396
+ }
397
+
398
+ function Get-AgentState {
399
+ if (-not (Test-Path $AgentStateFile)) {
400
+ return @{ agents = @() }
401
+ }
402
+
403
+ try {
404
+ $content = Get-Content $AgentStateFile -Raw -Encoding UTF8
405
+ return $content | ConvertFrom-Json
406
+ } catch {
407
+ return @{ agents = @() }
408
+ }
409
+ }
410
+
411
+ function Save-AgentState {
412
+ param($State)
413
+
414
+ try {
415
+ $State | Add-Member -NotePropertyName "last_updated" -NotePropertyValue (Get-Date -Format "o") -Force
416
+ $jsonContent = $State | ConvertTo-Json -Depth 10
417
+ Write-FileWithRetry -Path $AgentStateFile -Content $jsonContent
418
+ } catch {
419
+ Write-Log "Failed to save agent state: $_" "WARN"
420
+ # Don't propagate - allow scheduler to continue
421
+ }
422
+ }
423
+
424
+ function Test-DashboardHeartbeat {
425
+ # Check if dashboard is still alive via heartbeat file
426
+ # Stop agents when:
427
+ # 1. Dashboard explicitly sends "closing" signal
428
+ # 2. Heartbeat file hasn't been updated for too long (crash detection)
429
+ #
430
+ # NOTE: In Standalone mode, heartbeat is ignored entirely
431
+
432
+ # Standalone mode - always return alive
433
+ if ($script:StandaloneMode) {
434
+ return @{ Alive = $true; Reason = "Standalone mode (heartbeat ignored)" }
435
+ }
436
+
437
+ $HeartbeatTimeoutSeconds = 120 # 2 minutes - if no heartbeat update, assume crashed
438
+
439
+ if (-not (Test-Path $HeartbeatFile)) {
440
+ # No heartbeat file - assume running without dashboard (CLI mode)
441
+ return @{ Alive = $true; Reason = "No heartbeat file (CLI mode)" }
442
+ }
443
+
444
+ try {
445
+ $content = Get-Content $HeartbeatFile -Raw -Encoding UTF8
446
+ $heartbeat = $content | ConvertFrom-Json
447
+
448
+ # Check if dashboard is explicitly closing
449
+ if ($heartbeat.status -eq "closing") {
450
+ return @{ Alive = $false; Reason = "Dashboard closing" }
451
+ }
452
+
453
+ # Check heartbeat timeout (crash detection)
454
+ if ($heartbeat.timestamp) {
455
+ $lastHeartbeat = [DateTime]::Parse($heartbeat.timestamp)
456
+ $elapsed = (Get-Date) - $lastHeartbeat
457
+ if ($elapsed.TotalSeconds -gt $HeartbeatTimeoutSeconds) {
458
+ return @{
459
+ Alive = $false
460
+ Reason = "Dashboard heartbeat timeout ($([int]$elapsed.TotalSeconds)s > ${HeartbeatTimeoutSeconds}s)"
461
+ }
462
+ }
463
+ }
464
+
465
+ return @{ Alive = $true; SessionId = $heartbeat.session_id }
466
+ } catch {
467
+ # Failed to read/parse - check file modification time as fallback
468
+ try {
469
+ $fileInfo = Get-Item $HeartbeatFile
470
+ $elapsed = (Get-Date) - $fileInfo.LastWriteTime
471
+ if ($elapsed.TotalSeconds -gt $HeartbeatTimeoutSeconds) {
472
+ return @{
473
+ Alive = $false
474
+ Reason = "Dashboard heartbeat file stale ($([int]$elapsed.TotalSeconds)s)"
475
+ }
476
+ }
477
+ } catch { }
478
+ return @{ Alive = $true; Reason = "Heartbeat check fallback" }
479
+ }
480
+ }
481
+
482
+ function Add-AgentToState {
483
+ param(
484
+ [string]$AgentId,
485
+ [string]$TaskId,
486
+ [string]$TaskTitle,
487
+ [string]$Branch,
488
+ [string]$Status = "working",
489
+ [string]$AgentType = "dev_agent"
490
+ )
491
+
492
+ $agentState = Get-AgentState
493
+ $agents = [System.Collections.ArrayList]@()
494
+ if ($agentState.agents) {
495
+ $agents = [System.Collections.ArrayList]@($agentState.agents)
496
+ }
497
+
498
+ # Remove existing entry if any
499
+ $agents = [System.Collections.ArrayList]@($agents | Where-Object { $_.id -ne $AgentId })
500
+
501
+ # Map internal agent type to dashboard-compatible type
502
+ $dashboardType = switch ($AgentType) {
503
+ "developer" { "developer" }
504
+ "backend" { "backend_engineer" }
505
+ "designer" { "designer" }
506
+ "qa" { "qa_engineer" }
507
+ "sre" { "sre_agent" }
508
+ "frontend" { "frontend_agent" }
509
+ "architect" { "architect_agent" }
510
+ default { "dev_agent" }
511
+ }
512
+
513
+ # Debug: Log type mapping for troubleshooting
514
+ Write-Log "Type mapping: AgentType='$AgentType' -> dashboardType='$dashboardType'" "INFO"
515
+
516
+ # Add new agent entry (format compatible with dashboard)
517
+ $agents.Add(@{
518
+ id = $AgentId
519
+ type = $dashboardType
520
+ status = $Status
521
+ current_task = $TaskId
522
+ current_operation = $TaskTitle
523
+ branch = $Branch
524
+ started_at = (Get-Date -Format "o")
525
+ task_id = $TaskId
526
+ task_title = $TaskTitle
527
+ }) | Out-Null
528
+
529
+ $agentState.agents = $agents
530
+ Save-AgentState $agentState
531
+ $displayName = Get-AgentDisplayName -AgentType $AgentType
532
+ Write-Log "Added agent $AgentId ($displayName) to AGENT_STATE.json (task: $TaskId)"
533
+ }
534
+
535
+ function Remove-AgentFromState {
536
+ param([string]$AgentId)
537
+
538
+ $agentState = Get-AgentState
539
+ if ($agentState.agents) {
540
+ $newAgents = @($agentState.agents | Where-Object { $_.id -ne $AgentId })
541
+ $agentState.agents = $newAgents
542
+ Save-AgentState $agentState
543
+ Write-Log "Removed agent $AgentId from AGENT_STATE.json"
544
+ }
545
+ }
546
+
547
+ # Clean exit function - ensures AGENT_STATE.json is updated before exit
548
+ # PowerShell's `exit` doesn't trigger `finally` blocks, so we need this
549
+ function Stop-SchedulerCleanly {
550
+ param(
551
+ [int]$ExitCode = 0,
552
+ [string]$Reason = "Normal exit"
553
+ )
554
+
555
+ Write-Log "Scheduler stopping: $Reason"
556
+
557
+ # Terminate all active agents if any
558
+ if ($script:activeAgents -and $script:activeAgents.Count -gt 0) {
559
+ foreach ($agentId in @($script:activeAgents.Keys)) {
560
+ $agent = $script:activeAgents[$agentId]
561
+ if ($agent.Process -and -not $agent.Process.HasExited) {
562
+ Write-Log "Terminating agent $agentId (PID: $($agent.Process.Id))"
563
+ Stop-ProcessTree -ProcessId $agent.Process.Id
564
+ }
565
+ Remove-AgentLocks -AgentId $agentId
566
+ Remove-AgentFromState -AgentId $agentId
567
+ }
568
+ $script:activeAgents.Clear()
569
+ }
570
+
571
+ # Update AGENT_STATE.json to show scheduler is stopped
572
+ $agentState = @{
573
+ agents = @()
574
+ scheduler = @{
575
+ status = "stopped"
576
+ stopped_at = (Get-Date -Format "o")
577
+ }
578
+ last_updated = (Get-Date -Format "o")
579
+ }
580
+ Save-AgentState $agentState
581
+ Write-Log "AGENT_STATE.json updated: scheduler stopped"
582
+
583
+ exit $ExitCode
584
+ }
585
+
586
+ function Test-PathConflict {
587
+ param(
588
+ [string[]]$RequestedPaths,
589
+ $LockState
590
+ )
591
+
592
+ foreach ($reqPath in $RequestedPaths) {
593
+ foreach ($lock in $LockState.locks) {
594
+ $lockPath = $lock.path
595
+
596
+ # 檢查路徑衝突
597
+ if ($reqPath.StartsWith($lockPath) -or $lockPath.StartsWith($reqPath) -or $reqPath -eq $lockPath) {
598
+ return @{
599
+ Conflict = $true
600
+ ConflictingPath = $lockPath
601
+ HeldBy = $lock.agent_id
602
+ Task = $lock.task_id
603
+ }
604
+ }
605
+ }
606
+ }
607
+
608
+ return @{ Conflict = $false }
609
+ }
610
+
611
+ function Get-ReadyTasks {
612
+ param($Queue)
613
+
614
+ $readyTasks = @()
615
+ $completedIds = @()
616
+
617
+ # 收集已完成任務 ID
618
+ foreach ($task in $Queue.tasks) {
619
+ if ($task.status -eq "completed") {
620
+ $completedIds += $task.id
621
+ }
622
+ }
623
+
624
+ # 找出可執行的任務
625
+ foreach ($task in $Queue.tasks) {
626
+ if ($task.status -ne "pending") {
627
+ continue
628
+ }
629
+
630
+ # 檢查依賴
631
+ $depsOk = $true
632
+ if ($task.dependencies) {
633
+ foreach ($dep in $task.dependencies) {
634
+ if ($dep -notin $completedIds) {
635
+ $depsOk = $false
636
+ break
637
+ }
638
+ }
639
+ }
640
+
641
+ if ($depsOk) {
642
+ $readyTasks += $task
643
+ }
644
+ }
645
+
646
+ # 按優先級排序 (P0 > P1 > P2 > P3)
647
+ $priorityOrder = @{ "P0" = 0; "P1" = 1; "P2" = 2; "P3" = 3 }
648
+ $readyTasks = $readyTasks | Sort-Object { $priorityOrder[$_.priority] }
649
+
650
+ return $readyTasks
651
+ }
652
+
653
+ function New-AgentId {
654
+ return "agent_$(Get-Date -Format 'HHmmss')_$([guid]::NewGuid().ToString().Substring(0,4))"
655
+ }
656
+
657
+ function Start-AgentWorker {
658
+ param(
659
+ [string]$AgentId,
660
+ [string]$TaskId,
661
+ [string]$TaskTitle,
662
+ [string[]]$ScopePaths,
663
+ [string]$AgentType = "dev"
664
+ )
665
+
666
+ $displayName = Get-AgentDisplayName -AgentType $AgentType
667
+ $skillPath = Get-SkillPath -AgentType $AgentType
668
+ Write-Log "Starting $displayName ($AgentId) for task $TaskId" "SUCCESS"
669
+ Write-Log " Skill: $skillPath" "INFO"
670
+
671
+ if ($DryRun) {
672
+ Write-Log "[DRY RUN] Would start: claude -p '/agent-worker --task $TaskId --agent $AgentId'" "WARN"
673
+ return $null
674
+ }
675
+
676
+ # Check if task has a resumable branch (from previous interrupted session)
677
+ $queue = Get-TaskQueue
678
+ $resumeBranch = $null
679
+ if ($queue) {
680
+ $taskObj = $queue.tasks | Where-Object { $_.id -eq $TaskId } | Select-Object -First 1
681
+ if ($taskObj -and $taskObj.resume_branch) {
682
+ $resumeBranch = $taskObj.resume_branch
683
+ }
684
+ }
685
+
686
+ # 決定使用的 Branch
687
+ $branchName = if ($resumeBranch) {
688
+ Write-Log "Resuming task from existing branch: $resumeBranch" "INFO"
689
+ $resumeBranch
690
+ } else {
691
+ "task-$($TaskId.ToLower())-$AgentId"
692
+ }
693
+
694
+ try {
695
+ Push-Location $WorkingDirectory
696
+
697
+ if ($resumeBranch) {
698
+ # Resume: checkout existing branch
699
+ $null = git checkout $resumeBranch 2>$null
700
+ if ($LASTEXITCODE -ne 0) {
701
+ Write-Log "Failed to checkout resume branch, creating new one" "WARN"
702
+ $branchName = "task-$($TaskId.ToLower())-$AgentId"
703
+ $null = git checkout main 2>$null
704
+ if ($LASTEXITCODE -ne 0) { $null = git checkout master 2>$null }
705
+ $null = git checkout -b $branchName 2>$null
706
+ }
707
+ } else {
708
+ # New task: create new branch from main
709
+ $currentBranch = git rev-parse --abbrev-ref HEAD 2>$null
710
+ if ($currentBranch -ne "main" -and $currentBranch -ne "master") {
711
+ $null = git checkout main 2>$null
712
+ if ($LASTEXITCODE -ne 0) {
713
+ $null = git checkout master 2>$null
714
+ }
715
+ }
716
+ # 創建任務分支
717
+ $null = git checkout -b $branchName 2>$null
718
+ }
719
+
720
+ Pop-Location
721
+ } catch {
722
+ Write-Log "Failed to setup branch: $_" "ERROR"
723
+ Pop-Location
724
+ return $null
725
+ }
726
+
727
+ # 啟動 Claude Agent Worker
728
+ $prompt = "/agent-worker --task $TaskId --agent $AgentId --branch $branchName"
729
+
730
+ # Create log file for this agent's output
731
+ $logDir = Join-Path $WorkingDirectory "dev\pipeline\logs"
732
+ if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
733
+ $agentLogFile = Join-Path $logDir "agent_${AgentId}.log"
734
+
735
+ try {
736
+ # Use cmd.exe /c to invoke claude (npm global installs create .cmd scripts on Windows)
737
+ # This ensures proper execution regardless of how claude was installed
738
+ $process = Start-Process -FilePath "cmd.exe" `
739
+ -ArgumentList "/c", "claude", "--dangerously-skip-permissions", "-p", "`"$prompt`"" `
740
+ -WorkingDirectory $WorkingDirectory `
741
+ -PassThru `
742
+ -NoNewWindow `
743
+ -RedirectStandardOutput $agentLogFile `
744
+ -RedirectStandardError "${agentLogFile}.err"
745
+
746
+ Write-Log "Agent $AgentId started (PID: $($process.Id))"
747
+ Write-Log "Agent log: $agentLogFile"
748
+
749
+ $result = @{
750
+ Process = $process
751
+ AgentId = $AgentId
752
+ TaskId = $TaskId
753
+ Branch = $branchName
754
+ StartTime = Get-Date
755
+ LogFile = $agentLogFile
756
+ }
757
+ return $result
758
+ } catch {
759
+ Write-Log "Failed to start agent: $_" "ERROR"
760
+ return $null
761
+ }
762
+ }
763
+
764
+ function Update-TaskStatus {
765
+ param(
766
+ [string]$TaskId,
767
+ [string]$Status,
768
+ [string]$AgentId = $null,
769
+ [string]$Branch = $null
770
+ )
771
+
772
+ $queue = Get-TaskQueue
773
+ if (-not $queue) { return }
774
+
775
+ foreach ($task in $queue.tasks) {
776
+ if ($task.id -eq $TaskId) {
777
+ $task.status = $Status
778
+
779
+ if ($Status -eq "in_progress") {
780
+ $task | Add-Member -NotePropertyName "started_at" -NotePropertyValue (Get-Date -Format "o") -Force
781
+ if ($AgentId) {
782
+ $task | Add-Member -NotePropertyName "assigned_agent" -NotePropertyValue $AgentId -Force
783
+ }
784
+ if ($Branch) {
785
+ $task | Add-Member -NotePropertyName "branch" -NotePropertyValue $Branch -Force
786
+ }
787
+ # Clear resume_branch marker now that agent has started
788
+ if ($task.PSObject.Properties["resume_branch"]) {
789
+ $task.PSObject.Properties.Remove("resume_branch")
790
+ }
791
+ } elseif ($Status -eq "completed") {
792
+ $task | Add-Member -NotePropertyName "completed_at" -NotePropertyValue (Get-Date -Format "o") -Force
793
+ }
794
+
795
+ break
796
+ }
797
+ }
798
+
799
+ try {
800
+ $queue | Add-Member -NotePropertyName "last_updated" -NotePropertyValue (Get-Date -Format "o") -Force
801
+ $jsonContent = $queue | ConvertTo-Json -Depth 10
802
+ Write-FileWithRetry -Path $TaskQueueFile -Content $jsonContent
803
+ } catch {
804
+ Write-Log "Failed to update task queue for ${TaskId}: $_" "WARN"
805
+ # Don't propagate - allow scheduler to continue
806
+ }
807
+ }
808
+
809
+ function Move-TaskToCompleted {
810
+ param([string]$TaskId)
811
+
812
+ $tasksDir = Join-Path $WorkingDirectory "dev\pipeline\tasks"
813
+ $completedDir = Join-Path $WorkingDirectory "dev\pipeline\completed"
814
+
815
+ # Ensure completed directory exists
816
+ if (-not (Test-Path $completedDir)) {
817
+ New-Item -ItemType Directory -Path $completedDir -Force | Out-Null
818
+ }
819
+
820
+ # Find and move task file (support both .md and .yaml formats)
821
+ $taskFile = Get-ChildItem -Path $tasksDir -Filter "${TaskId}_*" -ErrorAction SilentlyContinue |
822
+ Where-Object { $_.Extension -in @('.md', '.yaml', '.yml') } |
823
+ Select-Object -First 1
824
+ if ($taskFile) {
825
+ $destPath = Join-Path $completedDir $taskFile.Name
826
+ Move-Item -Path $taskFile.FullName -Destination $destPath -Force
827
+ Write-Log "Moved task file to completed: $($taskFile.Name)" "INFO"
828
+ }
829
+ }
830
+
831
+ # ============================================================
832
+ # 任務檔案備份(啟動時執行)
833
+ # ============================================================
834
+
835
+ function Backup-TaskFiles {
836
+ # Backup tasks/ folder before starting scheduler
837
+ # This prevents loss of task files if scheduler crashes or misbehaves
838
+
839
+ $tasksDir = Join-Path $WorkingDirectory "dev\pipeline\tasks"
840
+ $backupDir = Join-Path $WorkingDirectory "dev\pipeline\backups"
841
+
842
+ if (-not (Test-Path $tasksDir)) {
843
+ Write-Log "Tasks directory not found, skipping backup" "WARN"
844
+ return $null
845
+ }
846
+
847
+ $taskFiles = Get-ChildItem -Path $tasksDir -File -ErrorAction SilentlyContinue
848
+ if (-not $taskFiles -or $taskFiles.Count -eq 0) {
849
+ Write-Log "No task files to backup" "INFO"
850
+ return $null
851
+ }
852
+
853
+ # Create backup directory with timestamp
854
+ $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
855
+ $backupPath = Join-Path $backupDir "tasks_$timestamp"
856
+
857
+ try {
858
+ if (-not (Test-Path $backupDir)) {
859
+ New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
860
+ }
861
+
862
+ # Copy entire tasks folder
863
+ Copy-Item -Path $tasksDir -Destination $backupPath -Recurse -Force
864
+
865
+ $backupCount = (Get-ChildItem -Path $backupPath -File -ErrorAction SilentlyContinue).Count
866
+ Write-Log "Backed up $backupCount task file(s) to: $backupPath" "SUCCESS"
867
+
868
+ # Clean up old backups (keep last 5)
869
+ $oldBackups = Get-ChildItem -Path $backupDir -Directory -Filter "tasks_*" |
870
+ Sort-Object CreationTime -Descending |
871
+ Select-Object -Skip 5
872
+
873
+ foreach ($old in $oldBackups) {
874
+ Remove-Item -Path $old.FullName -Recurse -Force -ErrorAction SilentlyContinue
875
+ Write-Log "Removed old backup: $($old.Name)" "INFO"
876
+ }
877
+
878
+ return $backupPath
879
+ } catch {
880
+ Write-Log "Failed to backup task files: $_" "ERROR"
881
+ return $null
882
+ }
883
+ }
884
+
885
+ # ============================================================
886
+ # 改進的 Exit Code 偵測
887
+ # ============================================================
888
+
889
+ function Test-AgentSuccess {
890
+ param(
891
+ [int]$ExitCode,
892
+ [string]$LogFile,
893
+ [string]$Branch,
894
+ [string]$TaskId
895
+ )
896
+
897
+ # Priority 1: Exit code 0 is always success
898
+ if ($ExitCode -eq 0) {
899
+ return @{ Success = $true; Reason = "Exit code 0" }
900
+ }
901
+
902
+ # Priority 2: Check for commits in branch (actual work done)
903
+ if ($Branch) {
904
+ try {
905
+ Push-Location $WorkingDirectory
906
+
907
+ # Check if branch exists
908
+ $null = git rev-parse --verify $Branch 2>$null
909
+ if ($LASTEXITCODE -eq 0) {
910
+ # Determine main branch
911
+ $mainBranch = "main"
912
+ $null = git rev-parse --verify main 2>$null
913
+ if ($LASTEXITCODE -ne 0) { $mainBranch = "master" }
914
+
915
+ # Count commits ahead of main
916
+ $commitsAhead = git rev-list --count "${mainBranch}..${Branch}" 2>$null
917
+ if ($LASTEXITCODE -eq 0 -and [int]$commitsAhead -gt 0) {
918
+ # Check if any commit message contains the task ID
919
+ $commitMessages = git log --oneline "${mainBranch}..${Branch}" 2>$null
920
+ if ($commitMessages -match $TaskId) {
921
+ Pop-Location
922
+ return @{
923
+ Success = $true
924
+ Reason = "Branch has $commitsAhead commit(s) with $TaskId"
925
+ }
926
+ }
927
+ }
928
+ }
929
+
930
+ Pop-Location
931
+ } catch {
932
+ Pop-Location
933
+ }
934
+ }
935
+
936
+ # Priority 3: Check log file for success indicators
937
+ if ($LogFile -and (Test-Path $LogFile)) {
938
+ try {
939
+ $logContent = Get-Content $LogFile -Raw -ErrorAction SilentlyContinue
940
+ if ($logContent) {
941
+ # Multiple success patterns
942
+ $successPatterns = @(
943
+ "Status:\s*SUCCESS",
944
+ "TASK_\d+\s+is\s+now\s+complete",
945
+ "Task.*completed\s+successfully",
946
+ "^\s*##\s+TASK_\d+\s+Summary",
947
+ "\[SUCCESS\].*completed",
948
+ "All\s+\d+\s+tests\s+pass"
949
+ )
950
+
951
+ foreach ($pattern in $successPatterns) {
952
+ if ($logContent -match $pattern) {
953
+ return @{
954
+ Success = $true
955
+ Reason = "Log matches success pattern: $pattern"
956
+ }
957
+ }
958
+ }
959
+
960
+ # Check for failure patterns (explicit failures)
961
+ $failurePatterns = @(
962
+ "API Error:\s*5\d\d",
963
+ "FATAL\s+ERROR",
964
+ "Build\s+failed",
965
+ "Compilation\s+error"
966
+ )
967
+
968
+ foreach ($pattern in $failurePatterns) {
969
+ if ($logContent -match $pattern) {
970
+ return @{
971
+ Success = $false
972
+ Reason = "Log matches failure pattern: $pattern"
973
+ }
974
+ }
975
+ }
976
+ }
977
+ } catch {
978
+ # Ignore log read errors
979
+ }
980
+ }
981
+
982
+ # Default: If exit code is null/unknown and no definitive indicators, check branch again
983
+ if ($null -eq $ExitCode -or $ExitCode -eq "") {
984
+ # One more check: if branch exists with commits, consider it partial success
985
+ if ($Branch) {
986
+ try {
987
+ Push-Location $WorkingDirectory
988
+ $null = git rev-parse --verify $Branch 2>$null
989
+ if ($LASTEXITCODE -eq 0) {
990
+ $commitsAhead = git rev-list --count "main..${Branch}" 2>$null
991
+ if ($LASTEXITCODE -ne 0) {
992
+ $commitsAhead = git rev-list --count "master..${Branch}" 2>$null
993
+ }
994
+ if ($LASTEXITCODE -eq 0 -and [int]$commitsAhead -gt 0) {
995
+ Pop-Location
996
+ return @{
997
+ Success = $true
998
+ Reason = "Exit null but branch has $commitsAhead commit(s) - assuming success"
999
+ }
1000
+ }
1001
+ }
1002
+ Pop-Location
1003
+ } catch {
1004
+ Pop-Location
1005
+ }
1006
+ }
1007
+
1008
+ return @{
1009
+ Success = $false
1010
+ Reason = "Exit code null with no success indicators"
1011
+ }
1012
+ }
1013
+
1014
+ return @{
1015
+ Success = $false
1016
+ Reason = "Exit code $ExitCode (non-zero)"
1017
+ }
1018
+ }
1019
+
1020
+ function Update-ProgressFile {
1021
+ param(
1022
+ [string]$TaskId,
1023
+ [string]$TaskTitle,
1024
+ [string]$Action
1025
+ )
1026
+
1027
+ $progressFile = Join-Path $WorkingDirectory "dev\pipeline\PROGRESS.md"
1028
+ if (-not (Test-Path $progressFile)) { return }
1029
+
1030
+ $content = Get-Content $progressFile -Raw -Encoding UTF8
1031
+ $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm"
1032
+
1033
+ # Update Last Update section
1034
+ $content = $content -replace '(?ms)(## Last Update\r?\n-.*?Time.*?:)[^\r\n]*', "`$1 $timestamp"
1035
+ $content = $content -replace '(?ms)(## Last Update\r?\n.*?Agent.*?:)[^\r\n]*', "`$1 Multi-Agent Scheduler"
1036
+ $content = $content -replace '(?ms)(## Last Update\r?\n.*?Session.*?:)[^\r\n]*', "`$1 $TaskId $Action"
1037
+
1038
+ # Update statistics based on current queue
1039
+ $queue = Get-TaskQueue
1040
+ if ($queue) {
1041
+ $total = $queue.tasks.Count
1042
+ $completed = @($queue.tasks | Where-Object { $_.status -eq "completed" }).Count
1043
+ $inProgress = @($queue.tasks | Where-Object { $_.status -eq "in_progress" }).Count
1044
+ $pending = @($queue.tasks | Where-Object { $_.status -eq "pending" }).Count
1045
+ $blocked = @($queue.tasks | Where-Object { $_.status -eq "blocked" }).Count
1046
+
1047
+ $completedPct = if ($total -gt 0) { [math]::Round($completed / $total * 100) } else { 0 }
1048
+ $inProgressPct = if ($total -gt 0) { [math]::Round($inProgress / $total * 100) } else { 0 }
1049
+ $pendingPct = if ($total -gt 0) { [math]::Round($pending / $total * 100) } else { 0 }
1050
+ $blockedPct = if ($total -gt 0) { [math]::Round($blocked / $total * 100) } else { 0 }
1051
+
1052
+ $statsBlock = @"
1053
+ Total Tasks: $total
1054
+ Completed: $completed ($completedPct%)
1055
+ In Progress: $inProgress ($inProgressPct%)
1056
+ Pending: $pending ($pendingPct%)
1057
+ Blocked: $blocked ($blockedPct%)
1058
+ "@
1059
+ $content = $content -replace '(?ms)(### Overall Progress\r?\n```\r?\n).*?(```)', "`$1$statsBlock`n`$2"
1060
+ }
1061
+
1062
+ # Add to change history
1063
+ $dateStr = Get-Date -Format "yyyy-MM-dd"
1064
+ $historyEntry = "| $dateStr | $Action | $TaskId - $TaskTitle |"
1065
+ $content = $content -replace '(\| 2026-01-29 \| Tasks Created \|[^\r\n]*)', "`$1`n$historyEntry"
1066
+
1067
+ Set-Content $progressFile -Value $content -Encoding UTF8
1068
+ Write-Log "Updated PROGRESS.md" "INFO"
1069
+ }
1070
+
1071
+ function Add-PathLocks {
1072
+ param(
1073
+ [string]$AgentId,
1074
+ [string]$TaskId,
1075
+ [string[]]$Paths
1076
+ )
1077
+
1078
+ $lockState = Get-LockState
1079
+ $locks = [System.Collections.ArrayList]@($lockState.locks)
1080
+
1081
+ foreach ($path in $Paths) {
1082
+ $locks.Add(@{
1083
+ path = $path
1084
+ agent_id = $AgentId
1085
+ task_id = $TaskId
1086
+ lock_type = if ($path.EndsWith("/")) { "directory" } else { "file" }
1087
+ acquired_at = (Get-Date -Format "o")
1088
+ }) | Out-Null
1089
+ }
1090
+
1091
+ $lockState.locks = $locks
1092
+ Save-LockState $lockState
1093
+
1094
+ Write-Log "Acquired locks for $AgentId on $($Paths.Count) paths"
1095
+ }
1096
+
1097
+ function Remove-AgentLocks {
1098
+ param([string]$AgentId)
1099
+
1100
+ $lockState = Get-LockState
1101
+ $newLocks = $lockState.locks | Where-Object { $_.agent_id -ne $AgentId }
1102
+ $lockState.locks = @($newLocks)
1103
+ Save-LockState $lockState
1104
+
1105
+ Write-Log "Released locks for $AgentId"
1106
+ }
1107
+
1108
+ # ============================================================
1109
+ # 狀態一致性檢查與清理(啟動時執行)
1110
+ # ============================================================
1111
+
1112
+ function Clear-StaleState {
1113
+ # CRITICAL: Clear all stale state from previous sessions at startup
1114
+ # This function runs BEFORE consistency check to ensure clean slate
1115
+ #
1116
+ # Why: When scheduler restarts, any existing locks/agents from previous
1117
+ # crashed sessions are invalid and must be cleared immediately
1118
+
1119
+ Write-Log "Cleaning stale state from previous sessions..." "INFO"
1120
+ $cleaned = @()
1121
+
1122
+ # 1. Clear ALL locks - new scheduler session means fresh state
1123
+ try {
1124
+ $lockState = Get-LockState
1125
+ if ($lockState.locks -and $lockState.locks.Count -gt 0) {
1126
+ $lockCount = $lockState.locks.Count
1127
+ $lockDetails = @($lockState.locks | ForEach-Object { "$($_.task_id):$($_.path)" })
1128
+ Write-Log "Found $lockCount stale lock(s) from previous session:" "WARN"
1129
+ foreach ($detail in $lockDetails) {
1130
+ Write-Log " - $detail" "WARN"
1131
+ }
1132
+
1133
+ # Clear all locks
1134
+ Save-LockState @{ locks = @() }
1135
+ $cleaned += "Cleared $lockCount stale lock(s)"
1136
+ }
1137
+ } catch {
1138
+ Write-Log "Error clearing locks: $_" "ERROR"
1139
+ # Force reset
1140
+ try {
1141
+ Save-LockState @{ locks = @() }
1142
+ $cleaned += "Force reset LOCK_STATE.json"
1143
+ } catch { }
1144
+ }
1145
+
1146
+ # 2. Clear stale agents from AGENT_STATE.json
1147
+ try {
1148
+ $agentState = Get-AgentState
1149
+ if ($agentState.agents -and $agentState.agents.Count -gt 0) {
1150
+ $agentCount = $agentState.agents.Count
1151
+ $agentDetails = @($agentState.agents | ForEach-Object { "$($_.id):$($_.current_task)" })
1152
+ Write-Log "Found $agentCount stale agent(s) from previous session:" "WARN"
1153
+ foreach ($detail in $agentDetails) {
1154
+ Write-Log " - $detail" "WARN"
1155
+ }
1156
+
1157
+ # Clear agents but preserve scheduler info
1158
+ $agentState.agents = @()
1159
+ Save-AgentState $agentState
1160
+ $cleaned += "Cleared $agentCount stale agent(s)"
1161
+ }
1162
+ } catch {
1163
+ Write-Log "Error clearing agents: $_" "ERROR"
1164
+ }
1165
+
1166
+ # 3. Reset in_progress tasks to pending (check branch progress first)
1167
+ try {
1168
+ $queue = Get-TaskQueue
1169
+ if ($queue -and $queue.tasks) {
1170
+ $inProgressTasks = @($queue.tasks | Where-Object { $_.status -eq "in_progress" })
1171
+ if ($inProgressTasks.Count -gt 0) {
1172
+ Write-Log "Found $($inProgressTasks.Count) in_progress task(s) to check:" "WARN"
1173
+ $resetCount = 0
1174
+ $resumeCount = 0
1175
+
1176
+ foreach ($task in $queue.tasks) {
1177
+ if ($task.status -eq "in_progress") {
1178
+ $hasProgress = $false
1179
+ $branchName = $task.branch
1180
+
1181
+ # Check if branch exists and has commits ahead of main
1182
+ if ($branchName) {
1183
+ try {
1184
+ Push-Location $WorkingDirectory
1185
+
1186
+ # Check if branch exists
1187
+ $null = git rev-parse --verify $branchName 2>$null
1188
+ if ($LASTEXITCODE -eq 0) {
1189
+ # Check for commits ahead of main/master
1190
+ $mainBranch = "main"
1191
+ $null = git rev-parse --verify main 2>$null
1192
+ if ($LASTEXITCODE -ne 0) { $mainBranch = "master" }
1193
+
1194
+ $commitsAhead = git rev-list --count "${mainBranch}..${branchName}" 2>$null
1195
+ if ($LASTEXITCODE -eq 0 -and [int]$commitsAhead -gt 0) {
1196
+ $hasProgress = $true
1197
+ Write-Log " - $($task.id): $($task.title) - branch has $commitsAhead commit(s), will RESUME" "SUCCESS"
1198
+ }
1199
+ }
1200
+
1201
+ Pop-Location
1202
+ } catch {
1203
+ Pop-Location
1204
+ }
1205
+ }
1206
+
1207
+ # Reset task to pending
1208
+ $task.status = "pending"
1209
+
1210
+ # Clear stale assignment info
1211
+ if ($task.PSObject.Properties["assigned_agent"]) {
1212
+ $task.PSObject.Properties.Remove("assigned_agent")
1213
+ }
1214
+ if ($task.PSObject.Properties["started_at"]) {
1215
+ $task.PSObject.Properties.Remove("started_at")
1216
+ }
1217
+
1218
+ if ($hasProgress) {
1219
+ # Keep branch for resume
1220
+ $task | Add-Member -NotePropertyName "resume_branch" -NotePropertyValue $branchName -Force
1221
+ if ($task.PSObject.Properties["branch"]) {
1222
+ $task.PSObject.Properties.Remove("branch")
1223
+ }
1224
+ $resumeCount++
1225
+ } else {
1226
+ # Full reset - no progress to preserve
1227
+ if (-not $hasProgress) {
1228
+ Write-Log " - $($task.id): $($task.title) - no progress, will RESET" "WARN"
1229
+ }
1230
+ if ($task.PSObject.Properties["branch"]) {
1231
+ $task.PSObject.Properties.Remove("branch")
1232
+ }
1233
+ if ($task.PSObject.Properties["resume_branch"]) {
1234
+ $task.PSObject.Properties.Remove("resume_branch")
1235
+ }
1236
+ $resetCount++
1237
+ }
1238
+ }
1239
+ }
1240
+
1241
+ # Save updated queue
1242
+ $queue.last_updated = (Get-Date -Format "o")
1243
+ $jsonContent = $queue | ConvertTo-Json -Depth 10
1244
+ Write-FileWithRetry -Path $TaskQueueFile -Content $jsonContent
1245
+
1246
+ if ($resetCount -gt 0) {
1247
+ $cleaned += "Reset $resetCount task(s) with no progress"
1248
+ }
1249
+ if ($resumeCount -gt 0) {
1250
+ $cleaned += "Will resume $resumeCount task(s) with branch progress"
1251
+ }
1252
+ }
1253
+ }
1254
+ } catch {
1255
+ Write-Log "Error resetting tasks: $_" "ERROR"
1256
+ }
1257
+
1258
+ # Report
1259
+ if ($cleaned.Count -gt 0) {
1260
+ Write-Log "Stale state cleanup completed:" "SUCCESS"
1261
+ foreach ($item in $cleaned) {
1262
+ Write-Log " - $item" "SUCCESS"
1263
+ }
1264
+ } else {
1265
+ Write-Log "No stale state found - clean startup" "SUCCESS"
1266
+ }
1267
+
1268
+ return $cleaned
1269
+ }
1270
+
1271
+ function Test-StateConsistency {
1272
+ # Comprehensive state consistency check at scheduler startup
1273
+ # This prevents issues from previous crashed sessions
1274
+ # NOTE: Clear-StaleState should be called BEFORE this function
1275
+
1276
+ $issues = @()
1277
+ $fixed = @()
1278
+
1279
+ # 1. Validate TASK_QUEUE.json
1280
+ Write-Log "Checking state consistency..." "INFO"
1281
+
1282
+ try {
1283
+ $queue = Get-TaskQueue
1284
+ if (-not $queue) {
1285
+ $issues += "TASK_QUEUE.json is missing or invalid"
1286
+ # Will be recreated by sync script
1287
+ }
1288
+ } catch {
1289
+ $issues += "TASK_QUEUE.json parse error: $_"
1290
+ }
1291
+
1292
+ # 2. Validate LOCK_STATE.json (should be empty after Clear-StaleState)
1293
+ try {
1294
+ $lockState = Get-LockState
1295
+ if ($lockState.locks -and $lockState.locks.Count -gt 0) {
1296
+ # This shouldn't happen after Clear-StaleState, but handle it
1297
+ $issues += "Unexpected locks remain after cleanup: $($lockState.locks.Count)"
1298
+ Save-LockState @{ locks = @() }
1299
+ $fixed += "Force cleared remaining locks"
1300
+ }
1301
+ } catch {
1302
+ $issues += "LOCK_STATE.json error: $_"
1303
+ try {
1304
+ Save-LockState @{ locks = @() }
1305
+ $fixed += "Reset LOCK_STATE.json"
1306
+ } catch { }
1307
+ }
1308
+
1309
+ # 3. Validate AGENT_STATE.json (should have no agents after Clear-StaleState)
1310
+ try {
1311
+ $agentState = Get-AgentState
1312
+ if ($agentState.agents -and $agentState.agents.Count -gt 0) {
1313
+ $issues += "Unexpected agents remain after cleanup: $($agentState.agents.Count)"
1314
+ }
1315
+ } catch {
1316
+ $issues += "AGENT_STATE.json error: $_"
1317
+ }
1318
+
1319
+ # 4. Check for tasks stuck in in_progress (should be none after Clear-StaleState)
1320
+ if ($queue -and $queue.tasks) {
1321
+ $stuckTasks = @($queue.tasks | Where-Object { $_.status -eq "in_progress" })
1322
+ if ($stuckTasks.Count -gt 0) {
1323
+ $issues += "Unexpected in_progress tasks after cleanup: $($stuckTasks.Count)"
1324
+ }
1325
+ }
1326
+
1327
+ # Report
1328
+ if ($issues.Count -gt 0) {
1329
+ Write-Log "State consistency issues found:" "WARN"
1330
+ foreach ($issue in $issues) {
1331
+ Write-Log " - $issue" "WARN"
1332
+ }
1333
+ }
1334
+
1335
+ if ($fixed.Count -gt 0) {
1336
+ Write-Log "Auto-fixed issues:" "SUCCESS"
1337
+ foreach ($fix in $fixed) {
1338
+ Write-Log " - $fix" "SUCCESS"
1339
+ }
1340
+ }
1341
+
1342
+ if ($issues.Count -eq 0 -and $fixed.Count -eq 0) {
1343
+ Write-Log "State consistency check passed" "SUCCESS"
1344
+ }
1345
+
1346
+ return @{
1347
+ Issues = $issues
1348
+ Fixed = $fixed
1349
+ HasCriticalIssues = ($issues | Where-Object { $_ -match "parse error|invalid" }).Count -gt 0
1350
+ }
1351
+ }
1352
+
1353
+ # ============================================================
1354
+ # 主循環
1355
+ # ============================================================
1356
+
1357
+ Clear-Host
1358
+ Write-Host ""
1359
+ Write-Host "============================================================" -ForegroundColor Cyan
1360
+ Write-Host " Multi-Agent Parallel Scheduler" -ForegroundColor Cyan
1361
+ Write-Host "============================================================" -ForegroundColor Cyan
1362
+ Write-Host " Max Parallel Agents: $MaxAgents"
1363
+ Write-Host " Poll Interval: ${PollIntervalSeconds}s"
1364
+ Write-Host " Task Timeout: ${TaskTimeoutMinutes}min"
1365
+ Write-Host " Working Dir: $WorkingDirectory"
1366
+ if ($DryRun) { Write-Host " Mode: DRY RUN" -ForegroundColor Yellow }
1367
+ if ($Standalone) { Write-Host " Mode: STANDALONE (heartbeat ignored)" -ForegroundColor Magenta }
1368
+ Write-Host "============================================================" -ForegroundColor Cyan
1369
+ Write-Host ""
1370
+
1371
+ # ============================================================
1372
+ # Prerequisite Checks
1373
+ # ============================================================
1374
+ Write-Host "[CHECK] Running prerequisite checks..." -ForegroundColor Yellow
1375
+ Write-DebugLog "Starting prerequisite checks"
1376
+
1377
+ # Check 1: Claude CLI installed
1378
+ try {
1379
+ $claudeCmd = Get-Command "claude" -ErrorAction SilentlyContinue
1380
+ $claudeInstalled = $null -ne $claudeCmd
1381
+ if ($claudeCmd) {
1382
+ Write-DebugLog "Claude CLI found at: $($claudeCmd.Source)"
1383
+ }
1384
+ } catch {
1385
+ $claudeInstalled = $false
1386
+ Write-DebugLog "Error checking Claude CLI: $_" "ERROR"
1387
+ }
1388
+
1389
+ if (-not $claudeInstalled) {
1390
+ $errorMsg = "Claude CLI not found in PATH"
1391
+ Write-DebugLog $errorMsg "ERROR"
1392
+ Write-Host ""
1393
+ Write-Host "============================================================" -ForegroundColor Red
1394
+ Write-Host " ERROR: Claude CLI not found!" -ForegroundColor Red
1395
+ Write-Host "============================================================" -ForegroundColor Red
1396
+ Write-Host ""
1397
+ Write-Host " Multi-Agent mode requires Claude CLI to be installed."
1398
+ Write-Host ""
1399
+ Write-Host " To install:" -ForegroundColor White
1400
+ Write-Host " npm install -g @anthropic-ai/claude-code" -ForegroundColor Cyan
1401
+ Write-Host ""
1402
+ Write-Host " After installation, restart your terminal and try again."
1403
+ Write-Host ""
1404
+ Write-Host "============================================================" -ForegroundColor Red
1405
+ Write-DebugLog "Exiting with code 1 - Claude CLI not found"
1406
+ exit 1
1407
+ }
1408
+ Write-Host " [OK] Claude CLI found" -ForegroundColor Green
1409
+ Write-DebugLog "Claude CLI check passed"
1410
+
1411
+ # Check 2: Working directory exists
1412
+ if (-not (Test-Path $WorkingDirectory)) {
1413
+ $errorMsg = "Working directory not found: $WorkingDirectory"
1414
+ Write-DebugLog $errorMsg "ERROR"
1415
+ Write-DebugLog "Exiting with code 1 - working directory not found"
1416
+ Write-Host " [ERROR] $errorMsg" -ForegroundColor Red
1417
+ exit 1
1418
+ }
1419
+ Write-Host " [OK] Working directory exists" -ForegroundColor Green
1420
+ Write-DebugLog "Working directory check passed: $WorkingDirectory"
1421
+
1422
+ # Check 3: dev/pipeline directory exists
1423
+ $pipelineDir = Join-Path $WorkingDirectory "dev\pipeline"
1424
+ if (-not (Test-Path $pipelineDir)) {
1425
+ $errorMsg = "Pipeline directory not found: $pipelineDir - Run Initialize Project first"
1426
+ Write-DebugLog $errorMsg "ERROR"
1427
+ Write-DebugLog "Exiting with code 1 - pipeline directory not found"
1428
+ Write-Host " [ERROR] $errorMsg" -ForegroundColor Red
1429
+ exit 1
1430
+ }
1431
+ Write-Host " [OK] Pipeline directory exists" -ForegroundColor Green
1432
+ Write-DebugLog "Pipeline directory check passed"
1433
+
1434
+ # Check 4: Task queue file exists (will be created by sync if needed)
1435
+ $TaskQueueFile = Join-Path $WorkingDirectory "dev\pipeline\TASK_QUEUE.json"
1436
+ if (-not (Test-Path $TaskQueueFile)) {
1437
+ Write-Host " [WARN] Task queue not found, will be created by sync" -ForegroundColor Yellow
1438
+ Write-DebugLog "Task queue file not found, will attempt sync" "WARN"
1439
+ }
1440
+
1441
+ Write-Host "[CHECK] Prerequisites passed" -ForegroundColor Green
1442
+ Write-DebugLog "All prerequisite checks passed"
1443
+ Write-Host ""
1444
+
1445
+ # Auto-sync task queue from markdown files
1446
+ Write-Host "[SYNC] Syncing task queue from markdown files..." -ForegroundColor Yellow
1447
+ Write-DebugLog "Starting task queue sync"
1448
+
1449
+ # Look for sync script in the same directory as this script (not in user's project)
1450
+ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
1451
+ Write-DebugLog "Script directory (raw): $scriptDir"
1452
+
1453
+ # Normalize path to handle \\?\ prefix from Tauri
1454
+ if ($scriptDir -and $scriptDir.StartsWith("\\?\")) {
1455
+ $scriptDir = $scriptDir.Substring(4)
1456
+ Write-DebugLog "Script directory (normalized): $scriptDir"
1457
+ }
1458
+
1459
+ $syncScript = Join-Path $scriptDir "sync_task_queue.ps1"
1460
+ Write-DebugLog "Looking for sync script at: $syncScript"
1461
+
1462
+ if (Test-Path $syncScript) {
1463
+ Write-DebugLog "Found sync script, executing..."
1464
+ try {
1465
+ & $syncScript -WorkingDirectory $WorkingDirectory -Quiet | Out-Null
1466
+ Write-Host "[SYNC] Task queue synced successfully" -ForegroundColor Green
1467
+ Write-DebugLog "Task queue sync completed successfully"
1468
+ } catch {
1469
+ Write-DebugLog "Sync script execution failed: $_" "ERROR"
1470
+ Write-Host "[SYNC] Sync failed: $_" -ForegroundColor Red
1471
+ }
1472
+ } else {
1473
+ Write-DebugLog "Sync script not found at primary location, trying fallback..."
1474
+ # Fallback: check in user's working directory (for manual runs)
1475
+ $syncScriptFallback = Join-Path $WorkingDirectory "dev\scripts\sync_task_queue.ps1"
1476
+ Write-DebugLog "Fallback sync script path: $syncScriptFallback"
1477
+
1478
+ if (Test-Path $syncScriptFallback) {
1479
+ Write-DebugLog "Found fallback sync script, executing..."
1480
+ try {
1481
+ & $syncScriptFallback -WorkingDirectory $WorkingDirectory -Quiet | Out-Null
1482
+ Write-Host "[SYNC] Task queue synced successfully" -ForegroundColor Green
1483
+ Write-DebugLog "Task queue sync completed successfully (fallback)"
1484
+ } catch {
1485
+ Write-DebugLog "Fallback sync script execution failed: $_" "ERROR"
1486
+ Write-Host "[SYNC] Sync failed: $_" -ForegroundColor Red
1487
+ }
1488
+ } else {
1489
+ Write-DebugLog "No sync script found at either location" "WARN"
1490
+ Write-Host "[SYNC] sync_task_queue.ps1 not found, skipping auto-sync" -ForegroundColor Yellow
1491
+ }
1492
+ }
1493
+ Write-Host ""
1494
+
1495
+ Write-Log "Multi-Agent Scheduler started (MaxAgents=$MaxAgents)"
1496
+
1497
+ # Backup task files before starting (prevent data loss)
1498
+ $backupResult = Backup-TaskFiles
1499
+ if ($backupResult) {
1500
+ Write-DebugLog "Task files backed up to: $backupResult"
1501
+ }
1502
+
1503
+ # CRITICAL: Clean stale state from previous sessions FIRST
1504
+ # This ensures locks, agents, and tasks are reset before any checks
1505
+ $null = Clear-StaleState
1506
+
1507
+ # Run state consistency check (should find no issues after cleanup)
1508
+ $consistencyResult = Test-StateConsistency
1509
+ if ($consistencyResult.HasCriticalIssues) {
1510
+ Write-Log "Critical state issues detected. Running sync to repair..." "WARN"
1511
+ if (Test-Path $syncScript) {
1512
+ & $syncScript -WorkingDirectory $WorkingDirectory -Force -Quiet | Out-Null
1513
+ Write-Log "Forced sync completed" "SUCCESS"
1514
+ }
1515
+ }
1516
+
1517
+ # NOTE: Removed orphan Claude process cleanup
1518
+ # The previous logic was dangerous - it would terminate ALL Claude processes
1519
+ # including user's active VSCode Claude extension sessions.
1520
+ # If cleanup is needed, user should manually terminate processes before starting scheduler.
1521
+ Write-Log "Scheduler ready (stale state cleaned)"
1522
+
1523
+ # Check if there are any pending tasks before starting the main loop
1524
+ $queue = Get-TaskQueue
1525
+ if ($queue) {
1526
+ $pendingTasks = @($queue.tasks | Where-Object { $_.status -eq "pending" })
1527
+ $completedTasks = @($queue.tasks | Where-Object { $_.status -eq "completed" })
1528
+ $totalTasks = $queue.tasks.Count
1529
+
1530
+ if ($pendingTasks.Count -eq 0) {
1531
+ Write-Host ""
1532
+ Write-Host "============================================================" -ForegroundColor Yellow
1533
+ Write-Host " No pending tasks found!" -ForegroundColor Yellow
1534
+ Write-Host "============================================================" -ForegroundColor Yellow
1535
+ Write-Host ""
1536
+ Write-Host " Task Queue Status:"
1537
+ Write-Host " Total: $totalTasks"
1538
+ Write-Host " Completed: $($completedTasks.Count)"
1539
+ Write-Host " Pending: 0"
1540
+ Write-Host ""
1541
+ Write-Host " To add tasks, you can:" -ForegroundColor White
1542
+ Write-Host " 1. Run /product to generate PRD and tasks" -ForegroundColor Cyan
1543
+ Write-Host " 2. Add task files to dev/pipeline/tasks/" -ForegroundColor Cyan
1544
+ Write-Host " 3. Use /dev to start development manually" -ForegroundColor Cyan
1545
+ Write-Host ""
1546
+ Write-Host "============================================================" -ForegroundColor Yellow
1547
+
1548
+ if ($SingleRun) {
1549
+ Stop-SchedulerCleanly -ExitCode 0 -Reason "No pending tasks (SingleRun mode)"
1550
+ }
1551
+
1552
+ Write-Log "No pending tasks. Scheduler will wait for new tasks..."
1553
+ } else {
1554
+ Write-Log "Found $($pendingTasks.Count) pending task(s) out of $totalTasks total" "SUCCESS"
1555
+ }
1556
+ } else {
1557
+ Write-Host ""
1558
+ Write-Host "============================================================" -ForegroundColor Red
1559
+ Write-Host " ERROR: Task queue not found or invalid!" -ForegroundColor Red
1560
+ Write-Host "============================================================" -ForegroundColor Red
1561
+ Write-Host ""
1562
+ Write-Host " Expected file: $TaskQueueFile"
1563
+ Write-Host ""
1564
+ Write-Host " Run Initialize Project first to create the task queue."
1565
+ Write-Host ""
1566
+ Write-DebugLog "Task queue not found or invalid: $TaskQueueFile" "ERROR"
1567
+ Write-DebugLog "Exiting with code 1 - task queue missing"
1568
+ exit 1
1569
+ }
1570
+
1571
+ # Clear stale agents and set scheduler status
1572
+ $schedulerState = @{
1573
+ agents = @()
1574
+ scheduler = @{
1575
+ status = "running"
1576
+ max_agents = $MaxAgents
1577
+ started_at = (Get-Date -Format "o")
1578
+ }
1579
+ last_updated = (Get-Date -Format "o")
1580
+ }
1581
+ Save-AgentState $schedulerState
1582
+ Write-Log "Scheduler status set to 'running' in AGENT_STATE.json"
1583
+
1584
+ # 活躍 Agent 追蹤
1585
+ $activeAgents = @{}
1586
+
1587
+ # 同步計數器(每 5 次輪詢同步一次新任務)
1588
+ $syncCounter = 0
1589
+ $SyncIntervalCycles = 5
1590
+
1591
+ # 保存狀態行位置(用於同一行更新,僅在交互式控制台)
1592
+ $statusLineRow = 0
1593
+ if ($script:IsInteractiveConsole) {
1594
+ $statusLineRow = [Console]::CursorTop
1595
+ }
1596
+
1597
+ Write-DebugLog "Entering main scheduler loop"
1598
+
1599
+ try {
1600
+ while ($true) {
1601
+ # Check if scheduler was stopped via API (AGENT_STATE.json status)
1602
+ $agentState = Get-AgentState
1603
+ if ($agentState.scheduler.status -eq "stopped") {
1604
+ Write-Log "Scheduler stop requested via API - shutting down..." "WARN"
1605
+
1606
+ # Terminate all active agents
1607
+ foreach ($agentId in $activeAgents.Keys) {
1608
+ $agent = $activeAgents[$agentId]
1609
+ if (-not $agent.Process.HasExited) {
1610
+ Write-Log "Terminating agent $agentId (stop requested, PID: $($agent.Process.Id))"
1611
+ Stop-ProcessTree -ProcessId $agent.Process.Id
1612
+ }
1613
+ Remove-AgentLocks -AgentId $agentId
1614
+ Remove-AgentFromState -AgentId $agentId
1615
+ }
1616
+ $activeAgents.Clear()
1617
+
1618
+ Write-Log "All agents terminated. Scheduler exiting." "SUCCESS"
1619
+ Stop-SchedulerCleanly -ExitCode 0 -Reason "Scheduler stop requested via API"
1620
+ }
1621
+
1622
+ # Check dashboard heartbeat - stop all agents if dashboard is closed
1623
+ $heartbeatStatus = Test-DashboardHeartbeat
1624
+ if (-not $heartbeatStatus.Alive) {
1625
+ Write-Log "Dashboard disconnected: $($heartbeatStatus.Reason)" "WARN"
1626
+ Write-Log "Stopping all agents due to dashboard disconnection..." "WARN"
1627
+
1628
+ # Terminate all active agents
1629
+ foreach ($agentId in $activeAgents.Keys) {
1630
+ $agent = $activeAgents[$agentId]
1631
+ if (-not $agent.Process.HasExited) {
1632
+ Write-Log "Terminating agent $agentId (dashboard closed, PID: $($agent.Process.Id))"
1633
+ # Use process tree termination to ensure all child processes are killed
1634
+ Stop-ProcessTree -ProcessId $agent.Process.Id
1635
+ }
1636
+ Remove-AgentLocks -AgentId $agentId
1637
+ Remove-AgentFromState -AgentId $agentId
1638
+
1639
+ # Reset task to pending for retry
1640
+ Update-TaskStatus -TaskId $agent.TaskId -Status "pending"
1641
+ }
1642
+ $activeAgents.Clear()
1643
+
1644
+ # If dashboard explicitly closed, exit immediately
1645
+ if ($heartbeatStatus.Reason -match "closing") {
1646
+ Write-Log "Dashboard explicitly closed. Scheduler exiting." "SUCCESS"
1647
+ Stop-SchedulerCleanly -ExitCode 0 -Reason "Dashboard explicitly closed"
1648
+ }
1649
+
1650
+ # Wait for dashboard to reconnect (max 60 seconds)
1651
+ $reconnectTimeout = 60
1652
+ $waited = 0
1653
+ Write-Log "Waiting up to ${reconnectTimeout}s for dashboard to reconnect..." "INFO"
1654
+ while (-not (Test-DashboardHeartbeat).Alive -and $waited -lt $reconnectTimeout) {
1655
+ Start-Sleep -Seconds 5
1656
+ $waited += 5
1657
+ Write-Host "." -NoNewline
1658
+ }
1659
+ Write-Host ""
1660
+
1661
+ if (-not (Test-DashboardHeartbeat).Alive) {
1662
+ Write-Log "Dashboard did not reconnect within ${reconnectTimeout}s. Scheduler exiting." "WARN"
1663
+ Stop-SchedulerCleanly -ExitCode 0 -Reason "Dashboard did not reconnect within timeout"
1664
+ }
1665
+
1666
+ Write-Log "Dashboard reconnected! Resuming scheduler..." "SUCCESS"
1667
+ }
1668
+
1669
+ # 定期同步新任務(每 5 個輪詢週期,或 Scheduler 空閒時)
1670
+ $syncCounter++
1671
+ $shouldSync = ($syncCounter -ge $SyncIntervalCycles) -or ($activeAgents.Count -eq 0)
1672
+ if ($shouldSync) {
1673
+ $syncCounter = 0
1674
+ if (Test-Path $syncScript) {
1675
+ & $syncScript -WorkingDirectory $WorkingDirectory -Quiet | Out-Null
1676
+ # 不輸出日誌,避免洗版
1677
+ }
1678
+ }
1679
+
1680
+ # 檢查已完成的 Agent
1681
+ $completedAgents = @()
1682
+ foreach ($agentId in $activeAgents.Keys) {
1683
+ $agent = $activeAgents[$agentId]
1684
+
1685
+ if ($agent.Process.HasExited) {
1686
+ $exitCode = $agent.Process.ExitCode
1687
+ $durationMin = [math]::Round(((Get-Date) - $agent.StartTime).TotalMinutes, 1)
1688
+
1689
+ # Use improved success detection
1690
+ $successResult = Test-AgentSuccess -ExitCode $exitCode -LogFile $agent.LogFile -Branch $agent.Branch -TaskId $agent.TaskId
1691
+ $isSuccess = $successResult.Success
1692
+
1693
+ if ($isSuccess -and $successResult.Reason -ne "Exit code 0") {
1694
+ Write-Log "Success detected via: $($successResult.Reason)" "INFO"
1695
+ }
1696
+
1697
+ if ($isSuccess) {
1698
+ Write-Log "Agent $agentId completed task $($agent.TaskId) successfully ($durationMin min)" "SUCCESS"
1699
+
1700
+ # Get task title before updating status
1701
+ $queue = Get-TaskQueue
1702
+ $taskTitle = ""
1703
+ foreach ($t in $queue.tasks) {
1704
+ if ($t.id -eq $agent.TaskId) {
1705
+ $taskTitle = $t.title
1706
+ break
1707
+ }
1708
+ }
1709
+
1710
+ Update-TaskStatus -TaskId $agent.TaskId -Status "completed"
1711
+
1712
+ # Move task file to completed folder
1713
+ Move-TaskToCompleted -TaskId $agent.TaskId
1714
+
1715
+ # Update PROGRESS.md
1716
+ Update-ProgressFile -TaskId $agent.TaskId -TaskTitle $taskTitle -Action "Completed"
1717
+
1718
+ # Merge task branch to main
1719
+ try {
1720
+ Push-Location $WorkingDirectory
1721
+ git checkout main 2>$null
1722
+ git merge $agent.Branch -m "Merge $($agent.TaskId): Completed by $agentId" 2>$null
1723
+ git branch -d $agent.Branch 2>$null
1724
+ Pop-Location
1725
+ Write-Log "Merged branch $($agent.Branch) to main" "SUCCESS"
1726
+ } catch {
1727
+ Pop-Location
1728
+ Write-Log "Failed to merge branch: $_" "WARN"
1729
+ }
1730
+ } else {
1731
+ $exitCodeStr = if ($null -eq $exitCode -or $exitCode -eq "") { "null" } else { $exitCode }
1732
+ Write-Log "Agent $agentId failed task $($agent.TaskId) after $durationMin min (exit: $exitCodeStr)" "ERROR"
1733
+ Write-Log "Failure reason: $($successResult.Reason)" "ERROR"
1734
+
1735
+ # Read error log for debugging
1736
+ if ($agent.LogFile -and (Test-Path "$($agent.LogFile).err")) {
1737
+ $errContent = Get-Content "$($agent.LogFile).err" -Raw -ErrorAction SilentlyContinue
1738
+ if ($errContent -and $errContent.Trim().Length -gt 0) {
1739
+ Write-Log "Agent error: $($errContent.Substring(0, [Math]::Min(500, $errContent.Length)))" "ERROR"
1740
+ }
1741
+ }
1742
+
1743
+ # Check if there's partial work done (branch with commits) - mark for resume instead of full reset
1744
+ $hasPartialWork = $false
1745
+ if ($agent.Branch) {
1746
+ try {
1747
+ Push-Location $WorkingDirectory
1748
+ $null = git rev-parse --verify $agent.Branch 2>$null
1749
+ if ($LASTEXITCODE -eq 0) {
1750
+ $mainBranch = "main"
1751
+ $null = git rev-parse --verify main 2>$null
1752
+ if ($LASTEXITCODE -ne 0) { $mainBranch = "master" }
1753
+ $commitsAhead = git rev-list --count "${mainBranch}..$($agent.Branch)" 2>$null
1754
+ if ($LASTEXITCODE -eq 0 -and [int]$commitsAhead -gt 0) {
1755
+ $hasPartialWork = $true
1756
+ Write-Log "Branch $($agent.Branch) has $commitsAhead commit(s) - marking for resume" "INFO"
1757
+ }
1758
+ }
1759
+ Pop-Location
1760
+ } catch {
1761
+ Pop-Location
1762
+ }
1763
+ }
1764
+
1765
+ # Reset task to pending for retry, potentially with resume info
1766
+ if ($hasPartialWork) {
1767
+ $queue = Get-TaskQueue
1768
+ if ($queue) {
1769
+ foreach ($t in $queue.tasks) {
1770
+ if ($t.id -eq $agent.TaskId) {
1771
+ $t.status = "pending"
1772
+ $t | Add-Member -NotePropertyName "resume_branch" -NotePropertyValue $agent.Branch -Force
1773
+ if ($t.PSObject.Properties["assigned_agent"]) {
1774
+ $t.PSObject.Properties.Remove("assigned_agent")
1775
+ }
1776
+ break
1777
+ }
1778
+ }
1779
+ $queue.last_updated = (Get-Date -Format "o")
1780
+ $jsonContent = $queue | ConvertTo-Json -Depth 10
1781
+ Write-FileWithRetry -Path $TaskQueueFile -Content $jsonContent
1782
+ }
1783
+ } else {
1784
+ Update-TaskStatus -TaskId $agent.TaskId -Status "pending"
1785
+ }
1786
+ }
1787
+
1788
+ # 釋放鎖
1789
+ Remove-AgentLocks -AgentId $agentId
1790
+ $completedAgents += $agentId
1791
+ } else {
1792
+ # 檢查超時
1793
+ $elapsed = ((Get-Date) - $agent.StartTime).TotalMinutes
1794
+ if ($elapsed -gt $TaskTimeoutMinutes) {
1795
+ Write-Log "Agent $agentId timed out on task $($agent.TaskId) (PID: $($agent.Process.Id))" "WARN"
1796
+ # Use process tree termination to ensure all child processes are killed
1797
+ Stop-ProcessTree -ProcessId $agent.Process.Id
1798
+ Remove-AgentLocks -AgentId $agentId
1799
+ $completedAgents += $agentId
1800
+ }
1801
+ }
1802
+ }
1803
+
1804
+ # 清理已完成的 Agent
1805
+ foreach ($agentId in $completedAgents) {
1806
+ $activeAgents.Remove($agentId)
1807
+ # 從 AGENT_STATE.json 移除 (for dashboard)
1808
+ Remove-AgentFromState -AgentId $agentId
1809
+ }
1810
+
1811
+ # 檢查是否可以啟動新 Agent
1812
+ $availableSlots = $MaxAgents - $activeAgents.Count
1813
+
1814
+ if ($availableSlots -gt 0) {
1815
+ $queue = Get-TaskQueue
1816
+
1817
+ if ($queue) {
1818
+ $readyTasks = Get-ReadyTasks -Queue $queue
1819
+ $lockState = Get-LockState
1820
+
1821
+ # Only log when there are ready tasks to potentially launch
1822
+ if ($readyTasks.Count -gt 0) {
1823
+ Write-Host "" # New line before log message
1824
+ Write-Log "Found $($readyTasks.Count) ready task(s), checking availability..."
1825
+ }
1826
+
1827
+ foreach ($task in $readyTasks) {
1828
+ if ($availableSlots -le 0) { break }
1829
+
1830
+ # 跳過已經在執行的任務
1831
+ $alreadyRunning = $false
1832
+ foreach ($agent in $activeAgents.Values) {
1833
+ if ($agent.TaskId -eq $task.id) {
1834
+ $alreadyRunning = $true
1835
+ break
1836
+ }
1837
+ }
1838
+ if ($alreadyRunning) { continue }
1839
+
1840
+ # 取得任務 scope
1841
+ $scopePaths = @()
1842
+ # 優先使用 scope_paths(新格式)
1843
+ if ($task.scope_paths) {
1844
+ $scopePaths += $task.scope_paths
1845
+ }
1846
+ # 向下相容:檢查舊格式 scope.paths / scope.files
1847
+ elseif ($task.scope) {
1848
+ if ($task.scope.paths) { $scopePaths += $task.scope.paths }
1849
+ if ($task.scope.files) { $scopePaths += $task.scope.files }
1850
+ }
1851
+
1852
+ # 如果沒有定義 scope,使用預設路徑
1853
+ if ($scopePaths.Count -eq 0) {
1854
+ $scopePaths = @("src/")
1855
+ }
1856
+
1857
+ # 檢查路徑衝突
1858
+ $conflict = Test-PathConflict -RequestedPaths $scopePaths -LockState $lockState
1859
+
1860
+ if ($conflict.Conflict) {
1861
+ Write-Log "Task $($task.id) conflicts with $($conflict.HeldBy) on $($conflict.ConflictingPath)" "WARN"
1862
+ continue
1863
+ }
1864
+
1865
+ # Determine agent type (smart dispatch)
1866
+ $explicitType = $null
1867
+ if ($task.agent_type) {
1868
+ $explicitType = $task.agent_type
1869
+ }
1870
+ $agentType = Get-AgentType -TaskId $task.id -TaskTitle $task.title -ScopePaths $scopePaths -ExplicitType $explicitType
1871
+
1872
+ # 啟動 Agent
1873
+ $agentId = New-AgentId
1874
+ $agentInfo = Start-AgentWorker -AgentId $agentId -TaskId $task.id -TaskTitle $task.title -ScopePaths $scopePaths -AgentType $agentType
1875
+
1876
+ if ($agentInfo -and $agentInfo -is [hashtable]) {
1877
+ try {
1878
+ # Store agent type in agent info for later reference
1879
+ $agentInfo.AgentType = $agentType
1880
+
1881
+ # 加入鎖
1882
+ Add-PathLocks -AgentId $agentId -TaskId $task.id -Paths $scopePaths
1883
+
1884
+ # 更新任務狀態
1885
+ Update-TaskStatus -TaskId $task.id -Status "in_progress" -AgentId $agentId -Branch $agentInfo.Branch
1886
+
1887
+ # 更新 AGENT_STATE.json (for dashboard) with agent type
1888
+ Add-AgentToState -AgentId $agentId -TaskId $task.id -TaskTitle $task.title -Branch $agentInfo.Branch -Status "working" -AgentType $agentType
1889
+
1890
+ # 追蹤 Agent (CRITICAL: must happen for cleanup to work)
1891
+ $activeAgents[$agentId] = $agentInfo
1892
+ $availableSlots--
1893
+
1894
+ $displayName = Get-AgentDisplayName -AgentType $agentType
1895
+ Write-Log "Launched $displayName ($agentId) for $($task.id): $($task.title)"
1896
+ } catch {
1897
+ Write-Log "Error setting up agent $agentId for task $($task.id): $_" "ERROR"
1898
+ # Agent process was started but setup failed - must still track it for cleanup
1899
+ if ($agentInfo.Process -and -not $agentInfo.Process.HasExited) {
1900
+ $activeAgents[$agentId] = $agentInfo
1901
+ Write-Log "Agent $agentId still tracked for cleanup despite setup error" "WARN"
1902
+ }
1903
+ # Release any locks that were acquired
1904
+ Remove-AgentLocks -AgentId $agentId
1905
+ }
1906
+ }
1907
+ }
1908
+ }
1909
+ }
1910
+
1911
+ # 顯示狀態 (更新同一行,避免洗版)
1912
+ $timestamp = Get-Date -Format "HH:mm:ss"
1913
+ $statusLine = "[$timestamp] Agents: $($activeAgents.Count)/$MaxAgents"
1914
+ if ($activeAgents.Count -gt 0) {
1915
+ $agentInfo = @()
1916
+ foreach ($agent in $activeAgents.Values) {
1917
+ $elapsedMin = [math]::Round(((Get-Date) - $agent.StartTime).TotalMinutes, 1)
1918
+ $agentInfo += "$($agent.TaskId)($elapsedMin min)"
1919
+ }
1920
+ $statusLine += " | " + ($agentInfo -join ", ")
1921
+ } else {
1922
+ $statusLine += " | Waiting for tasks..."
1923
+ }
1924
+ # 顯示狀態(交互式用同行更新,非交互式用普通輸出)
1925
+ if ($script:IsInteractiveConsole) {
1926
+ [Console]::SetCursorPosition(0, $statusLineRow)
1927
+ [Console]::ForegroundColor = [ConsoleColor]::Cyan
1928
+ [Console]::Write($statusLine.PadRight(100))
1929
+ [Console]::ResetColor()
1930
+ } else {
1931
+ Write-Host $statusLine -ForegroundColor Cyan
1932
+ }
1933
+
1934
+ if ($SingleRun -and $activeAgents.Count -eq 0) {
1935
+ Write-Log "Single run completed, no active agents"
1936
+ break
1937
+ }
1938
+
1939
+ # 等待下一個輪詢週期
1940
+ Start-Sleep -Seconds $PollIntervalSeconds
1941
+ }
1942
+ } catch {
1943
+ $errorMsg = "FATAL ERROR in main loop: $_"
1944
+ Write-DebugLog $errorMsg "ERROR"
1945
+ Write-DebugLog "Stack trace: $($_.ScriptStackTrace)" "ERROR"
1946
+ Write-Log $errorMsg "ERROR"
1947
+ throw
1948
+ } finally {
1949
+ # 清理:終止所有 Agent 並釋放鎖
1950
+ Write-Log "Shutting down scheduler..."
1951
+ Write-DebugLog "Entering finally block - cleanup starting"
1952
+
1953
+ foreach ($agentId in $activeAgents.Keys) {
1954
+ $agent = $activeAgents[$agentId]
1955
+ if (-not $agent.Process.HasExited) {
1956
+ Write-Log "Terminating agent $agentId (PID: $($agent.Process.Id))"
1957
+ Write-DebugLog "Terminating agent $agentId (PID: $($agent.Process.Id))"
1958
+ # Use process tree termination to ensure all child processes are killed
1959
+ Stop-ProcessTree -ProcessId $agent.Process.Id
1960
+ }
1961
+ Remove-AgentLocks -AgentId $agentId
1962
+ }
1963
+
1964
+ # 清理 AGENT_STATE.json - 移除所有 agents 並設置 scheduler 為 stopped
1965
+ $agentState = @{
1966
+ agents = @()
1967
+ scheduler = @{
1968
+ status = "stopped"
1969
+ stopped_at = (Get-Date -Format "o")
1970
+ }
1971
+ last_updated = (Get-Date -Format "o")
1972
+ }
1973
+ Save-AgentState $agentState
1974
+ Write-Log "Cleared AGENT_STATE.json and set scheduler to 'stopped'"
1975
+ Write-DebugLog "Cleanup completed, scheduler stopped"
1976
+
1977
+ Write-Log "Scheduler stopped"
1978
+ }