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