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