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.
- package/core/.agents/skills/typesafe-ai/LICENSE +21 -21
- package/core/.agents/skills/typesafe-ai/SKILL.md +149 -149
- package/core/.claude/commands/Ansuz.md +19 -30
- package/core/.claude/hooks/_ansuz-conversation.js +1 -0
- package/core/.claude/hooks/_ansuz-conversation.test.js +6 -0
- package/core/.claude/hooks/_run-user-prompt.js +0 -4
- package/core/.claude/hooks/_run-user-prompt.test.js +2 -3
- package/core/.claude/hooks/session-init.ps1 +49 -49
- package/core/.claude/settings.json +0 -37
- package/core/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -26
- package/core/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -97
- package/core/.claude/skills/ui-ux-pro-max/data/icons.csv +101 -101
- package/core/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -31
- package/core/.claude/skills/ui-ux-pro-max/data/products.csv +96 -96
- package/core/.claude/skills/ui-ux-pro-max/data/react-performance.csv +45 -45
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -54
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -53
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -56
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -53
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -53
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -51
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -59
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -52
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -54
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -61
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -54
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -51
- package/core/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -50
- package/core/.claude/skills/ui-ux-pro-max/data/styles.csv +68 -68
- package/core/.claude/skills/ui-ux-pro-max/data/typography.csv +57 -57
- package/core/.claude/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -101
- package/core/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +99 -99
- package/core/.claude/skills/ui-ux-pro-max/data/web-interface.csv +31 -31
- package/core/.claude/skills/ui-ux-pro-max/scripts/core.py +253 -253
- package/core/.claude/skills/ui-ux-pro-max/scripts/design_system.py +1067 -1067
- package/core/.claude/skills/ui-ux-pro-max/scripts/search.py +106 -106
- package/core/.collaboration/core/task-api.js +3 -8
- package/core/.formation/tests/hello.txt +2 -2
- package/core/.formation/tools/prompt.js +58 -5
- package/core/.formation/tools/prompt.test.js +29 -0
- package/core/.kenaz/claude-hooks-manifest.json +16 -16
- package/core/.kenaz/knowledge/ladybug_2c51da1e_shadow_file_frame_group.patch +163 -163
- package/core/.kenaz/knowledge/ladybug_30cf85690e_shadow_page_read_during_checkpoint.patch +393 -393
- package/core/.kenaz/knowledge/ladybug_7a17858d3a_node_group_delete_lock.patch +102 -102
- package/core/.kenaz/knowledge/ladybug_926a0ba1_column_checkpoint.patch +412 -412
- package/core/.kenaz/knowledge/ladybug_a346c4c4b6_pk_index_header_pages.patch +144 -144
- package/core/.kenaz/knowledge/ladybug_ba5f38815b_csr_node_group.patch +86 -86
- package/core/.kenaz/muninn_episode_write.ps1 +17 -17
- package/core/.kenaz/specialist/field_report_watermark.json +1 -1
- package/core/.kenaz/specialist/memory_guard_fired.json +1 -1
- package/core/.kenaz/specialist/memory_guard_trigger_cache.json +1 -1
- package/core/.kenaz/specialist/t1777_probe.txt +1 -1
- package/core/.matrix/index.matrix +5 -5
- package/core/.specialist/Ansuz/RULES.md +6 -10
- package/core/.specialist/tools/__tests__/task-2218-boot-session-id.test.js +17 -1
- package/core/.specialist/tools/__tests__/task-2222-session-identity-interleave.test.js +17 -1
- package/core/.specialist/tools/__tests__/task-2254-handoff-archive-contract.test.js +6 -5
- package/core/.specialist/tools/ansuz-boot-fast.js +44 -100
- package/core/.specialist/tools/ansuz-boot-fast.test.js +45 -1
- package/core/KENAZ_CORE_VERSION +1 -1
- package/core/dev/ANSUZ_STARTUP_INJECTION_REVIEW.md +355 -355
- package/core/dev/JEV_INTEGRATION_PLAN.md +189 -189
- package/core/dev/design/matrix_format_EXAMPLE_memory.matrix +13 -13
- package/core/dev/design/matrix_format_EXAMPLE_project_status.matrix +9 -9
- package/core/dev/design/matrix_format_EXAMPLE_tasks.matrix +14 -14
- package/core/dev/design/matrix_v2_EXAMPLE.matrix +67 -67
- package/core/dev/jev-eval/ACTIVE_CORRECTION.md +52 -52
- package/core/dev/jev-eval/CONTEXT_REVIEW.md +31 -31
- package/core/dev/jev-eval/EVALUATION_STATUS.md +39 -39
- package/core/dev/jev-eval/HISTORICAL_CORPUS.md +33 -33
- package/core/dev/jev-eval/README.md +99 -99
- package/core/dev/jev-eval/THALAMUS_RANKING.md +58 -58
- package/core/dev/jev-eval/reports/annotation-pilot-2026-09-20.json +32 -32
- package/core/dev/jev-eval/reports/context-review-2026-09-20.json +48 -48
- package/core/dev/jev-eval/reports/history-corpus-2026-09-20.json +52 -52
- package/core/dev/pipeline/SELF_ENHANCEMENT_STATUS.json.bak +47 -47
- package/core/dev/pipeline/tasks/.gitkeep +1 -1
- package/core/dev/pipeline/test_results/TASK_TEST_PARALLEL_002_result.txt +27 -27
- package/core/dev/plans/ANSUZ_SLIM_PLAN.md +68 -0
- package/core/dev/scripts/init_git.sh +0 -0
- package/core/dev/scripts/init_project.sh +0 -0
- package/core/dev/scripts/legacy/dev_agent_scheduler.ps1.bak +536 -536
- package/core/dev/scripts/legacy/multi_agent_scheduler.ps1.bak +1978 -1978
- package/core/dev/scripts/matrix/src/cli/commands/list-tags.ts.wip +140 -140
- package/core/dev/scripts/matrix/src/cli/commands/manage-tags.ts.wip +173 -173
- package/core/dev/scripts/matrix/src/cli/commands/stats.ts.wip +168 -168
- package/core/dev/scripts/matrix/src/cli/commands/tokens.ts.wip +126 -126
- package/core/dev/scripts/multi_agent_scheduler.sh +0 -0
- package/core/dev/scripts/run_once.sh +0 -0
- package/core/dev/scripts/setup_mcp.sh +0 -0
- package/core/dev/scripts/setup_rag.sh +0 -0
- package/core/dev/scripts/start_dev_agent.sh +0 -0
- package/core/dev/scripts/start_multi_agent.sh +0 -0
- package/core/dev/scripts/sync_task_queue.sh +0 -0
- package/core/k-cli/dist/alaya/run.js +91 -0
- package/core/k-cli/dist/core/output.js +24 -0
- package/core/k-cli/dist/core/process.js +38 -0
- package/core/k-cli/dist/core/project.js +80 -0
- package/core/k-cli/dist/core/spawn.js +268 -0
- package/core/k-cli/dist/core/types.js +2 -0
- package/core/k-cli/dist/dashboard/update.js +53 -0
- package/core/k-cli/dist/dashboard/version.js +20 -0
- package/core/k-cli/dist/dispatch/run.js +241 -0
- package/core/k-cli/dist/dispatch/status.js +103 -0
- package/core/k-cli/dist/index.js +283 -0
- package/core/k-cli/dist/project/initialize.js +301 -0
- package/core/k-cli/dist/project/install.js +89 -0
- package/core/k-cli/dist/project/status.js +138 -0
- package/core/k-cli/dist/specialist/list.js +100 -0
- package/core/k-cli/dist/specialist/run.js +93 -0
- package/core/k-cli/dist/task/create.js +194 -0
- package/core/k-cli/dist/task/delete.js +98 -0
- package/core/k-cli/dist/task/get.js +55 -0
- package/core/k-cli/dist/task/list.js +58 -0
- package/core/k-cli/dist/task/submit.js +156 -0
- package/core/k-cli/dist/ygg/run.js +51 -0
- package/core/k-cli/node_modules/.bin/yaml +16 -0
- package/core/k-cli/node_modules/.bin/yaml.cmd +17 -0
- package/core/k-cli/node_modules/.bin/yaml.ps1 +28 -0
- package/core/k-cli/node_modules/.package-lock.json +23 -0
- package/core/k-cli/node_modules/yaml/LICENSE +13 -0
- package/core/k-cli/node_modules/yaml/README.md +172 -0
- package/core/k-cli/node_modules/yaml/bin.mjs +11 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/compose-collection.js +88 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/compose-doc.js +43 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/compose-node.js +109 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/compose-scalar.js +86 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/composer.js +217 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-block-map.js +115 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-block-seq.js +49 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-end.js +37 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-flow-collection.js +207 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +223 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/resolve-props.js +146 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/util-contains-newline.js +34 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/util-empty-scalar-position.js +26 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
- package/core/k-cli/node_modules/yaml/browser/dist/compose/util-map-includes.js +13 -0
- package/core/k-cli/node_modules/yaml/browser/dist/doc/Document.js +335 -0
- package/core/k-cli/node_modules/yaml/browser/dist/doc/anchors.js +71 -0
- package/core/k-cli/node_modules/yaml/browser/dist/doc/applyReviver.js +55 -0
- package/core/k-cli/node_modules/yaml/browser/dist/doc/createNode.js +88 -0
- package/core/k-cli/node_modules/yaml/browser/dist/doc/directives.js +176 -0
- package/core/k-cli/node_modules/yaml/browser/dist/errors.js +57 -0
- package/core/k-cli/node_modules/yaml/browser/dist/index.js +17 -0
- package/core/k-cli/node_modules/yaml/browser/dist/log.js +11 -0
- package/core/k-cli/node_modules/yaml/browser/dist/nodes/Alias.js +114 -0
- package/core/k-cli/node_modules/yaml/browser/dist/nodes/Collection.js +147 -0
- package/core/k-cli/node_modules/yaml/browser/dist/nodes/Node.js +38 -0
- package/core/k-cli/node_modules/yaml/browser/dist/nodes/Pair.js +36 -0
- package/core/k-cli/node_modules/yaml/browser/dist/nodes/Scalar.js +24 -0
- package/core/k-cli/node_modules/yaml/browser/dist/nodes/YAMLMap.js +144 -0
- package/core/k-cli/node_modules/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
- package/core/k-cli/node_modules/yaml/browser/dist/nodes/addPairToJSMap.js +63 -0
- package/core/k-cli/node_modules/yaml/browser/dist/nodes/identity.js +36 -0
- package/core/k-cli/node_modules/yaml/browser/dist/nodes/toJS.js +37 -0
- package/core/k-cli/node_modules/yaml/browser/dist/parse/cst-scalar.js +214 -0
- package/core/k-cli/node_modules/yaml/browser/dist/parse/cst-stringify.js +61 -0
- package/core/k-cli/node_modules/yaml/browser/dist/parse/cst-visit.js +97 -0
- package/core/k-cli/node_modules/yaml/browser/dist/parse/cst.js +98 -0
- package/core/k-cli/node_modules/yaml/browser/dist/parse/lexer.js +717 -0
- package/core/k-cli/node_modules/yaml/browser/dist/parse/line-counter.js +39 -0
- package/core/k-cli/node_modules/yaml/browser/dist/parse/parser.js +967 -0
- package/core/k-cli/node_modules/yaml/browser/dist/public-api.js +102 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/Schema.js +37 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/common/map.js +17 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/common/null.js +15 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/common/seq.js +17 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/common/string.js +14 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/core/bool.js +19 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/core/float.js +43 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/core/int.js +38 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/core/schema.js +23 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/json/schema.js +62 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/tags.js +96 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/binary.js +58 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/merge.js +64 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/schema.js +39 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
- package/core/k-cli/node_modules/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
- package/core/k-cli/node_modules/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
- package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringify.js +129 -0
- package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyCollection.js +153 -0
- package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyComment.js +20 -0
- package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
- package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyNumber.js +24 -0
- package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyPair.js +150 -0
- package/core/k-cli/node_modules/yaml/browser/dist/stringify/stringifyString.js +336 -0
- package/core/k-cli/node_modules/yaml/browser/dist/util.js +11 -0
- package/core/k-cli/node_modules/yaml/browser/dist/visit.js +233 -0
- package/core/k-cli/node_modules/yaml/browser/index.js +5 -0
- package/core/k-cli/node_modules/yaml/browser/package.json +3 -0
- package/core/k-cli/node_modules/yaml/dist/cli.d.ts +8 -0
- package/core/k-cli/node_modules/yaml/dist/cli.mjs +201 -0
- package/core/k-cli/node_modules/yaml/dist/compose/compose-collection.d.ts +11 -0
- package/core/k-cli/node_modules/yaml/dist/compose/compose-collection.js +90 -0
- package/core/k-cli/node_modules/yaml/dist/compose/compose-doc.d.ts +7 -0
- package/core/k-cli/node_modules/yaml/dist/compose/compose-doc.js +45 -0
- package/core/k-cli/node_modules/yaml/dist/compose/compose-node.d.ts +29 -0
- package/core/k-cli/node_modules/yaml/dist/compose/compose-node.js +112 -0
- package/core/k-cli/node_modules/yaml/dist/compose/compose-scalar.d.ts +5 -0
- package/core/k-cli/node_modules/yaml/dist/compose/compose-scalar.js +88 -0
- package/core/k-cli/node_modules/yaml/dist/compose/composer.d.ts +63 -0
- package/core/k-cli/node_modules/yaml/dist/compose/composer.js +222 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-map.d.ts +6 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-map.js +117 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-scalar.js +200 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-block-seq.js +51 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-end.d.ts +6 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-end.js +39 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-flow-collection.js +209 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-flow-scalar.js +225 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-props.d.ts +23 -0
- package/core/k-cli/node_modules/yaml/dist/compose/resolve-props.js +148 -0
- package/core/k-cli/node_modules/yaml/dist/compose/util-contains-newline.d.ts +2 -0
- package/core/k-cli/node_modules/yaml/dist/compose/util-contains-newline.js +36 -0
- package/core/k-cli/node_modules/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
- package/core/k-cli/node_modules/yaml/dist/compose/util-empty-scalar-position.js +28 -0
- package/core/k-cli/node_modules/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
- package/core/k-cli/node_modules/yaml/dist/compose/util-flow-indent-check.js +17 -0
- package/core/k-cli/node_modules/yaml/dist/compose/util-map-includes.d.ts +4 -0
- package/core/k-cli/node_modules/yaml/dist/compose/util-map-includes.js +15 -0
- package/core/k-cli/node_modules/yaml/dist/doc/Document.d.ts +141 -0
- package/core/k-cli/node_modules/yaml/dist/doc/Document.js +337 -0
- package/core/k-cli/node_modules/yaml/dist/doc/anchors.d.ts +24 -0
- package/core/k-cli/node_modules/yaml/dist/doc/anchors.js +76 -0
- package/core/k-cli/node_modules/yaml/dist/doc/applyReviver.d.ts +9 -0
- package/core/k-cli/node_modules/yaml/dist/doc/applyReviver.js +57 -0
- package/core/k-cli/node_modules/yaml/dist/doc/createNode.d.ts +17 -0
- package/core/k-cli/node_modules/yaml/dist/doc/createNode.js +90 -0
- package/core/k-cli/node_modules/yaml/dist/doc/directives.d.ts +49 -0
- package/core/k-cli/node_modules/yaml/dist/doc/directives.js +178 -0
- package/core/k-cli/node_modules/yaml/dist/errors.d.ts +21 -0
- package/core/k-cli/node_modules/yaml/dist/errors.js +62 -0
- package/core/k-cli/node_modules/yaml/dist/index.d.ts +25 -0
- package/core/k-cli/node_modules/yaml/dist/index.js +50 -0
- package/core/k-cli/node_modules/yaml/dist/log.d.ts +3 -0
- package/core/k-cli/node_modules/yaml/dist/log.js +19 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/Alias.d.ts +29 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/Alias.js +116 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/Collection.d.ts +73 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/Collection.js +151 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/Node.d.ts +53 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/Node.js +40 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/Pair.d.ts +22 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/Pair.js +39 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/Scalar.d.ts +43 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/Scalar.js +27 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/YAMLMap.d.ts +53 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/YAMLMap.js +147 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/YAMLSeq.js +115 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/addPairToJSMap.js +65 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/identity.d.ts +23 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/identity.js +53 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/toJS.d.ts +29 -0
- package/core/k-cli/node_modules/yaml/dist/nodes/toJS.js +39 -0
- package/core/k-cli/node_modules/yaml/dist/options.d.ts +350 -0
- package/core/k-cli/node_modules/yaml/dist/parse/cst-scalar.d.ts +64 -0
- package/core/k-cli/node_modules/yaml/dist/parse/cst-scalar.js +218 -0
- package/core/k-cli/node_modules/yaml/dist/parse/cst-stringify.d.ts +8 -0
- package/core/k-cli/node_modules/yaml/dist/parse/cst-stringify.js +63 -0
- package/core/k-cli/node_modules/yaml/dist/parse/cst-visit.d.ts +39 -0
- package/core/k-cli/node_modules/yaml/dist/parse/cst-visit.js +99 -0
- package/core/k-cli/node_modules/yaml/dist/parse/cst.d.ts +109 -0
- package/core/k-cli/node_modules/yaml/dist/parse/cst.js +112 -0
- package/core/k-cli/node_modules/yaml/dist/parse/lexer.d.ts +87 -0
- package/core/k-cli/node_modules/yaml/dist/parse/lexer.js +719 -0
- package/core/k-cli/node_modules/yaml/dist/parse/line-counter.d.ts +22 -0
- package/core/k-cli/node_modules/yaml/dist/parse/line-counter.js +41 -0
- package/core/k-cli/node_modules/yaml/dist/parse/parser.d.ts +84 -0
- package/core/k-cli/node_modules/yaml/dist/parse/parser.js +972 -0
- package/core/k-cli/node_modules/yaml/dist/public-api.d.ts +44 -0
- package/core/k-cli/node_modules/yaml/dist/public-api.js +107 -0
- package/core/k-cli/node_modules/yaml/dist/schema/Schema.d.ts +17 -0
- package/core/k-cli/node_modules/yaml/dist/schema/Schema.js +39 -0
- package/core/k-cli/node_modules/yaml/dist/schema/common/map.d.ts +2 -0
- package/core/k-cli/node_modules/yaml/dist/schema/common/map.js +19 -0
- package/core/k-cli/node_modules/yaml/dist/schema/common/null.d.ts +4 -0
- package/core/k-cli/node_modules/yaml/dist/schema/common/null.js +17 -0
- package/core/k-cli/node_modules/yaml/dist/schema/common/seq.d.ts +2 -0
- package/core/k-cli/node_modules/yaml/dist/schema/common/seq.js +19 -0
- package/core/k-cli/node_modules/yaml/dist/schema/common/string.d.ts +2 -0
- package/core/k-cli/node_modules/yaml/dist/schema/common/string.js +16 -0
- package/core/k-cli/node_modules/yaml/dist/schema/core/bool.d.ts +4 -0
- package/core/k-cli/node_modules/yaml/dist/schema/core/bool.js +21 -0
- package/core/k-cli/node_modules/yaml/dist/schema/core/float.d.ts +4 -0
- package/core/k-cli/node_modules/yaml/dist/schema/core/float.js +47 -0
- package/core/k-cli/node_modules/yaml/dist/schema/core/int.d.ts +4 -0
- package/core/k-cli/node_modules/yaml/dist/schema/core/int.js +42 -0
- package/core/k-cli/node_modules/yaml/dist/schema/core/schema.d.ts +1 -0
- package/core/k-cli/node_modules/yaml/dist/schema/core/schema.js +25 -0
- package/core/k-cli/node_modules/yaml/dist/schema/json/schema.d.ts +2 -0
- package/core/k-cli/node_modules/yaml/dist/schema/json/schema.js +64 -0
- package/core/k-cli/node_modules/yaml/dist/schema/json-schema.d.ts +69 -0
- package/core/k-cli/node_modules/yaml/dist/schema/tags.d.ts +48 -0
- package/core/k-cli/node_modules/yaml/dist/schema/tags.js +99 -0
- package/core/k-cli/node_modules/yaml/dist/schema/types.d.ts +92 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/binary.js +70 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/bool.js +29 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/float.js +50 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/int.js +76 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/merge.d.ts +9 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/merge.js +68 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts +22 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/omap.js +77 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/schema.js +41 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/set.js +96 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
- package/core/k-cli/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/foldFlowLines.js +151 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringify.d.ts +21 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringify.js +132 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringifyCollection.js +155 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringifyComment.d.ts +10 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringifyComment.js +24 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringifyDocument.js +87 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringifyNumber.js +26 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringifyPair.d.ts +3 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringifyPair.js +152 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringifyString.d.ts +9 -0
- package/core/k-cli/node_modules/yaml/dist/stringify/stringifyString.js +338 -0
- package/core/k-cli/node_modules/yaml/dist/test-events.d.ts +4 -0
- package/core/k-cli/node_modules/yaml/dist/test-events.js +134 -0
- package/core/k-cli/node_modules/yaml/dist/util.d.ts +16 -0
- package/core/k-cli/node_modules/yaml/dist/util.js +28 -0
- package/core/k-cli/node_modules/yaml/dist/visit.d.ts +102 -0
- package/core/k-cli/node_modules/yaml/dist/visit.js +236 -0
- package/core/k-cli/node_modules/yaml/package.json +97 -0
- package/core/k-cli/node_modules/yaml/util.js +2 -0
- package/core/plugins/kenaz/commands/Ansuz.md +19 -30
- package/core/plugins/kenaz/hooks/hooks.json +1 -5
- package/core/plugins/kenaz/hooks/kenaz-hook.js +0 -4
- package/core/plugins/kenaz/scripts/kenaz.test.js +1 -1
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/charts.csv +26 -26
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/colors.csv +97 -97
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/icons.csv +101 -101
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/landing.csv +31 -31
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/products.csv +96 -96
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/react-performance.csv +45 -45
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -54
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -53
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -56
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -53
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -53
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -51
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -59
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -52
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/react.csv +54 -54
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -61
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -54
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -51
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -50
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/styles.csv +68 -68
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/typography.csv +57 -57
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -101
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/ux-guidelines.csv +99 -99
- package/core/plugins/kenaz/skills/ui-ux-pro-max/data/web-interface.csv +31 -31
- package/core/plugins/kenaz/skills/ui-ux-pro-max/scripts/core.py +253 -253
- package/core/plugins/kenaz/skills/ui-ux-pro-max/scripts/design_system.py +1067 -1067
- package/core/plugins/kenaz/skills/ui-ux-pro-max/scripts/search.py +106 -106
- package/lib/cli.js +36 -2
- package/lib/cli.test.js +24 -0
- package/lib/dashboard.js +10 -0
- package/package.json +6 -6
- package/core/.specialist/tools/__tests__/task-1863-boot-lease-reuse.test.js +0 -99
- 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
|
+
}
|