From 6cfa0c55d205eaeaf005f4d21f7b5e5cefa4448c Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 6 Oct 2026 20:02:40 +0200 Subject: [PATCH] refactor: drop 12 runtimes, keep Claude, Codex, OpenCode, Cursor, ZCode, Antigravity Removes kilo, kimi, kimi-code, copilot, windsurf, augment, trae, qwen, hermes, cline, codebuddy and pi end to end: capability descriptors, installer branches and converters (bin/install.js 14.9k -> 11.2k lines), TypeScript converters, hook surfaces and runtime homes, review lanes qwen/kimi-code, the two pi migrations, Kimi payload normalization in the hook guards, dead hostBehaviors vocabulary, launcher home probes, fixtures, runtime-specific tests and the prose that presented them as supported. Installer output for the six kept runtimes is byte-identical to before the prune. The Kimi tool-vocabulary tests in workflow-guard, read-guard and read-injection-scanner are left in place pending a decision. --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 - .github/ISSUE_TEMPLATE/feature_request.yml | 2 - .github/PULL_REQUEST_TEMPLATE/feature.md | 1 - .kilo/plugins/msd-core.js | 794 ---- CONTEXT.md | 2 +- README.ja-JP.md | 6 +- README.ko-KR.md | 6 +- README.md | 6 +- README.pt-BR.md | 6 +- README.zh-CN.md | 6 +- agents/msd-code-fixer.compact.md | 2 +- agents/msd-code-fixer.md | 2 +- agents/msd-debug-session-manager.compact.md | 2 +- agents/msd-debug-session-manager.md | 2 +- agents/msd-debugger.md | 2 +- agents/msd-eval-auditor.compact.md | 2 +- agents/msd-eval-auditor.md | 2 +- agents/msd-intel-updater.compact.md | 2 +- agents/msd-intel-updater.md | 2 +- agents/msd-phase-researcher.md | 2 +- agents/msd-project-researcher.compact.md | 2 +- agents/msd-project-researcher.md | 2 +- agents/msd-research-synthesizer.compact.md | 2 +- agents/msd-research-synthesizer.md | 2 +- agents/msd-ui-researcher.compact.md | 2 +- agents/msd-ui-researcher.md | 2 +- bin/install.js | 3999 +---------------- bin/msd-mcp-server.js | 4 +- capabilities/augment/capability.json | 114 - capabilities/cline/capability.json | 93 - capabilities/codebuddy/capability.json | 118 - capabilities/copilot/capability.json | 100 - capabilities/hermes/capability.json | 112 - capabilities/kilo/capability.json | 130 - capabilities/kimi-code/capability.json | 167 - capabilities/kimi/capability.json | 104 - capabilities/pi/capability.json | 70 - capabilities/qwen/capability.json | 150 - capabilities/trae/capability.json | 98 - capabilities/windsurf/capability.json | 92 - commands/msd/discuss-phase.md | 4 - commands/msd/execute-phase.md | 4 - commands/msd/graphify.md | 2 +- commands/msd/mvp-phase.md | 4 - commands/msd/new-project.md | 4 - commands/msd/onboard.md | 4 - commands/msd/plan-phase.md | 4 - commands/msd/plan-review-convergence.md | 8 +- commands/msd/review.md | 5 +- commands/msd/sketch.md | 4 - commands/msd/spec-phase.md | 4 - commands/msd/spike.md | 4 - docs/ARCHITECTURE.md | 68 +- docs/BETA.md | 2 +- docs/COMMANDS.md | 14 +- docs/CONFIGURATION.md | 38 +- docs/FEATURES.md | 90 +- docs/INVENTORY-MANIFEST.json | 5 - docs/INVENTORY.md | 13 +- docs/TESTING-SUITES.md | 4 +- docs/USER-GUIDE.md | 43 +- docs/explanation/context-engineering.md | 1 - .../embeddable-orchestration-system.md | 6 +- docs/features/cross-ai-peer-review.md | 4 +- ...ation-system-host-integration-interface.md | 2 +- .../hooks-declare-their-crash-policy.md | 4 +- docs/features/multi-runtime-support.md | 25 +- ...ntime-support-cline-codebuddy-qwen-code.md | 22 - ...runtime-support-trae-cline-augment-code.md | 14 - ...a-and-rate-limit-failure-classification.md | 2 +- docs/features/windsurf-runtime-support.md | 17 - .../add-or-update-a-host-integration.md | 18 +- docs/how-to/author-a-host-plugin.md | 4 +- docs/how-to/configure-model-profiles.md | 4 +- docs/how-to/connect-msd-mcp-server.md | 16 +- docs/how-to/declare-a-hook-crash-policy.md | 2 +- docs/how-to/develop-a-capability.md | 8 +- docs/how-to/fix-worktree-base-mismatch.md | 3 +- docs/how-to/install-on-your-runtime.md | 293 +- .../interpret-install-shadow-warnings.md | 2 +- ...te-an-install-test-to-the-executed-plan.md | 8 +- .../resolve-eslint-coverage-findings.md | 2 +- docs/how-to/set-up-cross-ai-review.md | 5 +- docs/how-to/update-msd.md | 2 +- docs/installer-migrations.md | 28 +- docs/ja-JP/ARCHITECTURE.md | 36 +- docs/ja-JP/COMMANDS.md | 8 +- docs/ja-JP/CONFIGURATION.md | 4 +- docs/ja-JP/FEATURES.md | 75 +- docs/ja-JP/INVENTORY.md | 2 +- docs/ja-JP/USER-GUIDE.md | 33 +- docs/ja-JP/how-to/configure-model-profiles.md | 4 +- docs/ja-JP/how-to/install-on-your-runtime.md | 116 +- docs/ja-JP/how-to/set-up-cross-ai-review.md | 3 +- docs/ko-KR/ARCHITECTURE.md | 43 +- docs/ko-KR/COMMANDS.md | 8 +- docs/ko-KR/CONFIGURATION.md | 4 +- docs/ko-KR/FEATURES.md | 50 +- docs/ko-KR/INVENTORY.md | 2 +- docs/ko-KR/USER-GUIDE.md | 33 +- docs/ko-KR/how-to/configure-model-profiles.md | 4 +- docs/ko-KR/how-to/install-on-your-runtime.md | 116 +- docs/ko-KR/how-to/set-up-cross-ai-review.md | 3 +- docs/manual-update.md | 4 - docs/migration/kimi-to-kimi-code.md | 97 - docs/pt-BR/ARCHITECTURE.md | 43 +- docs/pt-BR/COMMANDS.md | 8 +- docs/pt-BR/CONFIGURATION.md | 19 +- docs/pt-BR/FEATURES.md | 1 - docs/pt-BR/INVENTORY.md | 2 +- docs/pt-BR/USER-GUIDE.md | 33 +- docs/pt-BR/how-to/configure-model-profiles.md | 4 +- docs/pt-BR/how-to/install-on-your-runtime.md | 116 +- docs/pt-BR/how-to/set-up-cross-ai-review.md | 3 +- docs/reference/capability-manifest.md | 12 +- docs/reference/capability-matrix.md | 14 +- .../host-integration-capability-matrix.md | 513 +-- docs/reference/skill-mapping-matrix.md | 30 +- docs/reference/workflow-fragments.md | 4 +- docs/zh-CN/ARCHITECTURE.md | 43 +- docs/zh-CN/COMMANDS.md | 8 +- docs/zh-CN/CONFIGURATION.md | 19 +- docs/zh-CN/FEATURES.md | 74 +- docs/zh-CN/INVENTORY.md | 2 +- docs/zh-CN/USER-GUIDE.md | 33 +- docs/zh-CN/how-to/configure-model-profiles.md | 4 +- docs/zh-CN/how-to/install-on-your-runtime.md | 116 +- docs/zh-CN/how-to/set-up-cross-ai-review.md | 3 +- eslint.config.mjs | 4 - hooks/lib/cli-exit.js | 2 +- hooks/lib/git-cmd.js | 3 +- hooks/lib/git-probe.js | 4 +- hooks/lib/injection-patterns.js | 2 +- hooks/managed-hooks-registry.cjs | 2 - hooks/msd-agent-isolation-guard.js | 2 +- hooks/msd-check-update-worker.js | 8 +- hooks/msd-check-update.js | 4 +- hooks/msd-context-monitor.js | 2 +- hooks/msd-graphify-update.sh | 9 - hooks/msd-phase-boundary.sh | 14 +- hooks/msd-prompt-guard.js | 100 +- hooks/msd-read-guard.js | 96 +- hooks/msd-read-injection-scanner.js | 105 +- hooks/msd-secret-read-guard.js | 99 +- hooks/msd-windsurf-pre-command.js | 280 -- hooks/msd-windsurf-pre-write.js | 141 - hooks/msd-workflow-guard.js | 102 +- hooks/msd-worktree-path-guard.js | 121 +- hooks/msd-write-guard.js | 52 +- msd-core/bin/lib/capability-registry.cjs | 2736 ----------- msd-core/bin/lib/capability-validator.cjs | 41 +- msd-core/bin/lib/stale-bake-guard.cjs | 14 +- msd-core/bin/msd-tools.cjs | 27 +- msd-core/bin/shared/model-catalog.json | 60 - .../bin/shared/runtime-aliases.manifest.json | 50 - msd-core/references/model-profiles.md | 4 +- msd-core/references/msd-run-resolver.md | 2 +- msd-core/references/planning-config.md | 2 +- msd-core/references/runtime-aware-dispatch.md | 14 +- msd-core/templates/copilot-instructions.md | 7 - .../workflows/_runtime-launcher.snippet.sh | 2 +- msd-core/workflows/add-backlog.md | 2 +- msd-core/workflows/add-phase.md | 2 +- msd-core/workflows/add-tests.md | 2 +- msd-core/workflows/add-todo.md | 2 +- msd-core/workflows/ai-integration-phase.md | 2 +- msd-core/workflows/audit-fix.md | 4 +- msd-core/workflows/audit-milestone.md | 4 +- msd-core/workflows/audit-uat.md | 2 +- msd-core/workflows/autonomous.md | 2 +- msd-core/workflows/check-todos.md | 2 +- msd-core/workflows/cleanup.md | 2 +- msd-core/workflows/code-review-fix.md | 4 +- msd-core/workflows/code-review.md | 4 +- .../code-review/steps/structural-pre-pass.md | 2 +- msd-core/workflows/complete-milestone.md | 2 +- .../complete-milestone/detail/elaboration.md | 2 +- msd-core/workflows/debug.md | 4 +- msd-core/workflows/diagnose-issues.md | 4 +- .../workflows/discuss-phase-assumptions.md | 4 +- msd-core/workflows/discuss-phase.md | 2 +- .../workflows/discuss-phase/modes/advisor.md | 2 +- .../workflows/discuss-phase/modes/chain.md | 2 +- msd-core/workflows/do.md | 2 +- msd-core/workflows/docs-update.md | 6 +- msd-core/workflows/edit-phase.md | 2 +- msd-core/workflows/eval-review.md | 2 +- msd-core/workflows/execute-phase.md | 17 +- .../execute-phase/detail/elaboration.md | 2 +- .../steps/code-review-disposition.md | 2 +- .../steps/codebase-drift-gate.md | 4 +- .../steps/executor-isolation-dispatch.md | 2 +- .../steps/gap-closure-artifacts.md | 2 +- .../execute-phase/steps/partial-wave.md | 2 +- .../steps/per-plan-executor-routing.md | 4 +- .../steps/per-plan-worktree-gate.md | 2 +- .../execute-phase/steps/post-merge-gate.md | 2 +- .../execute-phase/steps/protected-branch.md | 2 +- .../steps/regression-gate-run.md | 2 +- .../steps/tdd-applicability-resolution.md | 2 +- .../steps/wave-post-gate-hooks.md | 2 +- .../steps/worktree-base-check.md | 4 +- msd-core/workflows/execute-plan.md | 2 +- msd-core/workflows/explore.md | 4 +- msd-core/workflows/extract-learnings.md | 2 +- msd-core/workflows/fast.md | 2 +- msd-core/workflows/forensics.md | 2 +- msd-core/workflows/graduation.md | 2 +- msd-core/workflows/health.md | 2 +- msd-core/workflows/help/modes/full.compact.md | 4 +- msd-core/workflows/help/modes/full.md | 4 +- msd-core/workflows/import.md | 4 +- msd-core/workflows/inbox.md | 2 +- msd-core/workflows/ingest-docs.md | 9 +- msd-core/workflows/insert-phase.md | 2 +- msd-core/workflows/list-seeds.md | 2 +- msd-core/workflows/list-workspaces.md | 2 +- msd-core/workflows/manager.md | 2 +- msd-core/workflows/map-codebase.md | 4 +- msd-core/workflows/milestone-summary.md | 2 +- msd-core/workflows/mvp-phase.md | 4 +- msd-core/workflows/new-milestone.md | 4 +- msd-core/workflows/new-project.md | 8 +- .../new-project/detail/elaboration.md | 2 +- .../new-project/steps/auto-mode-config.md | 2 +- msd-core/workflows/new-workspace.md | 2 +- msd-core/workflows/next.md | 2 +- msd-core/workflows/pause-work.md | 2 +- msd-core/workflows/plan-phase.md | 2 +- .../plan-phase/detail/elaboration.md | 2 +- .../steps/agent-recovery-and-timing.md | 1 + .../plan-phase/steps/chunked-planning-mode.md | 2 +- .../plan-phase/steps/prd-express-path.md | 2 +- .../steps/stall-detection-helpers.md | 2 +- msd-core/workflows/plan-review-convergence.md | 4 +- msd-core/workflows/plant-seed.md | 2 +- msd-core/workflows/pr-branch.md | 2 +- msd-core/workflows/profile-user.md | 2 +- msd-core/workflows/progress.md | 2 +- msd-core/workflows/quick-batch.md | 2 +- .../workflows/quick-batch/steps/batch-init.md | 2 +- .../workflows/quick-batch/steps/completion.md | 2 +- .../workflows/quick-batch/steps/merge-wave.md | 2 +- .../quick-batch/steps/planner-wave.md | 2 +- .../quick-batch/steps/research-phase.md | 2 +- .../quick-batch/steps/resume-mode.md | 2 +- .../quick-batch/steps/verification-wave.md | 2 +- .../quick-batch/steps/worktree-dispatch.md | 2 +- msd-core/workflows/quick.md | 2 +- .../quick/steps/quick-verification.md | 2 +- .../workflows/quick/steps/research-phase.md | 2 +- .../steps/worktree-pre-dispatch-commit.md | 2 +- msd-core/workflows/reapply-patches.md | 23 +- msd-core/workflows/remove-phase.md | 2 +- msd-core/workflows/remove-workspace.md | 2 +- msd-core/workflows/resume-project.md | 2 +- msd-core/workflows/review.md | 15 +- msd-core/workflows/scan.md | 4 +- msd-core/workflows/secure-phase.md | 4 +- msd-core/workflows/settings-advanced.md | 34 +- msd-core/workflows/settings-integrations.md | 10 +- msd-core/workflows/settings.md | 2 +- msd-core/workflows/ship.md | 4 +- msd-core/workflows/sketch-wrap-up.md | 2 +- msd-core/workflows/sketch.md | 2 +- msd-core/workflows/smart-entry.md | 2 +- msd-core/workflows/spec-phase.md | 2 +- msd-core/workflows/spike-wrap-up.md | 2 +- msd-core/workflows/spike.md | 2 +- msd-core/workflows/stats.md | 2 +- msd-core/workflows/sync-skills.md | 8 +- msd-core/workflows/thread.md | 2 +- msd-core/workflows/transition.md | 2 +- msd-core/workflows/ui-phase.md | 4 +- msd-core/workflows/ui-review.md | 4 +- msd-core/workflows/ultraplan-phase.md | 2 +- msd-core/workflows/undo.md | 2 +- msd-core/workflows/update.md | 40 +- msd-core/workflows/validate-phase.md | 4 +- msd-core/workflows/verify-work.md | 2 +- .../verify-work/detail/elaboration.md | 2 +- .../steps/automated-ui-verification.md | 2 +- .../verify-work/steps/mvp-uat-framing.md | 2 +- pi/msd.cjs | 444 -- scripts/build-hooks.js | 3 - scripts/docs-guard-registry.cjs | 1 - scripts/gen-context-index.cjs | 2 +- scripts/lib/cli-exit.cjs | 2 +- .../lib/macos-conformance-tier.generated.cjs | 3 - .../platform-conformance-tier.generated.cjs | 14 - .../lint-allow-test-rule-refs.allowlist.json | 6 - scripts/lint-allow-test-rule-refs.cjs | 14 +- ...llow-test-rule-refs.effective-ceiling.json | 2 +- ...ocs-guard-registration.exempt-baseline.cjs | 14 +- scripts/live-config-guard.cjs | 67 +- scripts/prompt-injection-scan.sh | 10 - scripts/release-tarball-smoke.cjs | 10 +- skills/msd-discuss-phase/SKILL.md | 4 - skills/msd-execute-phase/SKILL.md | 4 - skills/msd-graphify/SKILL.md | 2 +- skills/msd-mvp-phase/SKILL.md | 4 - skills/msd-new-project/SKILL.md | 4 - skills/msd-onboard/SKILL.md | 4 - skills/msd-plan-phase/SKILL.md | 4 - skills/msd-plan-review-convergence/SKILL.md | 8 +- skills/msd-review/SKILL.md | 5 +- skills/msd-sketch/SKILL.md | 4 - skills/msd-spec-phase/SKILL.md | 4 - skills/msd-spike/SKILL.md | 4 - src/adapter-imperative.cts | 2 +- src/agent-install-check.cts | 18 +- src/cli-exit.cts | 2 +- src/commonjs-marker.cts | 2 +- src/config-types.cts | 2 +- src/handshake-serialized.cts | 2 +- src/hook-bus.cts | 2 +- .../cline-sdk-binding.cts | 256 -- src/host-integration-sdk.cts | 2 +- src/host-integration.cts | 61 +- src/init.cts | 4 +- src/install-engine.cts | 272 +- src/install-model-override-resolver.cts | 6 +- src/install-profiles.cts | 19 +- src/installed-surface-resolver.cts | 2 +- src/installer-migration-report.cts | 3 - src/installer-migrations.cts | 2 +- .../000-first-time-baseline.cts | 9 - .../005-opencode-baseline-commands-dir.cts | 7 +- .../006-pi-extension-cjs-to-js.cts | 129 - ...007-retire-config-root-commonjs-marker.cts | 16 +- .../009-pi-retire-reserved-hooks-dir.cts | 241 - src/mcp-server.cts | 8 +- src/model-adapter.cts | 4 +- src/model-catalog.cts | 2 +- src/profile-output.cts | 13 +- src/review-lane-descriptor.cts | 88 +- src/review-lane-invocation.cts | 4 +- src/review-lane-runner.cts | 4 +- src/review-reviewer-selection.cts | 6 +- src/runtime-artifact-conversion.cts | 1886 +------- src/runtime-artifact-install-plan.cts | 4 +- src/runtime-artifact-layout.cts | 135 +- src/runtime-config-adapter-registry.cts | 17 +- src/runtime-homes.cts | 158 +- src/runtime-hooks-surface.cts | 852 +--- src/runtime-name-policy.cts | 87 +- src/runtime-slash.cts | 2 +- src/shell-command-projection.cts | 2 +- src/state-io.cts | 2 +- src/surface.cts | 89 +- src/update-context.cts | 10 - tests/adr-15-progress-converge.test.cjs | 1 - .../agent-descriptor-parity.install.test.cjs | 95 +- tests/agent-hint-routing-1689.test.cjs | 12 - tests/agent-install-check.test.cjs | 9 - tests/agent-install-validation.test.cjs | 147 +- tests/agent-tools.install.test.cjs | 118 +- tests/antigravity-repo-grounding.test.cjs | 5 - tests/augment-conversion.test.cjs | 171 - tests/augment-upgrades.test.cjs | 244 - tests/autonomous-converge.test.cjs | 1 - tests/capability-registry.test.cjs | 151 +- tests/check-update-config-dir.test.cjs | 4 +- tests/cline-beforetool-upgrade.test.cjs | 168 - tests/cline-dispatch-degradation.test.cjs | 101 - tests/cline-imperative-reference.test.cjs | 113 - tests/cline-install.test.cjs | 510 --- tests/cline-model-override-upgrade.test.cjs | 161 - tests/cline-support.test.cjs | 38 - tests/codebuddy-install.test.cjs | 270 -- tests/codebuddy-upgrades.test.cjs | 129 - tests/commonjs-marker.test.cjs | 101 +- tests/config-loader.test.cjs | 4 +- .../configured-entrypoint-validation.test.cjs | 131 +- tests/copilot-install.test.cjs | 1886 -------- tests/copilot-upgrades.test.cjs | 150 - tests/declarative-reference-augment.test.cjs | 255 -- .../declarative-reference-codebuddy.test.cjs | 164 - tests/declarative-reference-copilot.test.cjs | 168 - tests/declarative-reference-windsurf.test.cjs | 185 - tests/docs-hooks-table-parity.test.cjs | 27 +- tests/effort-surface-axis.test.cjs | 2 +- tests/emitted-attribution.test.cjs | 123 +- tests/emitted-caps-gate.test.cjs | 139 - tests/emitted-provenance.test.cjs | 98 +- tests/emitted-sizes.test.cjs | 4 +- tests/eslint-rules.test.cjs | 12 +- tests/execute-phase-active-flags.test.cjs | 6 +- tests/executed-plan.test.cjs | 104 +- ...feat-2483-review-claude-mds-guard.test.cjs | 8 +- .../compact-content-benchmark-baseline.json | 42 +- tests/fixtures/install-tree/antigravity.json | 3 - tests/fixtures/install-tree/augment.json | 784 ---- tests/fixtures/install-tree/claude-local.json | 3 - tests/fixtures/install-tree/claude.json | 3 - tests/fixtures/install-tree/cline.json | 672 --- tests/fixtures/install-tree/codebuddy.json | 783 ---- tests/fixtures/install-tree/codex.json | 1 - tests/fixtures/install-tree/copilot.json | 671 --- tests/fixtures/install-tree/cursor.json | 1 - tests/fixtures/install-tree/hermes.json | 713 --- tests/fixtures/install-tree/kilo.json | 786 ---- tests/fixtures/install-tree/kimi-code.json | 712 --- tests/fixtures/install-tree/kimi.json | 677 --- tests/fixtures/install-tree/opencode.json | 3 - tests/fixtures/install-tree/pi.json | 441 -- tests/fixtures/install-tree/qwen.json | 712 --- tests/fixtures/install-tree/trae.json | 670 --- tests/fixtures/install-tree/windsurf.json | 532 --- tests/fixtures/install-tree/zcode.json | 1 - tests/fixtures/pi-host-plugin.cjs | 70 - ...t-single-edit-propagation.install.test.cjs | 11 +- tests/gemini-runtime-removed.test.cjs | 12 +- tests/global-config-home-fragment.test.cjs | 16 +- tests/golden-parity-single-source.test.cjs | 35 +- tests/graphify-auto-update.slow.test.cjs | 72 - tests/helpers-process-isolation.test.cjs | 48 +- tests/helpers/emitted-provenance.cjs | 95 +- tests/helpers/install-shared.cjs | 17 +- tests/hermes-dispatch-upgrade.test.cjs | 74 - tests/hermes-imperative-reference.test.cjs | 132 - tests/hermes-skills-migration.test.cjs | 1282 ------ tests/hooks-commonjs-marker.test.cjs | 7 +- tests/hooks-crash-policy.test.cjs | 44 +- tests/hooks-opt-in.test.cjs | 89 - tests/host-integration-descriptors.test.cjs | 221 +- ...host-integration-validator-parity.test.cjs | 12 +- tests/host-integration.test.cjs | 96 +- tests/install-minimal-hooks.test.cjs | 456 +- tests/install-nested-layout.test.cjs | 20 +- tests/install-regressions.test.cjs | 265 +- tests/install-runtime-artifacts.test.cjs | 2424 +--------- tests/install-write-confinement.test.cjs | 31 +- tests/install.test.cjs | 1096 +---- tests/installed-surface-resolver.test.cjs | 34 - ...taller-migration-pi-extension-ext.test.cjs | 232 - ...ler-migration-pi-retire-hooks-dir.test.cjs | 328 -- tests/installer-migrations.test.cjs | 61 +- tests/kilo-imperative-reference.test.cjs | 186 - tests/kilo-upgrades.test.cjs | 434 -- tests/kimi-agent-converter.test.cjs | 590 --- .../kimi-guard-normalization-parity.test.cjs | 217 - tests/kimi-guard-typed-payload-reads.test.cjs | 160 - tests/kimi-imperative-reference.test.cjs | 155 - .../kimi-normalize-payload.property.test.cjs | 277 -- ...-payload-field-shadowing.security.test.cjs | 141 - tests/kimi-skill-converter.test.cjs | 94 - tests/kimi-tool-mapping.test.cjs | 107 - tests/kimi-upgrades.test.cjs | 796 ---- tests/kimi-variant-disambiguation.test.cjs | 95 - tests/live-config-guard.test.cjs | 84 +- tests/model-catalog-runtime-defaults.test.cjs | 4 +- tests/model-resolver.test.cjs | 132 +- tests/msd-agent-isolation-guard.test.cjs | 43 +- tests/msd-secret-read-guard.test.cjs | 24 +- tests/msd-write-guard.test.cjs | 80 +- tests/multi-runtime-select.test.cjs | 193 +- tests/opencode-command-dir-plural.test.cjs | 30 - tests/opencode-imperative-reference.test.cjs | 2 +- tests/path-replacement.test.cjs | 16 +- tests/pi-config-dir-env-override.test.cjs | 129 - tests/pi-extension-reachability.test.cjs | 173 - tests/pi-imperative-reference.test.cjs | 88 - tests/pi-upgrades.test.cjs | 265 -- tests/plan-review-convergence.test.cjs | 7 - tests/portability-vocab-drift.test.cjs | 5 +- tests/portable-node-runner.install.test.cjs | 28 - tests/profile-output.test.cjs | 22 - tests/qwen-imperative-reference.test.cjs | 127 - tests/qwen-skills-migration.test.cjs | 323 -- tests/qwen-upgrades.test.cjs | 152 - tests/repo-layout.test.cjs | 87 +- ...view-default-reviewers-resolution.test.cjs | 18 +- tests/review-lane-descriptor.test.cjs | 2 +- tests/review-lane-invocation.test.cjs | 14 +- tests/review-lane-runner.test.cjs | 53 +- tests/review-reviewer-selection.test.cjs | 44 +- tests/reviewer-docs-parity.test.cjs | 68 +- tests/reviewer-lane-declarations.test.cjs | 61 +- tests/runtime-artifact-install-plan.test.cjs | 4 - ...-artifact-layout-descriptor-drive.test.cjs | 177 +- ...-artifact-layout-install-profiles.test.cjs | 8 +- .../runtime-artifact-layout-surface.test.cjs | 136 +- ...e-artifact-layout-trigger-surface.test.cjs | 61 +- tests/runtime-artifact-layout.test.cjs | 342 +- tests/runtime-brand-swap-parity.test.cjs | 42 +- .../runtime-config-adapter-registry.test.cjs | 75 +- tests/runtime-converters.test.cjs | 306 +- tests/runtime-flags.test.cjs | 10 +- tests/runtime-homes-descriptor-drive.test.cjs | 558 +-- tests/runtime-homes.property.test.cjs | 6 +- tests/runtime-label-policy.test.cjs | 1 - tests/runtime-launcher-parity.test.cjs | 111 +- tests/runtime-name-policy.test.cjs | 198 +- ...ecurity-prompt-injection.security.test.cjs | 69 - tests/settings-integrations.test.cjs | 2 +- tests/shadow-report.test.cjs | 12 - tests/shared-hooks-dir-resolution.test.cjs | 168 +- tests/slash-command-namespace.test.cjs | 30 +- tests/slug-derivation-drift-guard.test.cjs | 5 +- tests/stale-bake-guard.test.cjs | 67 +- tests/trae-imperative-reference.test.cjs | 421 -- tests/trae-upgrades.test.cjs | 112 - tests/update-custom-backup.test.cjs | 71 +- tests/update-workflow.test.cjs | 5 +- tests/windsurf-conversion.test.cjs | 460 -- tests/windsurf-hooks-bridge.test.cjs | 402 -- tests/windsurf-install.test.cjs | 52 - tests/worktree-safety.test.cjs | 254 +- 509 files changed, 1869 insertions(+), 47737 deletions(-) delete mode 100644 .kilo/plugins/msd-core.js delete mode 100644 capabilities/augment/capability.json delete mode 100644 capabilities/cline/capability.json delete mode 100644 capabilities/codebuddy/capability.json delete mode 100644 capabilities/copilot/capability.json delete mode 100644 capabilities/hermes/capability.json delete mode 100644 capabilities/kilo/capability.json delete mode 100644 capabilities/kimi-code/capability.json delete mode 100644 capabilities/kimi/capability.json delete mode 100644 capabilities/pi/capability.json delete mode 100644 capabilities/qwen/capability.json delete mode 100644 capabilities/trae/capability.json delete mode 100644 capabilities/windsurf/capability.json delete mode 100644 docs/features/new-runtime-support-cline-codebuddy-qwen-code.md delete mode 100644 docs/features/new-runtime-support-trae-cline-augment-code.md delete mode 100644 docs/features/windsurf-runtime-support.md delete mode 100644 docs/migration/kimi-to-kimi-code.md delete mode 100644 hooks/msd-windsurf-pre-command.js delete mode 100644 hooks/msd-windsurf-pre-write.js delete mode 100644 msd-core/templates/copilot-instructions.md delete mode 100644 pi/msd.cjs delete mode 100644 src/host-integration-adapters/cline-sdk-binding.cts delete mode 100644 src/installer-migrations/006-pi-extension-cjs-to-js.cts delete mode 100644 src/installer-migrations/009-pi-retire-reserved-hooks-dir.cts delete mode 100644 tests/augment-conversion.test.cjs delete mode 100644 tests/augment-upgrades.test.cjs delete mode 100644 tests/cline-beforetool-upgrade.test.cjs delete mode 100644 tests/cline-dispatch-degradation.test.cjs delete mode 100644 tests/cline-imperative-reference.test.cjs delete mode 100644 tests/cline-install.test.cjs delete mode 100644 tests/cline-model-override-upgrade.test.cjs delete mode 100644 tests/cline-support.test.cjs delete mode 100644 tests/codebuddy-install.test.cjs delete mode 100644 tests/codebuddy-upgrades.test.cjs delete mode 100644 tests/copilot-install.test.cjs delete mode 100644 tests/copilot-upgrades.test.cjs delete mode 100644 tests/declarative-reference-augment.test.cjs delete mode 100644 tests/declarative-reference-codebuddy.test.cjs delete mode 100644 tests/declarative-reference-copilot.test.cjs delete mode 100644 tests/declarative-reference-windsurf.test.cjs delete mode 100644 tests/emitted-caps-gate.test.cjs delete mode 100644 tests/fixtures/install-tree/augment.json delete mode 100644 tests/fixtures/install-tree/cline.json delete mode 100644 tests/fixtures/install-tree/codebuddy.json delete mode 100644 tests/fixtures/install-tree/copilot.json delete mode 100644 tests/fixtures/install-tree/hermes.json delete mode 100644 tests/fixtures/install-tree/kilo.json delete mode 100644 tests/fixtures/install-tree/kimi-code.json delete mode 100644 tests/fixtures/install-tree/kimi.json delete mode 100644 tests/fixtures/install-tree/pi.json delete mode 100644 tests/fixtures/install-tree/qwen.json delete mode 100644 tests/fixtures/install-tree/trae.json delete mode 100644 tests/fixtures/install-tree/windsurf.json delete mode 100644 tests/fixtures/pi-host-plugin.cjs delete mode 100644 tests/hermes-dispatch-upgrade.test.cjs delete mode 100644 tests/hermes-imperative-reference.test.cjs delete mode 100644 tests/hermes-skills-migration.test.cjs delete mode 100644 tests/installer-migration-pi-extension-ext.test.cjs delete mode 100644 tests/installer-migration-pi-retire-hooks-dir.test.cjs delete mode 100644 tests/kilo-imperative-reference.test.cjs delete mode 100644 tests/kilo-upgrades.test.cjs delete mode 100644 tests/kimi-agent-converter.test.cjs delete mode 100644 tests/kimi-guard-normalization-parity.test.cjs delete mode 100644 tests/kimi-guard-typed-payload-reads.test.cjs delete mode 100644 tests/kimi-imperative-reference.test.cjs delete mode 100644 tests/kimi-normalize-payload.property.test.cjs delete mode 100644 tests/kimi-payload-field-shadowing.security.test.cjs delete mode 100644 tests/kimi-skill-converter.test.cjs delete mode 100644 tests/kimi-tool-mapping.test.cjs delete mode 100644 tests/kimi-upgrades.test.cjs delete mode 100644 tests/kimi-variant-disambiguation.test.cjs delete mode 100644 tests/pi-config-dir-env-override.test.cjs delete mode 100644 tests/pi-extension-reachability.test.cjs delete mode 100644 tests/pi-imperative-reference.test.cjs delete mode 100644 tests/pi-upgrades.test.cjs delete mode 100644 tests/qwen-imperative-reference.test.cjs delete mode 100644 tests/qwen-skills-migration.test.cjs delete mode 100644 tests/qwen-upgrades.test.cjs delete mode 100644 tests/trae-imperative-reference.test.cjs delete mode 100644 tests/trae-upgrades.test.cjs delete mode 100644 tests/windsurf-conversion.test.cjs delete mode 100644 tests/windsurf-hooks-bridge.test.cjs delete mode 100644 tests/windsurf-install.test.cjs diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f745971c7..3edca739d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -39,10 +39,8 @@ body: - Claude Code - OpenCode - Codex - - Copilot - Antigravity - Cursor - - Windsurf - ZCode (Z.ai) - Multiple (specify in description) validations: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index d553d709c..ca6e195fc 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -172,10 +172,8 @@ body: - label: Claude Code - label: OpenCode - label: Codex - - label: Copilot - label: Antigravity - label: Cursor - - label: Windsurf - label: All runtimes - type: textarea diff --git a/.github/PULL_REQUEST_TEMPLATE/feature.md b/.github/PULL_REQUEST_TEMPLATE/feature.md index 55018e73f..9f744c788 100644 --- a/.github/PULL_REQUEST_TEMPLATE/feature.md +++ b/.github/PULL_REQUEST_TEMPLATE/feature.md @@ -72,7 +72,6 @@ Closes # - [ ] Antigravity - [ ] OpenCode - [ ] Codex -- [ ] Copilot - [ ] Other: ___ - [ ] N/A — specify which runtimes are supported and why others are excluded diff --git a/.kilo/plugins/msd-core.js b/.kilo/plugins/msd-core.js deleted file mode 100644 index 7e91aa0e7..000000000 --- a/.kilo/plugins/msd-core.js +++ /dev/null @@ -1,794 +0,0 @@ -/** - * MSD plugin for OpenCode.ai (CommonJS) - * - * Architecture: SUBPROCESS REUSE. Instead of re-implementing hook logic inside - * the plugin, this file is a thin adapter that spawns the existing Claude Code - * hook scripts under hooks/ as child processes. The hooks speak a stable - * protocol (JSON on stdin, JSON + exit code on stdout); this adapter: - * 1. Translates OpenCode plugin events into Claude Code hook payloads - * 2. Spawns `node /.js` with the payload on stdin - * 3. Translates hook output back into OpenCode semantics - * - block → throw Error (OpenCode returns the error to the model) - * - advisory → output.metadata + console.error (best-effort surfacing) - * - * Namespace conversion (/msd:xxx → /msd-xxx) reuses scripts/fix-slash-commands.cjs - * via require(), keeping the single source of truth. - * - * ── Two distribution shapes, one adapter (issue #1914) ───────────────────── - * This single file serves both distribution paths, distinguished at load time - * by REPO_ROOT (path.resolve(__dirname, "../..")): - * - * • Option 1 — file copy (the supported MSD path). `bin/install.js` copies - * this file to /plugins/msd-core.js, so REPO_ROOT is the - * OpenCode config dir. MSD's own install already stages `hooks/*.js` and - * `msd-core/` there (ADR-857 skips hook *registration* for OpenCode, not the - * file copy), so the hook bridge and content rewriting resolve natively. - * Commands/agents/skills are ALREADY registered by MSD's native file copy in - * this mode, so the plugin's own config-hook registration is redundant and is - * SKIPPED (see IS_PACKAGE_TREE) to avoid double-registration. - * - * • Option 2 — package / git-spec. When loaded from the package tree (npm - * `main`, or an OpenCode git-spec install), REPO_ROOT is the package root and - * the source layout (commands/msd/, agents/, skills/) is present. Here the - * plugin IS the sole registrar, so it registers commands/agents/skills too. - * - * IS_PACKAGE_TREE keys off the presence of the SOURCE command layout - * (commands/msd/), which only exists in the package tree — never in an installed - * config dir (that uses the flattened command/ layout). The hook bridge and - * Read-time content rewriting run in BOTH modes; only the config-hook - * registration of commands/agents/skills is gated. - * - * Runtime-specific hooks are deliberately excluded: - * - msd-statusline.js / msd-update-banner.js (Claude Code statusline) - * - msd-cursor-*.js (Cursor-specific) - * - *.sh scripts (invoked directly by commands/agents, not hook events) - */ - -"use strict"; - -const path = require("path"); -const fs = require("fs"); -const os = require("os"); -const { spawnSync } = require("child_process"); - -// Resolve REPO_ROOT to the directory that actually holds the MSD payload -// (hooks/ + msd-core/). This must work across three physical layouts because a -// single adapter file serves both distribution shapes (see header): -// • package/git-spec tree: /.opencode/plugins/msd-core.js → -// • global file-copy: ~/.config/opencode/plugins/msd-core.js → ~/.config/opencode -// • local file-copy: /.opencode/plugins/msd-core.js → /.opencode -// A fixed "../.." only works for the first; the copied layouts sit one level -// shallower. Walking up to the first ancestor containing BOTH payload markers -// resolves all three deterministically. Falls back to the package-tree -// assumption ("../..") if no ancestor matches (keeps graceful degradation). -function resolveRepoRoot(startDir) { - let dir = startDir; - for (let i = 0; i < 6; i++) { - if ( - fs.existsSync(path.join(dir, "hooks")) && - fs.existsSync(path.join(dir, "msd-core")) - ) { - return dir; - } - const parent = path.dirname(dir); - if (parent === dir) break; // filesystem root - dir = parent; - } - // No ancestor carried both markers (broken/partial layout — the plugin can't - // function regardless). Fall back to the package-tree assumption ("../.."), - // matching the historical fixed-depth behavior and the .opencode/plugins/ - // source layout. - return path.resolve(startDir, "../.."); -} - -// CJS: __dirname is a global, no need to derive from import.meta.url -const REPO_ROOT = resolveRepoRoot(__dirname); -const HOOKS_DIR = path.join(REPO_ROOT, "hooks"); -const COMMANDS = path.join(REPO_ROOT, "commands", "msd"); -const AGENTS = path.join(REPO_ROOT, "agents"); -const SKILLS = path.join(REPO_ROOT, "skills"); -const MSD_CORE = path.join(REPO_ROOT, "msd-core"); - -// True only when loaded from the package/source tree (Option 2), detected by the -// presence of the SOURCE command layout (commands/msd/). In an installed OpenCode -// config dir (Option 1) this directory is absent — the flattened command/ layout -// is used instead — so the plugin skips its own command/agent/skill registration -// and lets MSD's native file copy own that surface (avoids double-registration). -const IS_PACKAGE_TREE = fs.existsSync(COMMANDS); - -// --------------------------------------------------------------------------- -// Namespace conversion — reuse the single source of truth -// --------------------------------------------------------------------------- - -let _cmdNames = null; -let _transformFn = null; - -/** - * Lazily load scripts/fix-slash-commands.cjs and cache the transform function - * + command name list. Returns null if the module is unavailable (the plugin - * still works, just without namespace conversion). - */ -function getNamespaceConverter() { - if (_transformFn) return _transformFn; - try { - const mod = require( - path.join(REPO_ROOT, "scripts", "fix-slash-commands.cjs"), - ); - _cmdNames = mod.readCmdNames(); - _transformFn = mod.transformContentToHyphen; - return _transformFn; - } catch { - return null; - } -} - -// --------------------------------------------------------------------------- -// Session state — tracked across plugin hook invocations -// --------------------------------------------------------------------------- - -let currentSessionId = null; -let currentCwd = process.cwd(); - -// --------------------------------------------------------------------------- -// Tool name / argument mapping (OpenCode ↔ Claude Code) -// --------------------------------------------------------------------------- - -const TOOL_NAME_MAP = { - read: "Read", - grep: "Grep", - write: "Write", - edit: "Edit", - apply_patch: "MultiEdit", - multi_edit: "MultiEdit", - bash: "Bash", - webfetch: "WebFetch", - web_search: "WebSearch", - websearch: "WebSearch", - task: "Task", - subagent: "Task", -}; - -function mapToolName(tool) { - if (!tool) return ""; - return TOOL_NAME_MAP[String(tool).toLowerCase()] || tool; -} - -// Build a Claude-style `tool_input` object from OpenCode's `output.args`. -function mapToolInput(args) { - const input = {}; - if (!args || typeof args !== "object") return input; - - // File-path keys (OpenCode uses filePath/path; Claude uses file_path) - const filePath = args.filePath || args.path || args.file_path; - if (filePath) input.file_path = filePath; - - // Content for Write - if (args.content !== undefined) input.content = args.content; - - // Edit patch fields - if (args.new_string !== undefined) input.new_string = args.new_string; - if (args.newString !== undefined) input.new_string = args.newString; - if (args.old_string !== undefined) input.old_string = args.old_string; - if (args.oldString !== undefined) input.old_string = args.oldString; - - // Bash command - if (args.command !== undefined) input.command = args.command; - - // Grep file filter (OpenCode uses include; Claude uses glob) - const glob = args.glob ?? args.include; - if (glob !== undefined) input.glob = glob; - - // Web - if (args.url !== undefined) input.url = args.url; - if (args.query !== undefined) input.query = args.query; - - return input; -} - -// --------------------------------------------------------------------------- -// Hook subprocess runner -// --------------------------------------------------------------------------- - -/** - * Spawn a Claude Code hook script and pipe a JSON payload to its stdin. - * - * Hooks follow the convention: - * - stdout: JSON object (decision/advisory) or empty - * - exit 0: allow (with optional advisory JSON on stdout) - * - exit 2: block (Claude convention; reason in stdout JSON) - * - any error: exit 0 silently (hooks swallow their own errors) - * - * @param {string} hookFile filename under hooks/, e.g. "msd-prompt-guard.js" - * @param {object} payload stdin JSON (hook_event_name, tool_name, ...) - * @param {object} [opts] - * @param {number} [opts.timeout=8000] spawn timeout in ms - * @param {string} [opts.cwd] working directory for the child - * @returns {{ stdout: string, exitCode: number, timedOut: boolean }} - */ -const warnedMissingHooks = new Set(); - -function runHook(hookFile, payload, opts = {}) { - const hookPath = path.join(HOOKS_DIR, hookFile); - if (!fs.existsSync(hookPath)) { - // A missing guard script means the guard is silently NOT enforced — the - // exact failure mode of #2305 (plugin staged, hooks bundle not). Never - // break the tool call (the adapter's design contract), but never be - // silent about it either: warn loudly, once per hook file. - if (!warnedMissingHooks.has(hookFile)) { - warnedMissingHooks.add(hookFile); - console.error( - `[msd-core] hook script missing: ${hookPath} — ${hookFile} is NOT ` + - "enforced. The MSD install may be incomplete; reinstall (or run " + - "/msd-update) to restage the hooks/ bundle.", - ); - } - return { stdout: "", exitCode: 0, timedOut: false }; - } - const timeout = opts.timeout ?? 8000; - let result; - try { - result = spawnSync(process.execPath, [hookPath], { - input: JSON.stringify(payload), - encoding: "utf8", - timeout, - cwd: opts.cwd || currentCwd, - windowsHide: true, - }); - } catch { - // Spawn failure — never break the tool call - return { stdout: "", exitCode: 0, timedOut: false }; - } - - const stdout = (result.stdout || "").trim(); - const exitCode = result.status == null ? 0 : result.status; - return { stdout, exitCode, timedOut: result.signal === "SIGTERM" }; -} - -/** - * In-process check for whether context-usage warnings are disabled in project - * config. Mirrors the exact semantics of the same check inside - * hooks/msd-context-monitor.js (introduced by #1073): an explicit - * `config.hooks.context_warnings === false` disables them; a missing or - * unparseable .planning/config.json keeps them enabled (the default). - * - * #2697: hoisting this check in-process lets the adapter SKIP the context-monitor - * spawn entirely when the user has opted out, instead of paying a full Node boot - * inside the child only to read the boolean and exit. Missing/unparseable config - * MUST behave identically to the hook (enabled) so the default path is unchanged. - * - * @param {string} cwd project working directory (the plugin's currentCwd) - * @returns {boolean} true when context warnings are explicitly disabled - */ -function contextWarningsDisabled(cwd) { - try { - const configPath = path.join(cwd, '.planning', 'config.json'); - const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); - return config.hooks?.context_warnings === false; - } catch { - // Missing or unparseable config → proceed with defaults (context warnings enabled). - return false; - } -} - -// --------------------------------------------------------------------------- -// Hook output translation → OpenCode semantics -// --------------------------------------------------------------------------- - -/** - * Parse a hook's stdout and apply its effect to the OpenCode output object. - * - * - Block → throw Error(parsed.reason) so OpenCode aborts the tool call - * - Advisory→ append to output.metadata._msdAdvisory[] and log to stderr - * - Silent → no-op - * - * @param {{ stdout: string, exitCode: number }} hookResult - * @param {object} [output] OpenCode mutable output object (optional) - */ -function handleHookResult(hookResult, output) { - const { stdout, exitCode } = hookResult; - if (!stdout && exitCode !== 2) return; // silent allow - - let parsed = null; - if (stdout) { - try { - parsed = JSON.parse(stdout); - } catch { - // Non-JSON stdout (e.g. a stray log) — treat exit 2 as hard block, else allow - } - } - - // Block: explicit decision OR Claude exit-code-2 convention - const isBlock = exitCode === 2 || (parsed && parsed.decision === "block"); - if (isBlock) { - const reason = - (parsed && parsed.reason) || "Blocked by MSD hook (no reason provided)."; - throw new Error(reason); - } - - // Advisory: inject additionalContext into metadata + log - const advisory = - parsed && - parsed.hookSpecificOutput && - parsed.hookSpecificOutput.additionalContext; - if (advisory) { - if (output) { - output.metadata = output.metadata || {}; - // Accumulate: a single tool call can run several advisory hooks in - // sequence (prompt guard, read guard, worktree guard, workflow guard). - // Storing a scalar would let a later advisory clobber an earlier one, so - // collect them all. - if (!Array.isArray(output.metadata._msdAdvisory)) { - output.metadata._msdAdvisory = []; - } - output.metadata._msdAdvisory.push(advisory); - } - // Best-effort visibility when metadata isn't surfaced to the model - console.error(advisory); - } -} - -// --------------------------------------------------------------------------- -// Frontmatter helpers (for config registration) -// --------------------------------------------------------------------------- - -function parseFrontmatter(content) { - const m = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); - if (!m) return { frontmatter: {}, body: content }; - const fm = {}; - for (const line of m[1].split("\n")) { - const i = line.indexOf(":"); - if (i > 0) { - let v = line.slice(i + 1).trim(); - if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1); - fm[line.slice(0, i).trim()] = v; - } - } - return { frontmatter: fm, body: m[2] }; -} - -// Rewrite @~/.claude/ includes to point at the repo root. -// Also applies /msd:xxx → /msd-xxx namespace conversion via the shared -// transform from scripts/fix-slash-commands.cjs (single source of truth). -function rewriteRefs(content) { - let out = content.replace(/@~\/\.claude\//g, `@${REPO_ROOT}/`); - const transform = getNamespaceConverter(); - if (transform && _cmdNames && _cmdNames.length) { - out = transform(out, _cmdNames); - } - return out; -} - -function loadDir(dir, keyFn, valFn) { - const result = {}; - if (!fs.existsSync(dir)) return result; - for (const f of fs.readdirSync(dir).filter((f) => f.endsWith(".md"))) { - const raw = fs.readFileSync(path.join(dir, f), "utf8"); - const { frontmatter, body } = parseFrontmatter(raw); - result[keyFn(f)] = valFn(body, frontmatter, f); - } - return result; -} - -// --------------------------------------------------------------------------- -// Runtime content transform — for Read tool results on MSD-managed files -// --------------------------------------------------------------------------- - -// Directories whose .md files may contain ~/.claude/ paths and msd: namespace -// refs. When the model reads these via the Read tool, we transparently rewrite -// both so OpenCode sees correct paths and hyphen-form command names. -const MSD_MANAGED_DIRS = [ - path.join(MSD_CORE, "workflows"), - path.join(MSD_CORE, "references"), - path.join(MSD_CORE, "templates"), - path.join(MSD_CORE, "contexts"), - COMMANDS, - AGENTS, - SKILLS, -]; - -function isMsdManagedFile(filePath) { - if (!filePath) return false; - const resolved = path.resolve(filePath); - return MSD_MANAGED_DIRS.some( - (dir) => resolved === dir || resolved.startsWith(dir + path.sep), - ); -} - -// Rewrite content for OpenCode consumption: -// 1. @-include paths: @~/.claude/ → @/ -// 2. plain-text paths: ~/.claude/msd-core/ → / -// 3. namespace: msd:xxx → msd-xxx (via fix-slash-commands.cjs) -function rewriteContent(content) { - let out = content; - out = out.replace(/@~\/\.claude\//g, `@${REPO_ROOT}/`); - out = out.replace(/~\/\.claude\/msd-core\//g, `${MSD_CORE}/`); - const transform = getNamespaceConverter(); - if (transform && _cmdNames && _cmdNames.length) { - out = transform(out, _cmdNames); - } - return out; -} - -// --------------------------------------------------------------------------- -// Skills cache — copy SKILL.md files with rewritten @-include paths -// --------------------------------------------------------------------------- -// -// OpenCode's skill loader reads SKILL.md files directly from disk and resolves -// @-includes internally — this bypasses our tool.execute hooks. To make -// @~/.claude/msd-core/... includes resolve, we copy all SKILL.md files to a -// cache directory with paths rewritten to the actual MSD_CORE location. -// -// Only used in package-tree mode (Option 2). In an installed OpenCode config -// dir (Option 1) skills are already staged + registered by MSD's native file -// copy, so we never register skills from the plugin (see IS_PACKAGE_TREE). - -const SKILLS_CACHE = path.join( - os.homedir(), - ".cache", - "opencode", - "msd-skills", -); - -function prepareSkillsCache() { - if (!fs.existsSync(SKILLS)) return null; - fs.mkdirSync(SKILLS_CACHE, { recursive: true }); - for (const dir of fs.readdirSync(SKILLS)) { - const srcFile = path.join(SKILLS, dir, "SKILL.md"); - if (!fs.existsSync(srcFile)) continue; - const raw = fs.readFileSync(srcFile, "utf8"); - // Rewrite @-include paths only; namespace conversion is handled at - // Read-time via tool.execute.after for workflow/reference files. - const rewritten = raw - .replace(/@~\/\.claude\/msd-core\//g, `@${MSD_CORE}/`) - .replace(/~\/\.claude\/msd-core\//g, `${MSD_CORE}/`); - const destDir = path.join(SKILLS_CACHE, dir); - fs.mkdirSync(destDir, { recursive: true }); - fs.writeFileSync(path.join(destDir, "SKILL.md"), rewritten); - } - return SKILLS_CACHE; -} - -// =========================================================================== -// Plugin entry -// =========================================================================== - -const MsdCorePlugin = async ({ directory } = {}) => { - if (directory) currentCwd = directory; - - return { - // ── Config: register commands / agents / skills paths ────────────── - // Only in package-tree mode (Option 2). In an installed config dir - // (Option 1) MSD's native file copy already registered these, so the - // plugin stays out of registration to avoid double-registering. - config: async (config) => { - if (!IS_PACKAGE_TREE) return; - - // Commands (commands/msd/*.md → msd-) - config.command = config.command || {}; - const cmds = loadDir( - COMMANDS, - (f) => "msd-" + f.slice(0, -3), - (body, fm, name) => ({ - template: rewriteRefs(body.trim()), - description: fm.description || `MSD ${name.slice(0, -3)} command`, - }), - ); - for (const [k, v] of Object.entries(cmds)) { - if (!config.command[k]) config.command[k] = v; - } - - // Agents (agents/*.md) - config.agent = config.agent || {}; - const agents = loadDir( - AGENTS, - (f) => f.slice(0, -3), - (body, fm, name) => ({ - prompt: rewriteRefs(body.trim()), - description: fm.description || `MSD ${name.slice(0, -3)} agent`, - mode: fm.mode || "subagent", - }), - ); - for (const [k, v] of Object.entries(agents)) { - if (!config.agent[k]) config.agent[k] = v; - } - - // Skills — copy SKILL.md files to cache with rewritten @-include paths, - // then register the cache directory. OpenCode's skill loader reads - // SKILL.md from disk and resolves @-includes internally (bypassing our - // tool.execute hooks), so we must pre-process the files. - const skillsCache = prepareSkillsCache(); - config.skills = config.skills || {}; - config.skills.paths = config.skills.paths || []; - const skillsPath = skillsCache || SKILLS; - if (!config.skills.paths.includes(skillsPath)) { - config.skills.paths.push(skillsPath); - } - }, - - // ── shell.env ─────────────────────────────────────────────────────── - "shell.env": async (_input, output) => { - output.env = output.env || {}; - output.env.MSD_DIR = MSD_CORE; - }, - - // ── tool.execute.before — PreToolUse hooks ───────────────────────── - "tool.execute.before": async (input, output) => { - const claudeTool = mapToolName(input.tool); - const toolInput = mapToolInput(output.args || {}); - const cwd = currentCwd; - - // 0. Read path rewrite — redirect ~/.claude/msd-core/ to actual MSD_CORE - // so the model can read workflow/reference/template files that SKILL.md - // and command templates reference via the canonical Claude path. - if (claudeTool === "Read" && toolInput.file_path) { - const original = toolInput.file_path; - const rewritten = original - .replace(/^~\/\.claude\/msd-core\//, MSD_CORE + "/") - .replace(/(?:.*)\/\.claude\/msd-core\//, MSD_CORE + "/"); - if (rewritten !== original) { - const args = output.args || {}; - if (args.filePath) args.filePath = rewritten; - else if (args.path) args.path = rewritten; - else if (args.file_path) args.file_path = rewritten; - else args.filePath = rewritten; - } - } - - const basePayload = { - hook_event_name: "PreToolUse", - cwd, - }; - // NOTE: session_id intentionally omitted for PreToolUse hooks. - // msd-read-guard.js treats a non-empty session_id as a Claude Code - // session and skips its advisory. On OpenCode we WANT the advisory. - const prePayload = (overrides = {}) => ({ - ...basePayload, - tool_name: claudeTool, - tool_input: toolInput, - ...overrides, - }); - - const isWriteLike = ["Write", "Edit", "MultiEdit"].includes(claudeTool); - - // 1. msd-prompt-guard.js — injection scan on .planning/ writes - if (claudeTool === "Write" || claudeTool === "Edit") { - const r = runHook("msd-prompt-guard.js", prePayload()); - handleHookResult(r, output); - } - - // 2. msd-read-guard.js — read-before-edit advisory - if (claudeTool === "Write" || claudeTool === "Edit") { - const r = runHook("msd-read-guard.js", prePayload()); - handleHookResult(r, output); - } - - // 3. msd-worktree-path-guard.js — hard-block edits outside worktree - if (isWriteLike) { - const r = runHook("msd-worktree-path-guard.js", prePayload()); - handleHookResult(r, output); - } - - // 4. msd-write-guard.js — hard-block catastrophic shrink of curated - // .planning/ artifacts (ROADMAP.md, milestones/*-ROADMAP.md, STATE.md) - if (claudeTool === "Write") { - const r = runHook("msd-write-guard.js", prePayload()); - handleHookResult(r, output); - } - - // 5. msd-workflow-guard.js — workflow advisory + git-force-add block - // (covers Write/Edit/MultiEdit AND Bash force-add detection) - if (isWriteLike || claudeTool === "Bash") { - const r = runHook("msd-workflow-guard.js", prePayload()); - handleHookResult(r, output); - } - - // 6. msd-secret-read-guard.js — hard-block reads of .env / .env. / - // .secrets via Read (file_path), Grep (path or glob) and Bash (command) - if (["Read", "Grep", "Bash"].includes(claudeTool)) { - const r = runHook("msd-secret-read-guard.js", prePayload()); - handleHookResult(r, output); - } - }, - - // ── tool.execute.after — PostToolUse hooks ───────────────────────── - "tool.execute.after": async (input, output) => { - const claudeTool = mapToolName(input.tool); - // NOTE: In the `after` hook, `args` lives on `input` (not `output`). - // The `output` object only has { title, output, metadata }. - const toolInput = mapToolInput(input.args || {}); - const cwd = currentCwd; - - // MSD content transform — rewrite paths + namespace in Read results - // BEFORE injection scanning so the scanner sees the final content. - if ( - claudeTool === "Read" && - output.output && - isMsdManagedFile(toolInput.file_path) - ) { - const content = - typeof output.output === "string" - ? output.output - : String(output.output); - output.output = rewriteContent(content); - } - - // msd-read-injection-scanner.js — scan Read/WebFetch/WebSearch results - if ( - claudeTool === "Read" || - claudeTool === "WebFetch" || - claudeTool === "WebSearch" - ) { - const payload = { - hook_event_name: "PostToolUse", - tool_name: claudeTool, - tool_input: toolInput, - tool_response: output.output, - cwd, - }; - const r = runHook("msd-read-injection-scanner.js", payload); - handleHookResult(r, output); - return; - } - - // msd-context-monitor.js — context usage warnings (Bash/Edit/Write/Task/...) - // Only meaningful when a session_id is tracked (writes metrics sentinel). - // #2697: skip the subprocess spawn entirely when context warnings are - // explicitly disabled in project config — the hook would exit early anyway, - // so hoisting the check in-process avoids paying a Node boot per tool call. - // Missing/unparseable config = enabled (default), so the spawn still runs. - if (currentSessionId && !contextWarningsDisabled(cwd)) { - const payload = { - hook_event_name: "PostToolUse", - tool_name: claudeTool, - tool_input: toolInput, - session_id: currentSessionId, - cwd, - }; - const r = runHook("msd-context-monitor.js", payload); - handleHookResult(r, output); - } - }, - - // ── experimental.session.compacting — PreCompact ─────────────────── - "experimental.session.compacting": async (_input, output) => { - if (!currentSessionId) return; - const payload = { - hook_event_name: "PreCompact", - session_id: currentSessionId, - cwd: currentCwd, - }; - const r = runHook("msd-context-monitor.js", payload); - handleHookResult(r, output); - - // Also inject a MSD compaction breadcrumb (mirrors the original plugin) - output.context = output.context || []; - output.context.push( - `[MSD] Active session: ${currentSessionId}. Preserve any in-flight phase/plan state.`, - ); - }, - - // ── General event subscriptions ───────────────────────────────────── - event: async ({ event }) => { - // session.created → SessionStart hooks - if (event.type === "session.created") { - // Track session for context-monitor payloads. - // SDK type EventSessionCreated: { properties: { info: Session } } - // Session has `id` and `directory` (not `cwd`). - const info = event.properties?.info; - currentSessionId = - info?.id || event.sessionID || event.session_id || null; - if (info?.directory) currentCwd = info.directory; - - // msd-ensure-canonical-path.js — no stdin dependency; silent - runHook("msd-ensure-canonical-path.js", { - hook_event_name: "SessionStart", - session_id: currentSessionId, - cwd: currentCwd, - }); - // msd-check-update.js — spawns its own background worker; no stdin - runHook("msd-check-update.js", { - hook_event_name: "SessionStart", - session_id: currentSessionId, - cwd: currentCwd, - }); - return; - } - - // file.edited → FileChanged hook (config.json reload) - if (event.type === "file.edited") { - // SDK type EventFileEdited: { properties: { file: string } } - const filePath = event.properties?.file || event.filePath || ""; - if (!filePath.endsWith("config.json")) return; - const cwd = event.properties?.cwd || currentCwd; - const expected = path.join(cwd, ".planning", "config.json"); - if (path.resolve(filePath) !== path.resolve(expected)) return; - - const payload = { - hook_event_name: "FileChanged", - file_path: filePath, - event: "change", - cwd, - }; - const r = runHook("msd-config-reload.js", payload); - // Advisory-only (additionalContext); surface to logs - handleHookResult(r); - return; - } - - // session.idle ↔ Claude Stop lifecycle point (#1682 Slice 1b/c). - // OpenCode fires session.idle when the run quiesces. MSD maps it to the - // Stop equivalent — the opencode-subset lifecycle peer of compaction - // (compaction preserves state across context-window summarization; idle - // marks end-of-turn). No-op sentinel today (MSD state is already - // persisted to .planning/), but it MUST be recognized so the declared - // opencode-subset surface is fully wired and a future Stop-class hook can - // attach without a plugin change. - if (event.type === "session.idle") { - return; - } - - // permission.asked / permission.replied — OpenCode permission lifecycle - // (#2087, opencode.ai/docs/plugins). MSD gates tool INPUTS at - // tool.execute.before (read-guard, injection-scanner); the permission - // grant/deny decision itself carries no MSD workflow-phase contribution, - // so these are recognized sentinels — wired so a future permission-aware - // gate can attach without a plugin change (the engine owns phase - // sequencing; this host bus is session/tool/permission-scoped, never - // phase-scoped — ADR-1239 §OpenCode). - if (event.type === "permission.asked" || event.type === "permission.replied") { - return; - } - - // session.error — OpenCode session-error lifecycle point (#2087). No MSD - // hook fires here today (loop state is already persisted to .planning/); - // recognized so the declared extension-event surface is fully wired and a - // future error-class hook can attach without a plugin change. - if (event.type === "session.error") { - return; - } - }, - }; -}; - -// Export shape — verified against OpenCode's plugin loader source -// (packages/opencode/src/plugin). The loader imports this module and runs -// `for (const entry of Object.values(mod)) { getServerPlugin(entry) }`, where -// `getServerPlugin` accepts a bare function OR an object exposing a `.server` -// function, and THROWS `TypeError("Plugin export is not a function")` for -// anything else. So EVERY enumerable value the loader iterates must be a -// function or an object with `.server`. -// -// The subtlety: depending on how OpenCode's runtime (Node or Bun) imports a -// CommonJS file, `mod` may be the raw `module.exports` OR an ESM namespace of -// the form `{ default: module.exports, ...syntheticNamedExports }`. A plain -// `module.exports = { id: "msd-core", server }` literal risks a string `id` -// appearing in `Object.values(mod)` (as a raw property, or as a lexer- -// synthesized named export) — which would trip the throw. Two defenses: -// 1. `id` is defined NON-ENUMERABLE, so it never appears in Object.values yet -// stays readable (via property access) for the loader's identity/dedup. -// 2. `module.exports` is assigned from a VARIABLE (not an object literal), so -// cjs-module-lexer cannot statically synthesize named exports from it — -// only `default` is exposed under ESM/Bun interop. -// Result: raw-CJS `Object.values` = `[server]`; ESM `Object.values` = -// `[{server, }]` — both fully extractable. Test-only helpers hang -// off the `server` FUNCTION (`server._internals`), never as a sibling export. -MsdCorePlugin._internals = { - REPO_ROOT, - IS_PACKAGE_TREE, - mapToolName, - mapToolInput, - parseFrontmatter, - rewriteContent, - isMsdManagedFile, - handleHookResult, - MsdCorePlugin, -}; - -const msdCorePluginExport = { server: MsdCorePlugin }; -Object.defineProperty(msdCorePluginExport, "id", { - value: "msd-core", - enumerable: false, - writable: false, - configurable: false, -}); -module.exports = msdCorePluginExport; diff --git a/CONTEXT.md b/CONTEXT.md index a90e66cf0..28cd47e20 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -254,7 +254,7 @@ Module owning the `{"type":"commonjs"}` module-type marker MSD writes beside its Module owning validation for Installer Migration Module records and planned actions. It enforces migration metadata, explicit install scopes, ownership evidence for destructive/config actions, and runtime contract citations for runtime config rewrites before a migration can enter planning or apply. ### Installer Module -Primary installer for all runtimes. Single production file: `bin/install.js` (hand-authored JS — it is NOT generated from `src/*.cts`; ADR-1508 keeps it hand-authored deliberately, and no `npm run build` step emits it). Exports: `install(isGlobal, runtime[, configDir])` → typed result `{ runtime, configDir, settingsPath, settings, statuslineCommand, updateBannerCommand, configuredEntrypoints }`; `uninstall(isGlobal, runtime[, configDir])`; `installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)`; `uninstallRuntimeArtifacts(runtime, configDir, scope)`; `writeManifest(configDir, runtime)`. Runtime enum: `allRuntimes` (18 values: claude, antigravity, augment, cline, codebuddy, codex, copilot, cursor, hermes, kimi, kimi-code, kilo, opencode, pi, qwen, trae, windsurf, zcode). Directory helpers: `getDirName(runtime)` → local dir name; `getConfigDirFromHome(runtime, isGlobal)` → shell-quoted path fragment. Per-runtime global config-dir resolution is delegated to `msd-core/bin/lib/runtime-homes.cjs:getGlobalConfigDir(runtime[, explicitDir])` — the canonical, env-var–aware projection (`explicitDir` override + opencode/kilo `*_CONFIG` file-path precedence); the legacy in-installer `getGlobalDir`/`getOpencodeGlobalDir`/`getKiloGlobalDir` were retired into it (#56). The same module exposes `detectAntigravityDirAmbiguity(opts)` — a side-effect-free probe reporting whether multiple `~/.gemini/antigravity{,-ide,-cli}` dirs coexist and which one MSD's `msd-core/VERSION` marker (the `dot-home-nested` `probeExists`) resolves to, for installer / `/msd-update` operator guidance when a pre-#217 install landed in the wrong sibling dir (#1441). Runtime-specific helpers: `resolveKiloConfigPath(configDir)`, `configureKiloPermissions(isGlobal[, explicitDir])`. Claude-specific permission helpers: `mergeClaudePermissions(settings)` — non-destructively appends MSD-owned allow entries (see `MSD_CLAUDE_ALLOW_PERMISSIONS`) to a Claude Code settings object and filters out the retired legacy forms (`MSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS` #2278; `MSD_CLAUDE_LEGACY_DENY_PERMISSIONS` #4221 — the `Read(.env*)` deny rules are retired in favor of the managed `msd-secret-read-guard.js` hook, and an emptied `deny` array is deleted); called from `finishInstall` for `runtime === 'claude'` only; uninstall removes exactly these entries (#768). Layout-driven artifact copy/removal delegates to `msd-core/bin/lib/runtime-artifact-layout.cjs:resolveRuntimeArtifactLayout` (throws `TypeError` for unknown runtimes). Five runtimes with non-recursive skill loaders (cline, qwen, hermes, augment, trae) use a nested router layout: 6 `msd-ns-*` router bundles emitted as top-level skills, with concrete skills nested at `/skills//SKILL.md` (hermes prefix='': `skills/msd/ns-*/…`). claude (reverted from nested per #924 — the Skill tool errors on unrouted names) and antigravity (one-level scan, but concrete skills must be top-level discoverable) plus the remaining skills-runtimes (cursor, codex, copilot, windsurf, codebuddy, opencode, kilo) use the flat `skills/msd-/` layout. See Skill Surface Budget Module and Runtime Artifact Layout Module. `finishInstall` calls `assertConfiguredEntrypoints` (Runtime Hooks Surface Module, dedup'd by `(configPath, scriptPath)` first) before its own `writeSettings` — a failing settings-json-surface install/update never persists that write. `installAllRuntimes`'s `finalize()` runs the same check once over the aggregate set before its `printSummaries()` loop calls `finishInstall` per runtime, so the per-runtime slice is checked twice; the second pass is a dozen-odd `statSync`/`accessSync` calls and is left unconditional rather than gated on a caller-supplied bypass flag. Codex/Cursor/Windsurf/Kimi/Cline writers persist their own hooks.json/config.toml/`.clinerules/hooks/PreToolUse` directly inside `install()`, ahead of `installAllRuntimes`'s aggregate `assertConfiguredEntrypoints` call, so a validation failure there is reported (and blocks the completion message) before that file is reverted where a rollback exists. Codex's `install()` result exposes two rollbacks: `rollbackInstallerMigrations` — like every other runtime's — is the installer-migrations-only closure the name describes, and the Codex-only `rollbackPreInstallSnapshot` binds `restoreCodexSnapshot` (#3245's full pre-install snapshot restore — config.toml, hooks.json, skills/, agents/, VERSION). `installAllRuntimes`'s `rollbackFinalizedInstallerMigrations` picks `rollbackPreInstallSnapshot` only when the triggering error is tagged `configuredEntrypointValidation` (set by `assertConfiguredEntrypoints`); any other finalize-stage exception gets `rollbackInstallerMigrations`, so an unrelated sibling failure (e.g. EACCES on another runtime's permission write) cannot un-install a Codex install that already succeeded. The discriminator is the error's KIND, not which runtime owns the bad entrypoint: the aggregate gate is all-or-nothing, so ANY runtime's invalid entrypoint reverts the Codex snapshot — including when Codex's own entrypoints were fine and its "Done!" summary already printed (#4249). Cursor/Windsurf/Kimi/Cline have no equivalent snapshot, so their result binds only the narrow rollback and a validation failure leaves their just-written file unrevertable (#4249). `bannerOpts.configuredEntrypoints` is the only source `finishInstall` validates — a caller other than `installAllRuntimes` that omits it gets no entrypoint validation at all. +Primary installer for all runtimes. Single production file: `bin/install.js` (hand-authored JS — it is NOT generated from `src/*.cts`; ADR-1508 keeps it hand-authored deliberately, and no `npm run build` step emits it). Exports: `install(isGlobal, runtime[, configDir])` → typed result `{ runtime, configDir, settingsPath, settings, statuslineCommand, updateBannerCommand, configuredEntrypoints }`; `uninstall(isGlobal, runtime[, configDir])`; `installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)`; `uninstallRuntimeArtifacts(runtime, configDir, scope)`; `writeManifest(configDir, runtime)`. Runtime enum: `allRuntimes` (6 values: claude, antigravity, codex, cursor, opencode, zcode). Directory helpers: `getDirName(runtime)` → local dir name; `getConfigDirFromHome(runtime, isGlobal)` → shell-quoted path fragment. Per-runtime global config-dir resolution is delegated to `msd-core/bin/lib/runtime-homes.cjs:getGlobalConfigDir(runtime[, explicitDir])` — the canonical, env-var–aware projection (`explicitDir` override + opencode/kilo `*_CONFIG` file-path precedence); the legacy in-installer `getGlobalDir`/`getOpencodeGlobalDir`/`getKiloGlobalDir` were retired into it (#56). The same module exposes `detectAntigravityDirAmbiguity(opts)` — a side-effect-free probe reporting whether multiple `~/.gemini/antigravity{,-ide,-cli}` dirs coexist and which one MSD's `msd-core/VERSION` marker (the `dot-home-nested` `probeExists`) resolves to, for installer / `/msd-update` operator guidance when a pre-#217 install landed in the wrong sibling dir (#1441). Runtime-specific helpers: `resolveKiloConfigPath(configDir)`, `configureKiloPermissions(isGlobal[, explicitDir])`. Claude-specific permission helpers: `mergeClaudePermissions(settings)` — non-destructively appends MSD-owned allow entries (see `MSD_CLAUDE_ALLOW_PERMISSIONS`) to a Claude Code settings object and filters out the retired legacy forms (`MSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS` #2278; `MSD_CLAUDE_LEGACY_DENY_PERMISSIONS` #4221 — the `Read(.env*)` deny rules are retired in favor of the managed `msd-secret-read-guard.js` hook, and an emptied `deny` array is deleted); called from `finishInstall` for `runtime === 'claude'` only; uninstall removes exactly these entries (#768). Layout-driven artifact copy/removal delegates to `msd-core/bin/lib/runtime-artifact-layout.cjs:resolveRuntimeArtifactLayout` (throws `TypeError` for unknown runtimes). Five runtimes with non-recursive skill loaders (cline, qwen, hermes, augment, trae) use a nested router layout: 6 `msd-ns-*` router bundles emitted as top-level skills, with concrete skills nested at `/skills//SKILL.md` (hermes prefix='': `skills/msd/ns-*/…`). claude (reverted from nested per #924 — the Skill tool errors on unrouted names) and antigravity (one-level scan, but concrete skills must be top-level discoverable) plus the remaining skills-runtimes (cursor, codex, copilot, windsurf, codebuddy, opencode, kilo) use the flat `skills/msd-/` layout. See Skill Surface Budget Module and Runtime Artifact Layout Module. `finishInstall` calls `assertConfiguredEntrypoints` (Runtime Hooks Surface Module, dedup'd by `(configPath, scriptPath)` first) before its own `writeSettings` — a failing settings-json-surface install/update never persists that write. `installAllRuntimes`'s `finalize()` runs the same check once over the aggregate set before its `printSummaries()` loop calls `finishInstall` per runtime, so the per-runtime slice is checked twice; the second pass is a dozen-odd `statSync`/`accessSync` calls and is left unconditional rather than gated on a caller-supplied bypass flag. Codex/Cursor/Windsurf/Kimi/Cline writers persist their own hooks.json/config.toml/`.clinerules/hooks/PreToolUse` directly inside `install()`, ahead of `installAllRuntimes`'s aggregate `assertConfiguredEntrypoints` call, so a validation failure there is reported (and blocks the completion message) before that file is reverted where a rollback exists. Codex's `install()` result exposes two rollbacks: `rollbackInstallerMigrations` — like every other runtime's — is the installer-migrations-only closure the name describes, and the Codex-only `rollbackPreInstallSnapshot` binds `restoreCodexSnapshot` (#3245's full pre-install snapshot restore — config.toml, hooks.json, skills/, agents/, VERSION). `installAllRuntimes`'s `rollbackFinalizedInstallerMigrations` picks `rollbackPreInstallSnapshot` only when the triggering error is tagged `configuredEntrypointValidation` (set by `assertConfiguredEntrypoints`); any other finalize-stage exception gets `rollbackInstallerMigrations`, so an unrelated sibling failure (e.g. EACCES on another runtime's permission write) cannot un-install a Codex install that already succeeded. The discriminator is the error's KIND, not which runtime owns the bad entrypoint: the aggregate gate is all-or-nothing, so ANY runtime's invalid entrypoint reverts the Codex snapshot — including when Codex's own entrypoints were fine and its "Done!" summary already printed (#4249). Cursor/Windsurf/Kimi/Cline have no equivalent snapshot, so their result binds only the narrow rollback and a validation failure leaves their just-written file unrevertable (#4249). `bannerOpts.configuredEntrypoints` is the only source `finishInstall` validates — a caller other than `installAllRuntimes` that omits it gets no entrypoint validation at all. ### I/O Module Module owning the tool's CLI I/O primitives: `output()` result emission (with large-payload temp-file spillover via `MSD_TEMP_DIR`/`ensureMsdTempDir`/`reapStaleTempFiles`), `error()` stderr emission with exit-code mapping, and the JSON-error-mode toggle (`setJsonErrorMode`/`getJsonErrorMode`, `ERROR_REASON`). **Degraded result vs fault (ADR-2980, #2980):** the two emitters are a deliberate two-channel failure contract, not a drift. A **fault** is `error(message, reason)` — stderr, exit **1**, structured `{ok:false,reason,message}` envelope under `--json-errors`. A **degraded result** is `output({ error: … })` — stdout, exit **0**, `--json-errors` does not apply — and means the command ran to completion and is reporting a condition (absent artifact, and in practice also missing-argument and unusable-input cases) through its result; a caller detects it by inspecting the payload, never by exit code. Ratified across **60 sites in 9 modules** (`state` 25, `verify` 8, `workstream` 7, `frontmatter` 6, `commands` 5, `template` 3, `gsd2-import` 2, `phase` 2, `roadmap` 2) because normalizing them to exit 1 is a Hyrum's Law break over a CRITICAL radius (`get_impact(cmdStateSnapshot)`; `output` has 170 direct callers). #2966/#2980 record "42 sites" — that counts only literals whose FIRST key is `error` (the `output\(\{\s*error:` regex); 18 more put another key first (`{found:false, error}`) and are identical in contract, so 60 is the population and 42 is a subset. New code prefers the fault path or a named-field result (`{updated:false, reason}`), not a 61st site. Known cost carried by the decision: the exit code does not distinguish absent from unusable, which is ADR-1411's "corrupt is not absent" open edge. Docs: `docs/json-errors.md` → "Degraded results vs faults". Extracted from the Core module per ADR-857 rollout phase 1 (#859) so feature modules (`graphify`, `intel`, `audit`, `profile-pipeline`) depend on a small I/O seam instead of the core god-module; the `core.cjs` re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: `msd-core/bin/lib/io.cjs` (generated from `src/io.cts`). diff --git a/README.ja-JP.md b/README.ja-JP.md index e6b16c80d..f75b086b9 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -6,7 +6,7 @@ [English](README.md) · [Português](README.pt-BR.md) · [简体中文](README.zh-CN.md) · **日本語** · [한국어](README.ko-KR.md) -**Claude Code、OpenCode、Antigravity CLI、Kimi CLI、Kilo、Codex、Copilot、Cursor、Windsurf などに対応した、軽量なメタプロンプティング・コンテキストエンジニアリング・仕様駆動開発システムです。** +**Claude Code、OpenCode、Antigravity CLI、Codex、Cursor などに対応した、軽量なメタプロンプティング・コンテキストエンジニアリング・仕様駆動開発システムです。** [![npm version](https://img.shields.io/npm/v/%40golem15%2Fmsd-core?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@golem15/msd-core) [![npm downloads](https://img.shields.io/npm/dm/%40golem15%2Fmsd-core?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@golem15/msd-core) @@ -21,7 +21,7 @@ ## MSD Core とは -MSD Core は、コンテキストエンジニアリングと仕様駆動開発のフレームワークです。AI コーディングエージェント(Claude Code、Codex、Antigravity CLI、Kimi CLI、Copilot、Cursor など)を規律あるフェーズループで動かします。[コンテキストの腐敗](docs/ja-JP/explanation/context-engineering.md)—AI がコンテキストウィンドウを埋めるにつれて出力品質が低下する問題—を解決するために、重いリサーチ・計画・実行作業をすべて新鮮なコンテキストのサブエージェントで実行し、メインセッションをスリムに保ちます。 +MSD Core は、コンテキストエンジニアリングと仕様駆動開発のフレームワークです。AI コーディングエージェント(Claude Code、Codex、Antigravity CLI、Cursor など)を規律あるフェーズループで動かします。[コンテキストの腐敗](docs/ja-JP/explanation/context-engineering.md)—AI がコンテキストウィンドウを埋めるにつれて出力品質が低下する問題—を解決するために、重いリサーチ・計画・実行作業をすべて新鮮なコンテキストのサブエージェントで実行し、メインセッションをスリムに保ちます。 --- @@ -43,7 +43,7 @@ MSD Core は、コンテキストエンジニアリングと仕様駆動開発 npx @golem15/msd-core@latest ``` -インストーラーはランタイム(Claude Code、OpenCode、Antigravity CLI、Kimi CLI、Kilo、Codex、Copilot、Cursor、Windsurf など)とグローバルインストールかローカルインストールかを尋ねます。クロスランタイム互換性のためにインストーラーが必要です。`agents/` や `commands/` からファイルを直接コピーしないでください。 +インストーラーはランタイム(Claude Code、OpenCode、Antigravity CLI、Codex、Cursor など)とグローバルインストールかローカルインストールかを尋ねます。クロスランタイム互換性のためにインストーラーが必要です。`agents/` や `commands/` からファイルを直接コピーしないでください。 別のランタイムをお使いの場合や Node.js がない場合は [ランタイムへのインストール](docs/ja-JP/how-to/install-on-your-runtime.md) を参照してください。 diff --git a/README.ko-KR.md b/README.ko-KR.md index 013ffebc6..3e8806cc7 100644 --- a/README.ko-KR.md +++ b/README.ko-KR.md @@ -6,7 +6,7 @@ [English](README.md) · [Português](README.pt-BR.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · **한국어** -**Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf 등을 위한 경량 메타 프롬프팅, 컨텍스트 엔지니어링, 스펙 기반 개발 시스템.** +**Claude Code, OpenCode, Antigravity CLI, Codex, Cursor 등을 위한 경량 메타 프롬프팅, 컨텍스트 엔지니어링, 스펙 기반 개발 시스템.** [![npm version](https://img.shields.io/npm/v/%40golem15%2Fmsd-core?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@golem15/msd-core) [![npm downloads](https://img.shields.io/npm/dm/%40golem15%2Fmsd-core?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@golem15/msd-core) @@ -21,7 +21,7 @@ ## MSD Core란 -MSD Core는 컨텍스트 엔지니어링 및 스펙 기반 개발 프레임워크로, AI 코딩 에이전트(Claude Code, Codex, Antigravity CLI, Kimi CLI, Copilot, Cursor 등)를 엄격한 단계 루프로 운용합니다. AI가 컨텍스트 창을 채워 나가면서 발생하는 품질 저하인 [컨텍스트 rot](docs/ko-KR/explanation/context-engineering.md) 문제를 해결합니다. 무거운 리서치, 기획, 실행 작업은 새로운 컨텍스트의 서브에이전트에서 처리하고, 메인 세션은 가볍게 유지됩니다. +MSD Core는 컨텍스트 엔지니어링 및 스펙 기반 개발 프레임워크로, AI 코딩 에이전트(Claude Code, Codex, Antigravity CLI, Cursor 등)를 엄격한 단계 루프로 운용합니다. AI가 컨텍스트 창을 채워 나가면서 발생하는 품질 저하인 [컨텍스트 rot](docs/ko-KR/explanation/context-engineering.md) 문제를 해결합니다. 무거운 리서치, 기획, 실행 작업은 새로운 컨텍스트의 서브에이전트에서 처리하고, 메인 세션은 가볍게 유지됩니다. --- @@ -43,7 +43,7 @@ MSD Core는 컨텍스트 엔지니어링 및 스펙 기반 개발 프레임워 npx @golem15/msd-core@latest ``` -설치 프로그램이 런타임(Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf 등)과 전역/로컬 설치 여부를 묻습니다. 크로스 런타임 호환성을 위해 설치 프로그램을 사용해야 합니다 — `agents/` 또는 `commands/`에서 파일을 직접 복사하지 마세요. +설치 프로그램이 런타임(Claude Code, OpenCode, Antigravity CLI, Codex, Cursor 등)과 전역/로컬 설치 여부를 묻습니다. 크로스 런타임 호환성을 위해 설치 프로그램을 사용해야 합니다 — `agents/` 또는 `commands/`에서 파일을 직접 복사하지 마세요. 다른 런타임이나 Node.js가 없는 환경은 [런타임에 설치하기](docs/ko-KR/how-to/install-on-your-runtime.md)를 참조하세요. diff --git a/README.md b/README.md index 8b992a65e..30f23ae24 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **English** · [Português](README.pt-BR.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [한국어](README.ko-KR.md) -**A light-weight meta-prompting, context engineering, and spec-driven development system for Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more.** +**A light-weight meta-prompting, context engineering, and spec-driven development system for Claude Code, OpenCode, Antigravity CLI, Codex, Cursor, and more.** [![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](LICENSE) @@ -16,7 +16,7 @@ ## What is MSD Core -MSD Core is a context-engineering and spec-driven development framework that drives AI coding agents (Claude Code, Codex, Antigravity CLI, Kimi CLI, Copilot, Cursor, and more) through a disciplined phase loop. It solves [context rot](docs/explanation/context-engineering.md) — the quality degradation that accumulates as an AI fills its context window — by running all heavy research, planning, and execution work in fresh-context subagents while keeping your main session lean. +MSD Core is a context-engineering and spec-driven development framework that drives AI coding agents (Claude Code, Codex, Antigravity CLI, Cursor, and more) through a disciplined phase loop. It solves [context rot](docs/explanation/context-engineering.md) — the quality degradation that accumulates as an AI fills its context window — by running all heavy research, planning, and execution work in fresh-context subagents while keeping your main session lean. --- @@ -42,7 +42,7 @@ cd msd-core scripts/install-golem15.sh # Node 24; installs for Claude Code and Codex ``` -For any other runtime, run `node bin/install.js` directly. The installer prompts for your runtime (Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more) and whether to install globally or locally. The installer is required for cross-runtime compatibility — do not copy files from `agents/` or `commands/` directly. +For any other runtime, run `node bin/install.js` directly. The installer prompts for your runtime (Claude Code, OpenCode, Antigravity CLI, Codex, Cursor, and more) and whether to install globally or locally. The installer is required for cross-runtime compatibility — do not copy files from `agents/` or `commands/` directly. On another runtime or without Node.js? See [Install on your runtime](docs/how-to/install-on-your-runtime.md). diff --git a/README.pt-BR.md b/README.pt-BR.md index 982269bb9..f51661ef4 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -6,7 +6,7 @@ [English](README.md) · **Português** · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [한국어](README.ko-KR.md) -**Um sistema leve de meta-prompting, engenharia de contexto e desenvolvimento orientado a especificações para Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf e muito mais.** +**Um sistema leve de meta-prompting, engenharia de contexto e desenvolvimento orientado a especificações para Claude Code, OpenCode, Antigravity CLI, Codex, Cursor e muito mais.** [![npm version](https://img.shields.io/npm/v/%40golem15%2Fmsd-core?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@golem15/msd-core) [![npm downloads](https://img.shields.io/npm/dm/%40golem15%2Fmsd-core?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@golem15/msd-core) @@ -21,7 +21,7 @@ ## O que é o MSD Core -MSD Core é um framework de engenharia de contexto e desenvolvimento orientado a especificações que conduz agentes de codificação com IA (Claude Code, Codex, Antigravity CLI, Kimi CLI, Copilot, Cursor e mais) por meio de um ciclo de fases disciplinado. Ele resolve o [context rot](docs/pt-BR/explanation/context-engineering.md) — a degradação de qualidade que se acumula à medida que uma IA preenche sua janela de contexto — executando todo o trabalho pesado de pesquisa, planejamento e execução em subagentes com contexto limpo, mantendo sua sessão principal enxuta. +MSD Core é um framework de engenharia de contexto e desenvolvimento orientado a especificações que conduz agentes de codificação com IA (Claude Code, Codex, Antigravity CLI, Cursor e mais) por meio de um ciclo de fases disciplinado. Ele resolve o [context rot](docs/pt-BR/explanation/context-engineering.md) — a degradação de qualidade que se acumula à medida que uma IA preenche sua janela de contexto — executando todo o trabalho pesado de pesquisa, planejamento e execução em subagentes com contexto limpo, mantendo sua sessão principal enxuta. --- @@ -43,7 +43,7 @@ Cada marco repete o mesmo ciclo de cinco etapas, uma fase por vez: npx @golem15/msd-core@latest ``` -O instalador solicita seu ambiente de execução (Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf e mais) e se deseja instalar globalmente ou localmente. O instalador é necessário para compatibilidade entre runtimes — não copie arquivos diretamente de `agents/` ou `commands/`. +O instalador solicita seu ambiente de execução (Claude Code, OpenCode, Antigravity CLI, Codex, Cursor e mais) e se deseja instalar globalmente ou localmente. O instalador é necessário para compatibilidade entre runtimes — não copie arquivos diretamente de `agents/` ou `commands/`. Em outro runtime ou sem Node.js? Consulte [Instalar no seu runtime](docs/pt-BR/how-to/install-on-your-runtime.md). diff --git a/README.zh-CN.md b/README.zh-CN.md index 1ab68e29d..97cbdf1bd 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -6,7 +6,7 @@ [English](README.md) · [Português](README.pt-BR.md) · **简体中文** · [日本語](README.ja-JP.md) · [한국어](README.ko-KR.md) -**一套轻量级的元提示、上下文工程与规范驱动开发系统,适用于 Claude Code、OpenCode、Antigravity CLI、Kimi CLI、Kilo、Codex、Copilot、Cursor、Windsurf 等 AI 编程工具。** +**一套轻量级的元提示、上下文工程与规范驱动开发系统,适用于 Claude Code、OpenCode、Antigravity CLI、Codex、Cursor 等 AI 编程工具。** [![npm version](https://img.shields.io/npm/v/%40golem15%2Fmsd-core?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@golem15/msd-core) [![npm downloads](https://img.shields.io/npm/dm/%40golem15%2Fmsd-core?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@golem15/msd-core) @@ -21,7 +21,7 @@ ## 什么是 MSD Core -MSD Core 是一套上下文工程与规范驱动开发框架,能够引导 AI 编程智能体(Claude Code、Codex、Antigravity CLI、Kimi CLI、Copilot、Cursor 等)按照严格的阶段循环推进工作。它解决了[上下文腐化](docs/zh-CN/explanation/context-engineering.md)问题——即随着 AI 填满上下文窗口而逐渐累积的质量下降——通过在全新上下文的子智能体中运行所有繁重的研究、规划和执行工作,同时保持主会话的精简。 +MSD Core 是一套上下文工程与规范驱动开发框架,能够引导 AI 编程智能体(Claude Code、Codex、Antigravity CLI、Cursor 等)按照严格的阶段循环推进工作。它解决了[上下文腐化](docs/zh-CN/explanation/context-engineering.md)问题——即随着 AI 填满上下文窗口而逐渐累积的质量下降——通过在全新上下文的子智能体中运行所有繁重的研究、规划和执行工作,同时保持主会话的精简。 --- @@ -43,7 +43,7 @@ MSD Core 是一套上下文工程与规范驱动开发框架,能够引导 AI npx @golem15/msd-core@latest ``` -安装程序会提示选择运行时(Claude Code、OpenCode、Antigravity CLI、Kimi CLI、Kilo、Codex、Copilot、Cursor、Windsurf 等)以及是全局安装还是本地安装。跨运行时兼容性需要使用安装程序——请勿直接从 `agents/` 或 `commands/` 目录复制文件。 +安装程序会提示选择运行时(Claude Code、OpenCode、Antigravity CLI、Codex、Cursor 等)以及是全局安装还是本地安装。跨运行时兼容性需要使用安装程序——请勿直接从 `agents/` 或 `commands/` 目录复制文件。 使用其他运行时或没有 Node.js?请参阅[在你的运行时上安装](docs/zh-CN/how-to/install-on-your-runtime.md)。 diff --git a/agents/msd-code-fixer.compact.md b/agents/msd-code-fixer.compact.md index 37f4fa770..5fffc6920 100644 --- a/agents/msd-code-fixer.compact.md +++ b/agents/msd-code-fixer.compact.md @@ -287,7 +287,7 @@ For each finding in sorted order: **f. Commit atomically.** If verification passed, use `msd_run query commit` (message first, then every staged file path): ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run query commit \ "fix({padded_phase}): {finding_id} {short_description}" \ --files \ diff --git a/agents/msd-code-fixer.md b/agents/msd-code-fixer.md index 9f8b97b00..1e9e58aec 100644 --- a/agents/msd-code-fixer.md +++ b/agents/msd-code-fixer.md @@ -532,7 +532,7 @@ For each finding in sorted order: Use `msd_run query commit` with conventional format (message first, then every staged file path): ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run query commit \ "fix({padded_phase}): {finding_id} {short_description}" \ --files \ diff --git a/agents/msd-debug-session-manager.compact.md b/agents/msd-debug-session-manager.compact.md index ec021aa5a..b34cf201a 100644 --- a/agents/msd-debug-session-manager.compact.md +++ b/agents/msd-debug-session-manager.compact.md @@ -113,7 +113,7 @@ literal in this file. Resolve the debugger model before spawning (canonical `msd_run` preamble — established once here, the single definition this agent carries): ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi debugger_model=$(msd_run query resolve-model msd-debugger 2>/dev/null | jq -r '.model' 2>/dev/null || true) ``` diff --git a/agents/msd-debug-session-manager.md b/agents/msd-debug-session-manager.md index 91e4e3445..69d846f02 100644 --- a/agents/msd-debug-session-manager.md +++ b/agents/msd-debug-session-manager.md @@ -119,7 +119,7 @@ literal in this file. Resolve the debugger model before spawning: ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi debugger_model=$(msd_run query resolve-model msd-debugger 2>/dev/null | jq -r '.model' 2>/dev/null || true) ``` diff --git a/agents/msd-debugger.md b/agents/msd-debugger.md index 57f2fce05..5d070a1f6 100644 --- a/agents/msd-debugger.md +++ b/agents/msd-debugger.md @@ -958,7 +958,7 @@ mv .planning/debug/{slug}.md .planning/debug/resolved/ **Check planning config using state load (commit_docs is available from the output):** ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi INIT=$(msd_run query state.load) if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi # commit_docs is in the JSON output diff --git a/agents/msd-eval-auditor.compact.md b/agents/msd-eval-auditor.compact.md index 8ac91624a..9c8ed62ac 100644 --- a/agents/msd-eval-auditor.compact.md +++ b/agents/msd-eval-auditor.compact.md @@ -83,7 +83,7 @@ Score 5 components (ok/partial/missing): **Eval tooling** — installed and actu Do NOT compute scores by hand. Call the deterministic verb with your audited inputs: ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run query eval.score --covered --total --infra ,,,, --raw ``` diff --git a/agents/msd-eval-auditor.md b/agents/msd-eval-auditor.md index f23ae21f1..86e05f5c8 100644 --- a/agents/msd-eval-auditor.md +++ b/agents/msd-eval-auditor.md @@ -114,7 +114,7 @@ Score 5 components (ok / partial / missing): Do NOT compute scores by hand. Call the deterministic verb with your audited inputs: ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run query eval.score --covered --total --infra ,,,, --raw ``` diff --git a/agents/msd-intel-updater.compact.md b/agents/msd-intel-updater.compact.md index e6aff485b..71fe87226 100644 --- a/agents/msd-intel-updater.compact.md +++ b/agents/msd-intel-updater.compact.md @@ -144,7 +144,7 @@ Glob for project structure indicators: `**/package.json`, `**/tsconfig.json`, `* ### Step 2: Stack Detection Read package.json, configs, build files. Write `stack.json`. Then patch its timestamp: ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run intel patch-meta .planning/intel/stack.json ``` (This bootstrap runs once per fresh shell; later `msd_run intel patch-meta ...` calls in Steps 3-6 reuse the same block — repeat it verbatim if invoking in a new Bash call.) diff --git a/agents/msd-intel-updater.md b/agents/msd-intel-updater.md index 8b9e27946..8735a25ea 100644 --- a/agents/msd-intel-updater.md +++ b/agents/msd-intel-updater.md @@ -212,7 +212,7 @@ Glob for project structure indicators: Read package.json, configs, and build files. Write `stack.json`. Then patch its timestamp: ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run intel patch-meta .planning/intel/stack.json ``` diff --git a/agents/msd-phase-researcher.md b/agents/msd-phase-researcher.md index 3346a5609..e3df7baf3 100644 --- a/agents/msd-phase-researcher.md +++ b/agents/msd-phase-researcher.md @@ -118,7 +118,7 @@ Construct a JSON file at a temp path (e.g. `/tmp/research-plan-input.json`): ### Step B — Obtain the fetch plan ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run query research-plan --input /tmp/research-plan-input.json ``` diff --git a/agents/msd-project-researcher.compact.md b/agents/msd-project-researcher.compact.md index b3df818da..66b08bd77 100644 --- a/agents/msd-project-researcher.compact.md +++ b/agents/msd-project-researcher.compact.md @@ -79,7 +79,7 @@ JSON file at a temp path (e.g. `/tmp/research-plan-input.json`): ### Step B — Obtain the fetch plan ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run query research-plan --input /tmp/research-plan-input.json ``` diff --git a/agents/msd-project-researcher.md b/agents/msd-project-researcher.md index eaec8da8d..fae558740 100644 --- a/agents/msd-project-researcher.md +++ b/agents/msd-project-researcher.md @@ -80,7 +80,7 @@ Construct a JSON file at a temp path (e.g. `/tmp/research-plan-input.json`): ### Step B — Obtain the fetch plan ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run query research-plan --input /tmp/research-plan-input.json ``` diff --git a/agents/msd-research-synthesizer.compact.md b/agents/msd-research-synthesizer.compact.md index 576e35725..e89da6f9f 100644 --- a/agents/msd-research-synthesizer.compact.md +++ b/agents/msd-research-synthesizer.compact.md @@ -114,7 +114,7 @@ Write to `.planning/research/SUMMARY.md`. The 4 parallel researcher agents write files but do NOT commit. You commit everything together. ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run query commit "docs: complete project research" --files .planning/research/ ``` diff --git a/agents/msd-research-synthesizer.md b/agents/msd-research-synthesizer.md index ee65020ce..c2f1d72c1 100644 --- a/agents/msd-research-synthesizer.md +++ b/agents/msd-research-synthesizer.md @@ -155,7 +155,7 @@ Write to `.planning/research/SUMMARY.md`. The 4 parallel researcher agents write files but do NOT commit. You commit everything together. ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run query commit "docs: complete project research" --files .planning/research/ ``` diff --git a/agents/msd-ui-researcher.compact.md b/agents/msd-ui-researcher.compact.md index 0d2373694..c52a4b2ed 100644 --- a/agents/msd-ui-researcher.compact.md +++ b/agents/msd-ui-researcher.compact.md @@ -176,7 +176,7 @@ Read template `~/.claude/msd-core/templates/UI-SPEC.md`. Fill all sections. Writ ## Step 6: Commit (optional) ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run query commit "docs($PHASE): UI design contract" --files "$PHASE_DIR/$PADDED_PHASE-UI-SPEC.md" ``` diff --git a/agents/msd-ui-researcher.md b/agents/msd-ui-researcher.md index cf70a1057..b4d1d21aa 100644 --- a/agents/msd-ui-researcher.md +++ b/agents/msd-ui-researcher.md @@ -327,7 +327,7 @@ This file is the canonical output of this agent. The orchestrator reads `$PHASE_ ## Step 6: Commit (optional) ```bash -_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/msd-core/bin/${_MSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/msd-core/bin/${_MSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/msd-core/bin/${_MSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/msd-core/bin/${_MSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/msd-core/bin/${_MSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi +_MSD_SHIM_NAME="msd-tools.cjs"; _MSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; MSD_TOOLS="${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}"; _msd_at() { for _p; do if [ -f "$_p" ]; then MSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _msd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@golem15/msd-core"'*'}') return 0;; *) return 1;; esac; }; _msd_homes() { _msd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msd-core/bin/${_MSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/msd-core/bin/${_MSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/msd-core/bin/${_MSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/msd-core/bin/${_MSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/msd-core/bin/${_MSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/msd-core/bin/${_MSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/msd-core/bin/${_MSD_SHIM_NAME}"; }; if _msd_at "${_MSD_RUNTIME_ROOT}/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.claude/msd-core/bin/${_MSD_SHIM_NAME}" "${_MSD_RUNTIME_ROOT}/.codex/msd-core/bin/${_MSD_SHIM_NAME}"; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif _msd_homes; then msd_run() { node "$MSD_TOOLS" "$@"; }; elif unset -f msd_run; _G="$(command -v msd_run)"; [ -n "$_G" ] && _msd_id_ok "$_G"; then MSD_TOOLS="$_G"; msd_run() { "$MSD_TOOLS" "$@"; }; else echo "ERROR: msd-tools.cjs not found at $MSD_TOOLS and no identity-proving msd_run is on PATH. Run: npx -y @golem15/msd-core@latest --claude --local" >&2; exit 1; fi; MSD_IDENTITY_STATUS=unverified; _msd_id_ok msd_run && MSD_IDENTITY_STATUS=ok; export MSD_IDENTITY_STATUS; [ "$MSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$MSD_TOOLS\" did not prove it is @golem15/msd-core - it is either a different package or an @golem15/msd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-msd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${MSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${MSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi msd_run query commit "docs($PHASE): UI design contract" --files "$PHASE_DIR/$PADDED_PHASE-UI-SPEC.md" ``` diff --git a/bin/install.js b/bin/install.js index a300d6fbd..0a8e70bf8 100755 --- a/bin/install.js +++ b/bin/install.js @@ -33,7 +33,6 @@ const { resolveAntigravityGlobalDir, getGlobalConfigDir, getGlobalSkillsBase, - resolveKimiHooksTomlDir, isRegisteredRuntimeId, } = require('../msd-core/bin/lib/runtime-homes.cjs'); // #2870: the Install Scope Module — turns a bare 'global' | 'local' scope id @@ -86,7 +85,7 @@ const INSTALLED_HOOK_FILES = new Set(_HOOKS_TO_COPY); // kept working — #2876 found zero production/test consumers of any of those // re-exports (tests import runtime-hooks-surface.cjs directly) and retired // them from module.exports. install.js still requires hooksSurface below for -// its own internal call sites (writeCursorHooksJson, writeClineArtifacts, +// its own internal call sites (writeCursorHooksJson, // resolveNodeRunner, applySettingsJsonHooks, etc.). const hooksSurface = require('../msd-core/bin/lib/runtime-hooks-surface.cjs'); @@ -297,36 +296,6 @@ function mergeClaudePermissions(settings) { } } -// Copilot instructions marker constants -const MSD_COPILOT_INSTRUCTIONS_MARKER = ''; -const MSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER = ''; - -// #786 \u2014 GitHub Copilot CLI lifecycle hook constants. -// Copilot reads hook configs from /hooks/*.json (repo scope: .github/hooks/, -// user scope: ~/.copilot/hooks/) with the shape { version, hooks: { : [...] } }. -// Events use camelCase (sessionStart, preToolUse, postToolUse, ...). A `command` -// hook runs an INLINE shell command (bash / powershell), so the MSD hook is fully -// self-contained \u2014 there is no separate hook script to install, and therefore -// nothing that can dangle if a script copy is skipped. See -// https://docs.github.com/en/copilot/reference/hooks-configuration -const MSD_COPILOT_HOOK_FILE = 'msd-session.json'; -// Copilot parses a command hook's stdout as the hook-output JSON. For sessionStart -// the schema is `{ additionalContext?: string }` (the text is prepended to the -// session as context). So the hook must emit that JSON envelope — not bare text. -// The two messages contain no JSON-special characters, so they embed verbatim. -const MSD_COPILOT_SESSION_MSG_PRESENT = - 'MSD: .planning/STATE.md present - review the current phase and any blockers before acting.'; -const MSD_COPILOT_SESSION_MSG_ABSENT = - 'MSD: no .planning/ workflow found - run /msd-new-project to start a tracked workflow.'; -const MSD_COPILOT_SESSION_HOOK_BASH = - 'if [ -f .planning/STATE.md ]; then ' + - `printf '%s' '{"additionalContext":"${MSD_COPILOT_SESSION_MSG_PRESENT}"}'; else ` + - `printf '%s' '{"additionalContext":"${MSD_COPILOT_SESSION_MSG_ABSENT}"}'; fi`; -const MSD_COPILOT_SESSION_HOOK_PWSH = - 'if (Test-Path .planning/STATE.md) ' + - `{ '{"additionalContext":"${MSD_COPILOT_SESSION_MSG_PRESENT}"}' } ` + - `else { '{"additionalContext":"${MSD_COPILOT_SESSION_MSG_ABSENT}"}' }`; - // #777 — Cursor CLI lifecycle hook constants. // Cursor reads hook configs from /.cursor/hooks.json (local) or // ~/.cursor/hooks.json (global) with the shape { version: 1, hooks: { : [...] } }. @@ -371,30 +340,6 @@ const MSD_CURSOR_HOOK_SCRIPTS = [ // Marker comment embedded in managed hook entries so MSD can find+remove them. const MSD_CURSOR_HOOK_MARKER = hooksSurface.MSD_CURSOR_HOOK_MARKER; -// #2100 Stage 2 — Windsurf/Cascade lifecycle hook constants. -// Windsurf/Cascade reads hook configs from /.windsurf/hooks.json -// (local) or ~/.codeium/windsurf/hooks.json (global) with the shape -// { hooks: { : [ { command, ... } ] } } — note: no top-level `version` -// field, and each entry carries a bare `command` shell string (no `type` -// field), unlike Cursor's hooks.json. MSD registers two managed BLOCKING -// hooks (exit code 2 to block, vs. Cursor's stdout-JSON form): -// pre_write_code → msd-windsurf-pre-write.js (write-path guard) -// pre_run_command → msd-windsurf-pre-command.js (destructive-command guard) -// Cascade has no context-injection channel, so the 4 advisory hooks MSD -// registers on Cursor (sessionStart, postToolUse, stop, subagentStart/Stop) -// have no Windsurf counterpart and are deliberately NOT ported. -// Cascade hooks docs (reference): https://docs.windsurf.com/llms-full.txt , -// https://docs.devin.ai/desktop/cascade/hooks -// -// Same #2876 fix as the Cursor block above: bare references to hooksSurface's -// own exports instead of a second, unlinked literal copy. -const MSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT = hooksSurface.MSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT; -const MSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT = hooksSurface.MSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT; -// All MSD-managed Windsurf hook scripts (used by uninstall cleanup). -// hooksSurface independently defines the same array — bare reference here so -// the two can never drift. -const MSD_WINDSURF_HOOK_SCRIPTS = hooksSurface.MSD_WINDSURF_HOOK_SCRIPTS; - // MSD-managed files under hooks/lib/ (helpers required by msd-*.js hooks). // git-cmd.js does not start with "msd-" (shared classifier for #3129), msd-graphify-rebuild.sh does. // cursor-workspace.js (#2587) is required by the Cursor lifecycle hooks. Those @@ -411,15 +356,9 @@ const MSD_HOOK_LIB_FILES = ['git-cmd.js', 'msd-graphify-rebuild.sh', 'cursor-wor /** * Directory name MSD stages its shared hook bundle under, inside a runtime's * install root. Defaults to 'hooks' — the name every runtime used before #3023. - * - * pi (pi.dev) reserves `hooks/` as its own now-deprecated extension location and - * prints a migration warning on every startup when one exists, so pi overrides - * this via hostBehaviors.sharedHooksDirName. Following pi's advised remediation - * (move it into extensions/) would break the adapter's path resolution AND expose - * MSD's .js helpers to pi's extension auto-discovery, so the bundle is renamed in - * place instead — same depth, so every `__dirname/..`-relative resolution inside - * the bundle (e.g. hooks/msd-context-monitor.js reaching ../msd-core/bin/) keeps - * working. + * A runtime may override it via hostBehaviors.sharedHooksDirName (single path + * segment, same depth, so every `__dirname/..`-relative resolution inside the + * bundle keeps working). */ const SHARED_HOOKS_DIR_DEFAULT = 'hooks'; @@ -523,23 +462,6 @@ const { extractToolsValue, } = require(path.join(__dirname, '..', 'msd-core', 'bin', 'lib', 'codex-agent-toml.cjs')); -// Copilot tool name mapping — Claude Code tools to GitHub Copilot tools -// Tool mapping applies ONLY to agents, NOT to skills (per CONTEXT.md decision) -const claudeToCopilotTools = { - Read: 'read', - Write: 'edit', - Edit: 'edit', - Bash: 'execute', - Grep: 'search', - Glob: 'search', - Task: 'agent', - WebSearch: 'web', - WebFetch: 'web', - TodoWrite: 'todo', - AskUserQuestion: 'ask_user', - SlashCommand: 'skill', -}; - // Get version from package.json const pkg = require('../package.json'); @@ -690,25 +612,6 @@ function _hostBehaviors(runtime) { return _resolveHostBehaviors(runtime, _capabilityRegistry); } -/** - * Read a runtime's documentation-sourced `hostIntegration.dispatch` axes - * (ADR-1239 Phase A — `capabilities//capability.json` - * `runtime.hostIntegration.dispatch`): `{namedDispatch, nested, maxDepth, - * background, backgroundDispatch, subagentToolkit}`. These are validated, - * closed-vocabulary FACTS about what the runtime's real dispatch primitive - * supports (never inferred) — see `docs/reference/host-integration-capability- - * matrix.md` for citations. Unlike `_hostBehaviors` (install *policy*), this is - * the negotiated *capability* surface; #2284 is its first content-projection - * consumer (previously read only by `shouldFlattenDispatch`). Returns `{}` if - * the registry or the runtime's descriptor is unavailable, so callers must - * treat every axis as absent/unknown (fail-closed) rather than assume a value. - */ -function _hostIntegrationDispatch(runtime) { - const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime]; - const dispatch = cap && cap.runtime && cap.runtime.hostIntegration && cap.runtime.hostIntegration.dispatch; - return dispatch || {}; -} - /** * #2870: shared install()/uninstall() scope resolution. Routes `id` through * the Install Scope Module (src/install-scope.cts) and degrades to `null` on @@ -760,8 +663,7 @@ function _kindDestDirSafe(runtime, configDir, scope, kindName) { * that are inert or invalid in a foreign harness surface only at dispatch * time, months later. Warn-and-proceed is the issue-sanctioned option (b): * a fresh custom dir (the documented brand-specific-dir use), a msd-only dir - * (updates, the --all shared dir — including kimi's root `msd.md`, which is - * MSD-owned despite the bare `msd` stem), and the no-flag default-home path + * (updates, the --all shared dir), and the no-flag default-home path * (users keep personal agents in ~/.claude/agents) all stay silent. Degrades * silently on any resolution failure — the warn path never blocks install. */ @@ -769,10 +671,7 @@ function warnIfForeignAgentDest(runtime, targetDir, scope, explicitConfigDir) { if (explicitConfigDir !== true) return; try { const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope); - // kimi's global agents kind is `kimi-agents` (#2095 EoS), every other - // runtime's is `agents`. - const agentsDir = _kindDestDir(layout, 'agents', targetDir) - || _kindDestDir(layout, 'kimi-agents', targetDir); + const agentsDir = _kindDestDir(layout, 'agents', targetDir); if (!agentsDir) return; if (!fs.existsSync(agentsDir) || !fs.statSync(agentsDir).isDirectory()) return; const foreign = fs @@ -856,13 +755,12 @@ const { // getCommitAttribution STAYS here (impure install-time config I/O); it is injected // into the engine functions via the resolveAttribution parameter at each call site. const installEngine = require(path.join(_msdLibDir, 'install-engine.cjs')); -// #2876: _copyStaged, convertClaudeCommandToOpencodeSkill, and -// convertClaudeCommandToKiloSkill used to be destructured here too — all -// three had no install.js internal caller and no export consumer (tests -// import all three directly from msd-core/bin/lib/install-engine.cjs), so -// the retired bindings were dead code. applyOpencodeFamilyPathPrefix, -// _runLegacyInstallMigrations, _runLegacyUninstallCleanup, -// _removeMsdEntries, _restoreDir, and _removeHermesBareStemDirs were the +// #2876: _copyStaged and convertClaudeCommandToOpencodeSkill used to be +// destructured here too — both had no install.js internal caller and no +// export consumer (tests import them directly from +// msd-core/bin/lib/install-engine.cjs), so the retired bindings were dead +// code. applyOpencodeFamilyPathPrefix, _runLegacyInstallMigrations, +// _runLegacyUninstallCleanup, _removeMsdEntries, and _restoreDir were the // same — unused local bindings that were never part of this module's export // surface either — found and retired in the same sweep. const { @@ -870,10 +768,8 @@ const { uninstallRuntimeArtifacts, installOpencodeFamilySkills, installAgentsKindStandalone, - _installNativePluginIfDeclared, hasExistingSymlinkBetween, isSymlinkedDestOptIn, - migrateLegacyDevPreferencesToSkill, USER_OWNED_ARTIFACTS, _snapshotDir, } = installEngine; @@ -956,17 +852,6 @@ const hasDryRun = args.includes('--dry-run'); // out of sync the way five signatures can. const hasRelativeIncludes = args.includes('--relative-includes') || process.env.MSD_RELATIVE_INCLUDES === '1'; if (hasRelativeIncludes) process.env.MSD_RELATIVE_INCLUDES = '1'; -// #3031: opt-in reclaim of the MSD artifacts a PRE-#2755 `--kimi-code` install -// orphaned in Kimi CLI's `~/.kimi`. Opt-in and not automatic because the stale -// block is BYTE-IDENTICAL to a legitimate Kimi CLI one — both runtimes render -// the same bytes for the same root, since the command paths derive from the -// hooks root and not from the runtime — so no inspection can tell "litter MSD -// wrote for kimi-code" from "Kimi CLI's working hooks". Cleaning unasked would -// break #2755's own acceptance criterion ("Uninstalling MSD hooks for one -// runtime does not touch or remove the other runtime's hooks") for anyone with -// both products installed. The user, who knows which products they run, is the -// only party that can decide — so they ask for it explicitly. -const hasReclaimKimiLegacy = args.includes('--reclaim-kimi-legacy'); // --profile= or --profile=, (composable); mutually exclusive with --minimal const _profileArgRaw = (() => { for (const arg of args) { @@ -990,7 +875,7 @@ if (hasMinimal && _profileArgRaw) { function selectRuntimesFromArgs(runtimeArgs) { if (runtimeArgs.includes('--all')) { - return ['claude', 'kimi', 'kimi-code', 'kilo', 'opencode', 'pi', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'zcode']; + return ['claude', 'opencode', 'codex', 'antigravity', 'cursor', 'zcode']; } if (runtimeArgs.includes('--both')) { return ['claude', 'opencode']; @@ -999,21 +884,9 @@ function selectRuntimesFromArgs(runtimeArgs) { const selected = []; if (runtimeArgs.includes('--claude')) selected.push('claude'); if (runtimeArgs.includes('--opencode')) selected.push('opencode'); - if (runtimeArgs.includes('--pi')) selected.push('pi'); - if (runtimeArgs.includes('--kilo')) selected.push('kilo'); if (runtimeArgs.includes('--codex')) selected.push('codex'); - if (runtimeArgs.includes('--copilot')) selected.push('copilot'); if (runtimeArgs.includes('--antigravity')) selected.push('antigravity'); if (runtimeArgs.includes('--cursor')) selected.push('cursor'); - if (runtimeArgs.includes('--windsurf') || runtimeArgs.includes('--devin-desktop')) selected.push('windsurf'); - if (runtimeArgs.includes('--augment')) selected.push('augment'); - if (runtimeArgs.includes('--trae')) selected.push('trae'); - if (runtimeArgs.includes('--qwen')) selected.push('qwen'); - if (runtimeArgs.includes('--hermes')) selected.push('hermes'); - if (runtimeArgs.includes('--kimi')) selected.push('kimi'); - if (runtimeArgs.includes('--kimi-code')) selected.push('kimi-code'); - if (runtimeArgs.includes('--codebuddy')) selected.push('codebuddy'); - if (runtimeArgs.includes('--cline')) selected.push('cline'); if (runtimeArgs.includes('--zcode')) selected.push('zcode'); return selected; } @@ -1021,77 +894,6 @@ function selectRuntimesFromArgs(runtimeArgs) { // Runtime selection - can be set by flags or interactive prompt let selectedRuntimes = selectRuntimesFromArgs(args); -// #2505 Phase 5: Kimi variant disambiguation (#2513). Kimi CLI (Python, ~/.kimi/) -// and Kimi Code (Node, ~/.kimi-code/) are two distinct Moonshot products that -// share the "kimi" brand. Probe for each product's config.toml and warn when -// the selected runtime doesn't match the detected install — catches the common -// "ran --kimi --global but actually on Kimi Code" mistake that produced inert -// YAMLs and empty agent-skills before the Phase 1 descriptor split. -function disambiguateKimiVariant(runtimes) { - const home = os.homedir(); - const hasKimiCli = fs.existsSync(path.join(home, '.kimi', 'config.toml')); - const hasKimiCode = fs.existsSync(path.join(home, '.kimi-code', 'config.toml')); - const notices = []; - if (runtimes.includes('kimi') && hasKimiCode && !hasKimiCli) { - notices.push({ - kind: 'wrong-variant', - selected: 'kimi', - detected: 'kimi-code', - message: `Detected ~/.kimi-code/config.toml (Kimi Code, Node CLI) but not ~/.kimi/config.toml (Kimi CLI, Python). You selected --kimi but appear to be on Kimi Code. Re-run with --kimi-code for a working install. (Kimi CLI = Python kimi-cli with named subagents; Kimi Code = Node CLI with coder/explore/plan built-ins only.)`, - }); - } - if (runtimes.includes('kimi-code') && hasKimiCli && !hasKimiCode) { - notices.push({ - kind: 'wrong-variant', - selected: 'kimi-code', - detected: 'kimi', - message: `Detected ~/.kimi/config.toml (Kimi CLI, Python) but not ~/.kimi-code/config.toml (Kimi Code, Node CLI). You selected --kimi-code but appear to be on Kimi CLI. Re-run with --kimi for a working install.`, - }); - } - // Distinct-entry descriptions when either Kimi variant is selected. - if (runtimes.includes('kimi')) { - notices.push({ - kind: 'description', - runtime: 'kimi', - message: 'Kimi CLI (Python kimi-cli): named subagents via YAML, config at ~/.kimi/, hooks via ~/.kimi/config.toml [[hooks]].', - }); - } - if (runtimes.includes('kimi-code')) { - notices.push({ - kind: 'description', - runtime: 'kimi-code', - message: 'Kimi Code (Node CLI): three built-in subagents (coder/explore/plan), Agent Skills at ~/.kimi-code/skills/, config at ~/.kimi-code/.', - }); - } - return notices; -} - -// #3031: `--reclaim-kimi-legacy` only ever acts inside the kimi-code GLOBAL -// install branch. Say so when it cannot act, rather than exiting 0 having -// silently done nothing: the user asked for a cleanup, and silence is -// indistinguishable from "it ran and found nothing". Not a hard error — it -// stays composable with `--all`, where it is legitimately inert for the other -// seventeen runtimes. -if (hasReclaimKimiLegacy && !selectedRuntimes.includes('kimi-code')) { - console.error(`${yellow}⚠ --reclaim-kimi-legacy ignored${reset} — it applies only to a --kimi-code install; nothing in ~/.kimi was touched.`); -} else if (hasReclaimKimiLegacy && hasLocal) { - // Scope, checked HERE rather than inside install(): kimi-code declares - // hostBehaviors.localInstallDeferred, so install() returns early long before - // the kimi-hooks-toml branch — a warning placed there would be unreachable. - console.error(`${yellow}⚠ --reclaim-kimi-legacy ignored${reset} — the legacy root is a global location; re-run with --global to reclaim it.`); -} - -if (selectedRuntimes.includes('kimi') || selectedRuntimes.includes('kimi-code')) { - const kimiNotices = disambiguateKimiVariant(selectedRuntimes); - for (const notice of kimiNotices) { - if (notice.kind === 'wrong-variant') { - console.error(`${yellow}⚠ Kimi variant mismatch (${notice.selected} → ${notice.detected}).${reset} ${notice.message}`); - } else if (notice.kind === 'description') { - console.log(`${dim} ${notice.runtime}: ${notice.message}${reset}`); - } - } -} - // #1928: Google sunset Gemini CLI on 2026-06-18; Antigravity CLI is its // official successor. `--gemini` is no longer a valid runtime selector — // selectRuntimesFromArgs above no longer recognizes it, so it never lands in @@ -1164,7 +966,7 @@ Then re-run: npx ${pkg.name}@latest /** * Get the config directory path relative to home directory for a runtime * Used for templating hooks that use path.join(homeDir, '', ...) - * @param {string} runtime - 'claude', 'opencode', 'codex', or 'copilot' + * @param {string} runtime - 'claude', 'opencode', 'codex', ... * @param {boolean} isGlobal - Whether this is a global install */ function getConfigDirFromHome(runtime, isGlobal) { @@ -1180,10 +982,7 @@ function getConfigDirFromHome(runtime, isGlobal) { // `runtime === 'antigravity'` literal into a read of the runtime's // `hostBehaviors.globalDirResolver` descriptor field (via _hostBehaviors, which // also degrades to FALLBACK_HOST_BEHAVIORS on registry-load failure). This is - // antigravity-unique: unlike `configHome.kind === 'dot-home-nested'` (which - // windsurf also declares — see capabilities/windsurf/capability.json — and - // would wrongly route windsurf's global dir through - // resolveAntigravityGlobalDir), `globalDirResolver` is only set by antigravity. + // antigravity-unique: `globalDirResolver` is only set by antigravity. if (_hostBehaviors(runtime).globalDirResolver === 'antigravity') { const antigravityDir = resolveAntigravityGlobalDir(); const rel = path.relative(os.homedir(), antigravityDir); @@ -1218,7 +1017,7 @@ const banner = '\n' + ' MSD Core ' + dim + 'v' + pkg.version + reset + '\n' + ' Make Software Done.\n' + ' A meta-prompting, context engineering and spec-driven\n' + - ' development workflows for Claude Code, OpenCode, Kimi CLI, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline, CodeBuddy, ZCode and pi.\n'; + ' development workflows for Claude Code, Codex, OpenCode, Cursor, ZCode and Antigravity.\n'; // Pure seam: parse --config-dir / -c from an arbitrary args array. // Returns the path string, '' for an empty equals-form value, or null when the @@ -1283,7 +1082,7 @@ if (hasUninstall) { // Show help if requested if (hasHelp) { - console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--kimi-code${reset} Install for Kimi Code only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--zcode${reset} Install for ZCode only\n ${cyan}--pi${reset} Install for Pi only\n ${cyan}--gemini${reset} Install for Gemini CLI only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall MSD (remove all MSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}--no-legacy-cleanup${reset} Skip the legacy get-shit-done-cc artifact scan\n (an explicit --config-dir already scopes the scan to it)\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n and resolve the node runner at hook-fire time via\n hooks/msd-node-runner.sh (WSL/Docker bind-mount\n setups; also MSD_PORTABLE_HOOKS=1)\n ${cyan}--relative-includes${reset} With --local: write project-relative @ includes\n (.claude/msd-core/...) instead of this checkout's\n absolute path, so several git worktrees of one repo\n each read their own copy (also MSD_RELATIVE_INCLUDES=1)\n ${cyan}--reclaim-kimi-legacy${reset} With --kimi-code: also remove the MSD hooks a\n pre-1.10.0 --kimi-code install orphaned in ~/.kimi.\n Opt-in — those artifacts are indistinguishable from\n Kimi CLI's own, so skip it if you use Kimi CLI too.\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`msd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi Code globally (its own ~/.kimi-code root)${reset}\n npx ${pkg.name} --kimi-code --global\n\n ${dim}# Kimi Code, also reclaiming hooks a pre-1.10.0 install left in ~/.kimi${reset}\n npx ${pkg.name} --kimi-code --global --reclaim-kimi-legacy\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline globally${reset}\n npx ${pkg.name} --cline --global\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Local install for a repo worked from several git worktrees${reset}\n npx ${pkg.name} --claude --local --relative-includes\n\n ${dim}# Uninstall MSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / COPILOT_HOME / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to the first existing generic skills root: ${cyan}~/.config/agents/skills${reset}, then ${cyan}~/.agents/skills${reset}; if neither exists, MSD creates ${cyan}~/.config/agents${reset}.\n Kimi CLI and Kimi Code are separate products with separate hook roots: use ${cyan}--kimi${reset} (${cyan}~/.kimi${reset}, ${cyan}KIMI_SHARE_DIR${reset}) or ${cyan}--kimi-code${reset} (${cyan}~/.kimi-code${reset}, ${cyan}KIMI_CODE_HOME${reset}).\n`); + console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--zcode${reset} Install for ZCode only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall MSD (remove all MSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}--no-legacy-cleanup${reset} Skip the legacy get-shit-done-cc artifact scan\n (an explicit --config-dir already scopes the scan to it)\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n and resolve the node runner at hook-fire time via\n hooks/msd-node-runner.sh (WSL/Docker bind-mount\n setups; also MSD_PORTABLE_HOOKS=1)\n ${cyan}--relative-includes${reset} With --local: write project-relative @ includes\n (.claude/msd-core/...) instead of this checkout's\n absolute path, so several git worktrees of one repo\n each read their own copy (also MSD_RELATIVE_INCLUDES=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`msd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Local install for a repo worked from several git worktrees${reset}\n npx ${pkg.name} --claude --local --relative-includes\n\n ${dim}# Uninstall MSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / CODEX_HOME / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / ZCODE_CONFIG_DIR environment variables.\n`); process.exit(0); } @@ -1314,10 +1113,6 @@ const referencesHook = hooksSurface.referencesHook; // applySettingsJsonHooks: mutates settings.hooks.* in place with all MSD-managed // hook registrations for settings.json-surface runtimes (ADR-857 phase 5f-1b). const applySettingsJsonHooks = hooksSurface.applySettingsJsonHooks; -// writeKimiHooksToml / removeKimiHooksToml: kimi's native config.toml [[hooks]] -// surface (#2095 EoS/kimi Upgrade 1) — separate from settings.json entirely. -const writeKimiHooksToml = hooksSurface.writeKimiHooksToml; -const removeKimiHooksToml = hooksSurface.removeKimiHooksToml; // processAttribution: pure Co-Authored-By content transform, relocated to the // conversion module (ADR-1508 / #1510 Phase 1). Bound here so install.js // callers continue to work and there is a single implementation. (All call @@ -1328,26 +1123,12 @@ const filterRuntimeNotesForTarget = runtimeArtifactConversion.filterRuntimeNotes // (ADR-1508 / #1511 Phase 2 — single owner). Re-bound here so install.js call // sites continue to work. #2876 retired the sibling // applyRuntimeContentRewritesInPlace / applyRuntimeContentRewritesForCommandsInPlace -// re-bindings that used to sit alongside it, and the entire #1675 Augment -// converter family re-binding (convertClaudeToAugmentMarkdown / -// convertClaudeCommandToAugmentSkill / convertClaudeAgentToAugmentAgent) that -// used to follow — bin/install.js had no internal caller for any of them (the -// descriptor pipeline in runtimeArtifactConversion calls them directly). +// re-bindings that used to sit alongside it. // (All call sites are below this line → no TDZ hazard.) const computePathPrefix = runtimeArtifactConversion._computePathPrefix; -// #2931 (ADR-1508): the windsurf converter family is single-sourced in the -// conversion module. install.js re-binds (does not re-define) the one member -// it still calls internally so there is exactly one body — the -// generative-drift hazard the dedup removes. #2876 retired the sibling -// convertClaudeCommandToWindsurfSkill / convertClaudeCommandToWindsurfWorkflow / -// convertClaudeAgentToWindsurfAgent re-bindings — bin/install.js had no -// internal caller for any of them (the descriptor pipeline in -// runtimeArtifactConversion calls them directly). -// (All call sites are below this line → no TDZ hazard.) -const convertClaudeToWindsurfMarkdown = runtimeArtifactConversion.convertClaudeToWindsurfMarkdown; // #2931 (ADR-1508): single-sourced in the conversion module — was a second, -// unlinked verbatim copy here (used by the local Cursor/Trae/CodeBuddy/Cline -// converters below), the exact drift class this PR exists to reduce. Verified +// unlinked verbatim copy here (used by the local Cursor converter below), +// the exact drift class this PR exists to reduce. Verified // behaviorally identical (no block / one block / adjacent blocks / whole- // content block / unclosed opening tag / nested-looking tags / repeated // sequential calls for global-regex lastIndex leakage) before merging. @@ -1448,19 +1229,8 @@ function resolveOpencodeConfigPath(configDir) { return path.join(configDir, 'opencode.json'); } -/** - * Resolve the Kilo config file path, preferring .jsonc if it exists. - */ -function resolveKiloConfigPath(configDir) { - const jsoncPath = path.join(configDir, 'kilo.jsonc'); - if (fs.existsSync(jsoncPath)) { - return jsoncPath; - } - return path.join(configDir, 'kilo.json'); -} - // #2087 — attribution config-path resolvers, keyed by descriptor (hostBehaviors.attributionConfigResolver) -const ATTRIBUTION_CONFIG_RESOLVERS = { opencode: resolveOpencodeConfigPath, kilo: resolveKiloConfigPath }; +const ATTRIBUTION_CONFIG_RESOLVERS = { opencode: resolveOpencodeConfigPath }; /** * Strip JSONC comments (// and /* *​/) from a string to produce valid JSON. @@ -1562,7 +1332,7 @@ function writeSettings(settingsPath, settings) { // and this installer resolve model_overrides / model_profile_overrides // through the SAME code — a single source of truth for the precedence chain // the inline agent loop below used to duplicate across ~24 lines per runtime -// (kilo, opencode). See install-model-override-resolver.cts's module doc. +// (opencode). See install-model-override-resolver.cts's module doc. const { readMsdEffectiveModelOverrides, readMsdRuntimeProfileResolver, @@ -1583,7 +1353,7 @@ const attributionCache = new Map(); /** * Get commit attribution setting for a runtime - * @param {string} runtime - 'claude', 'opencode', 'codex', or 'copilot' + * @param {string} runtime - 'claude', 'opencode', 'codex', ... * @returns {null|undefined|string} null = remove, undefined = keep default, string = custom */ function getCommitAttribution(runtime) { @@ -1610,7 +1380,7 @@ function getCommitAttribution(runtime) { result = settings.attribution.commit; } } else { - // Codex and Copilot currently have no attribution setting equivalent + // Codex currently has no attribution setting equivalent result = undefined; } @@ -1671,35 +1441,6 @@ const claudeToAntigravityTools = { TodoWrite: 'write_todos', } -// Tool name mapping from Claude/MSD agents to Kimi CLI module paths. -// Kimi custom agent YAML requires fully-qualified module paths. -const claudeToKimiTools = { - Read: 'kimi_cli.tools.file:ReadFile', - ReadFile: 'kimi_cli.tools.file:ReadFile', - Write: 'kimi_cli.tools.file:WriteFile', - WriteFile: 'kimi_cli.tools.file:WriteFile', - Edit: 'kimi_cli.tools.file:StrReplaceFile', - MultiEdit: 'kimi_cli.tools.file:StrReplaceFile', - StrReplaceFile: 'kimi_cli.tools.file:StrReplaceFile', - Bash: 'kimi_cli.tools.shell:Shell', - Shell: 'kimi_cli.tools.shell:Shell', - Grep: 'kimi_cli.tools.file:Grep', - Glob: 'kimi_cli.tools.file:Glob', - Agent: 'kimi_cli.tools.agent:Agent', - Task: 'kimi_cli.tools.agent:Agent', - AskUserQuestion: 'kimi_cli.tools.ask_user:AskUserQuestion', - TodoWrite: 'kimi_cli.tools.todo:SetTodoList', - SetTodoList: 'kimi_cli.tools.todo:SetTodoList', - WebSearch: 'kimi_cli.tools.web:SearchWeb', - SearchWeb: 'kimi_cli.tools.web:SearchWeb', - WebFetch: 'kimi_cli.tools.web:FetchURL', - FetchURL: 'kimi_cli.tools.web:FetchURL', - ReadMediaFile: 'kimi_cli.tools.file:ReadMediaFile', - TaskList: 'kimi_cli.tools.background:TaskList', - TaskOutput: 'kimi_cli.tools.background:TaskOutput', - TaskStop: 'kimi_cli.tools.background:TaskStop', -}; - /** * Convert a Claude Code tool name to OpenCode format * - Applies special mappings (AskUserQuestion -> question, etc.) @@ -1755,234 +1496,6 @@ function convertAntigravityToolName(claudeTool) { return claudeTool.toLowerCase(); } -function createKimiToolDiagnostic(reason, tool, source = null) { - const isMcp = reason === 'mcp_managed'; - return { - level: 'warning', - code: isMcp ? 'kimi_mcp_tool_excluded' : 'kimi_unsupported_tool', - reason, - message: isMcp - ? `MCP-managed tool '${tool}' is configured outside Kimi agent YAML.` - : `Tool '${tool}' is not supported by the Kimi tool mapper.`, - value: tool, - source, - }; -} - -/** - * Convert a Claude/MSD tool name to a Kimi CLI module path. - * @returns {string|null} Kimi module path, or null when excluded/unsupported. - */ -function convertKimiToolName(claudeTool) { - const tool = String(claudeTool || '').trim(); - if (!tool) return null; - if (tool.startsWith('mcp__')) return null; - return claudeToKimiTools[tool] || null; -} - -function mapClaudeToolsToKimiTools(claudeTools, options = {}) { - const diagnostics = []; - const tools = []; - const seen = new Set(); - const source = options && Object.prototype.hasOwnProperty.call(options, 'source') - ? options.source - : null; - - for (const rawTool of Array.isArray(claudeTools) ? claudeTools : []) { - const tool = String(rawTool || '').trim(); - if (!tool) continue; - - if (tool.startsWith('mcp__')) { - diagnostics.push(createKimiToolDiagnostic('mcp_managed', tool, source)); - continue; - } - - const kimiTool = convertKimiToolName(tool); - if (!kimiTool) { - diagnostics.push(createKimiToolDiagnostic('unsupported_tool', tool, source)); - continue; - } - - if (!seen.has(kimiTool)) { - seen.add(kimiTool); - tools.push(kimiTool); - } - } - - return { tools, diagnostics }; -} - -const claudeToKiloAgentPermissions = { - Read: 'read', - Write: 'edit', - Edit: 'edit', - Bash: 'bash', - Grep: 'grep', - Glob: 'glob', - Task: 'task', - WebFetch: 'webfetch', - WebSearch: 'websearch', - TodoWrite: 'todowrite', - AskUserQuestion: 'question', - SlashCommand: 'skill', -}; - -const kiloAgentPermissionOrder = [ - 'read', - 'edit', - 'bash', - 'grep', - 'glob', - 'task', - 'webfetch', - 'websearch', - 'skill', - 'question', - 'todowrite', - 'list', - 'codesearch', - 'lsp', -]; - -const kiloMcpPermissionPattern = /^mcp__([A-Za-z0-9_-]+)__((?:[A-Za-z0-9_-]+)|\*)$/; - -// Derives Kilo's native `{server}_{tool}` MCP permission key (kilo.ai/docs — -// external, fixed format). Not injective: both capture groups allow `_`, so -// e.g. `mcp__a_b__c` and `mcp__a__b_c` derive the same key. The `Set` below -// resolves any such collision deterministically to first-seen-wins — see -// src/runtime-artifact-conversion.cts's regression test. Mirrors that file's -// convertClaudeToKiloPermissionTool exactly (#4032). -function convertClaudeToKiloPermissionTool(claudeTool) { - const builtinPermission = claudeToKiloAgentPermissions[claudeTool]; - if (builtinPermission) return builtinPermission; - - const mcpPermission = kiloMcpPermissionPattern.exec(claudeTool); - return mcpPermission ? `${mcpPermission[1]}_${mcpPermission[2]}` : null; -} - -function buildKiloAgentPermissionBlock(claudeTools) { - const allowedPermissions = new Set(); - - for (const tool of claudeTools) { - const mapped = convertClaudeToKiloPermissionTool(tool); - if (mapped) { - allowedPermissions.add(mapped); - } - } - - const lines = ['permission:']; - for (const permission of kiloAgentPermissionOrder) { - lines.push(` ${permission}: ${allowedPermissions.has(permission) ? 'allow' : 'deny'}`); - } - for (const permission of allowedPermissions) { - if (kiloAgentPermissionOrder.includes(permission)) continue; - lines.push(` ${permission}: allow`); - } - - return lines; -} - -function replaceRelativePathReference(content, fromPath, toPath) { - const escapedPath = escapeRegExp(fromPath); - return content.replace( - new RegExp(`(^|[^A-Za-z0-9_./-])${escapedPath}`, 'g'), - (_, prefix) => `${prefix}${toPath}`, - ); -} - -/** - * Convert a Claude Code tool name to GitHub Copilot format. - * - Applies explicit mapping from claudeToCopilotTools - * - Handles mcp__context7__* prefix → io.github.upstash/context7/* - * - Falls back to lowercase for unknown tools - */ -function convertCopilotToolName(claudeTool) { - // mcp__context7__* wildcard → io.github.upstash/context7/* - if (claudeTool.startsWith('mcp__context7__')) { - return 'io.github.upstash/context7/' + claudeTool.slice('mcp__context7__'.length); - } - // Check explicit mapping - if (claudeToCopilotTools[claudeTool]) { - return claudeToCopilotTools[claudeTool]; - } - // mcp__{tavily,ref,jina,exa,firecrawl}__* use the generic MCP passthrough like exa/firecrawl; - // add explicit Copilot registry mappings when the io.github ids are confirmed (#657 follow-up) - // Default: lowercase - return claudeTool.toLowerCase(); -} - -/** - * Apply Copilot-specific content conversion — CONV-06 (paths) + CONV-07 (command names). - * Path mappings depend on install mode: - * Global: ~/.claude/ → ~/.copilot/, ./.claude/ → ./.github/ - * Local: ~/.claude/ → ./.github/, ./.claude/ → ./.github/ - * Applied to ALL Copilot content (skills, agents, engine files). - * @param {string} content - Source content to convert - * @param {boolean} [isGlobal=false] - Whether this is a global install - */ -function convertClaudeToCopilotContent(content, isGlobal = false) { - let c = content; - // CONV-06: Path replacement — most specific first to avoid substring matches. - // Handle both `~/.claude/foo` (trailing slash) and bare `~/.claude` forms in - // one pass via a capture group, matching the approach used by Antigravity, - // OpenCode, Kilo, and Codex converters (issue #2545). - if (isGlobal) { - c = c.replace(/\$HOME\/\.claude(\/|\b)/g, '$HOME/.copilot$1'); - c = c.replace(/~\/\.claude(\/|\b)/g, '~/.copilot$1'); - } else { - c = c.replace(/\$HOME\/\.claude\//g, '.github/'); - c = c.replace(/~\/\.claude\//g, '.github/'); - c = c.replace(/\$HOME\/\.claude\b/g, '.github'); - c = c.replace(/~\/\.claude\b/g, '.github'); - } - c = c.replace(/\.\/\.claude\//g, './.github/'); - c = c.replace(/\.claude\//g, '.github/'); - // CONV-07: Command name conversion (all msd: references → msd-) - c = c.replace(/msd:/g, 'msd-'); - // Runtime-neutral agent name replacement (#766) - c = neutralizeAgentReferences(c, 'copilot-instructions.md'); - return c; -} - -// isGlobal is the 5th positional arg (3rd/4th are runtime/cmdNames passed by the skills wrapper). See runtime-artifact-layout skillsKind. -/** - * Convert a Claude command (.md) to a Copilot skill (SKILL.md). - * Transforms frontmatter only — body passes through with CONV-06/07 applied. - * Skills keep original tool names (no mapping) per CONTEXT.md decision. - */ -function convertClaudeCommandToCopilotSkill(content, skillName, _runtime = null, _cmdNames = null, isGlobal = false) { - const converted = convertClaudeToCopilotContent(content, isGlobal); - const { frontmatter, body } = extractFrontmatterAndBody(converted); - if (!frontmatter) return converted; - - const description = extractFrontmatterField(frontmatter, 'description') || ''; - const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint'); - const agent = extractFrontmatterField(frontmatter, 'agent'); - - // CONV-02: Extract allowed-tools YAML multiline list → comma-separated string - const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m); - let toolsLine = ''; - if (toolsMatch) { - const tools = toolsMatch[1].match(/^\s+-\s+(.+)/gm); - if (tools) { - toolsLine = tools.map(t => t.replace(/^\s+-\s+/, '').trim()).join(', '); - } - } - - // Reconstruct frontmatter in Copilot format - // #2876: descriptions starting with a YAML flow indicator (`[BETA] …`, - // `{ … }`, `*ref`, `&anchor`, etc.) parse as flow sequences/mappings and - // crash gh-copilot's frontmatter loader. Always quote so any leading - // character is parser-safe. - let fm = `---\nname: ${skillName}\ndescription: ${yamlQuote(description)}\n`; - if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`; - if (agent) fm += `agent: ${agent}\n`; - if (toolsLine) fm += `allowed-tools: ${toolsLine}\n`; - fm += '---'; - - return `${fm}\n${body}`; -} - /** * Map a skill directory name (msd-) to the frontmatter `name:` used * by Claude Code as the skill identity. Emits the hyphen form (msd-) @@ -2002,40 +1515,6 @@ function skillFrontmatterName(skillDirName) { return skillDirName; } -/** - * Qwen Code skills accept an optional numeric `priority` frontmatter field. - * Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified - * #778): HIGHER values sort EARLIER in the `/skills` TUI listing (omitted ≈ 0; - * negatives sort below unset). It affects ONLY the `/skills` list order — - * slash-command completion and the `/help` view stay alphabetical. - * - * We assign descending priorities to MSD's main-loop commands so the most-used - * workflow skills surface first; utility skills are deliberately left unset - * (default 0) and sort below. - * - * NOTE: the #778 issue body proposed the INVERSE numbering (plan-phase: 10, - * utilities: 90+). The verified spec shows that would BURY the core loop below - * utilities, so we implement the spec-correct direction (core = high) instead. - * Keyed by command stem (skill dir is `msd-`). - */ -const QWEN_SKILL_PRIORITY = Object.freeze({ - 'new-project': 100, - 'discuss-phase': 95, - 'plan-phase': 90, - 'execute-phase': 85, - progress: 80, - 'verify-work': 75, - phase: 70, - review: 65, - ship: 60, - config: 55, - surface: 50, - 'resume-work': 45, - 'pause-work': 40, - help: 35, - update: 30, -}); - /** * Convert a Claude command (.md) to a Claude skill (SKILL.md). * Claude Code is the native format, so minimal conversion needed — @@ -2043,13 +1522,13 @@ const QWEN_SKILL_PRIORITY = Object.freeze({ * Emits `name: msd-` (hyphen) so Skill(skill="msd-") calls and * tab autocomplete use the canonical command namespace. */ -function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, cmdNames = null) { +function convertClaudeCommandToClaudeSkill(content, skillName, _runtime = null, cmdNames = null) { const { frontmatter, body } = extractFrontmatterAndBody(content); if (!frontmatter) return content; // #3583: rewrite any /msd: or msd: in the body to the canonical // hyphen form (msd-) so installed SKILL.md bodies match the hyphen - // `name:` Claude Code (and Qwen/Hermes) register under (#2808). `cmdNames` + // `name:` Claude Code registers under (#2808). `cmdNames` // is optional and pre-computed by the caller for performance; direct test // calls fall back to reading the list. const names = cmdNames || readMsdCommandNames(); @@ -2081,24 +1560,6 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c // Reconstruct frontmatter in Claude skill format const frontmatterName = skillFrontmatterName(skillName); let fm = `---\nname: ${frontmatterName}\ndescription: ${yamlQuote(description)}\n`; - // Hermes' SKILL.md spec lists `version` as a required frontmatter field. - // Track MSD's package version so Hermes' skill_view() reports a stable - // identifier per install. - if (_hostBehaviors(runtime).skillFrontmatterVersion) fm += `version: ${yamlQuote(pkg.version)}\n`; - // #778 (b) — numeric priority for /skills ordering, declared on the runtime - // descriptor (runtime.hostBehaviors.skillPriorityFrontmatter). Scoped to - // runtimes that declare the flag so Claude/Hermes skill frontmatter is - // unchanged (they ignore the field, but we keep their output byte-stable). - // skillName is the `msd-` dir name. (ADR-1239 / #2086) - if (_hostBehaviors(runtime).skillPriorityFrontmatter) { - const stem = typeof skillName === 'string' && skillName.startsWith('msd-') - ? skillName.slice(4) - : skillName; - const priority = Object.prototype.hasOwnProperty.call(QWEN_SKILL_PRIORITY, stem) - ? QWEN_SKILL_PRIORITY[stem] - : undefined; - if (typeof priority === 'number') fm += `priority: ${priority}\n`; - } if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`; if (agent) fm += `agent: ${agent}\n`; // #769: emit context: when present so the runtime can honour it natively @@ -2114,334 +1575,6 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c return `${fm}\n${normalizedBody}`; } -function normalizeKimiSkillName(skillName) { - let text = String(skillName || '').trim().toLowerCase(); - if (text.startsWith('/')) text = text.slice(1); - if (text.startsWith('$')) text = text.slice(1); - text = text.replace(/^msd:/, 'msd-'); - if (!text.startsWith('msd-')) text = `msd-${text}`; - text = text.replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); - return text || 'msd-command'; -} - -function convertMsdCommandReferencesToKimiSkillInvocations(content, cmdNames) { - if (!Array.isArray(cmdNames) || cmdNames.length === 0) return content; - const commands = [...cmdNames].sort((a, b) => b.length - a.length).map(escapeRegExp); - const commandGroup = commands.join('|'); - const colonPattern = new RegExp(`(? `/skill:msd-${cmd}`) - .replace(hyphenPattern, (_, cmd) => `/skill:msd-${cmd}`); -} - -// DEFECT.GENERATIVE-FIX: this body is mirrored in -// src/runtime-artifact-conversion.cts's convertClaudeCommandToKimiSkill (dead -// for the live skills-install path, which routes here via -// install-engine.cts's SKILLS_CONVERTER_REGISTRY through the kimi capability -// descriptor's artifactLayout `converter: "convertClaudeCommandToKimiSkill"`; -// kept for bin/install.js's own module-level export/test surface). Neither -// copy re-exports the other — mirror any behavior change into both. Guarded -// by the output-parity test in tests/runtime-converters.test.cjs (#2095). -function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cmdNames = null) { - const { frontmatter, body } = extractFrontmatterAndBody(content); - const kimiSkillName = normalizeKimiSkillName(skillName); - const names = cmdNames || readMsdCommandNames(); - const description = frontmatter - ? extractFrontmatterField(frontmatter, 'description') || `Run MSD workflow ${kimiSkillName}.` - : `Run MSD workflow ${kimiSkillName}.`; - const normalizedBody = convertMsdCommandReferencesToKimiSkillInvocations( - frontmatter ? body : content, - names - ); - - return `---\nname: ${kimiSkillName}\ndescription: ${yamlQuote(toSingleLine(description))}\n---\nInvoke this Kimi skill with \`/skill:${kimiSkillName}\`.\n\n${normalizedBody}`; -} - -const KIMI_CANONICAL_MSD_AGENT_RE = /^msd-[a-z0-9-]+$/; - -function parseKimiAgentSource(source) { - if (typeof source === 'string') { - return { - path: null, - content: source, - }; - } - if (!source || typeof source !== 'object' || typeof source.content !== 'string') { - return null; - } - return { - path: typeof source.path === 'string' ? source.path : null, - content: source.content, - }; -} - -function parseFrontmatterTools(frontmatter) { - if (!frontmatter) return []; - const lines = frontmatter.split(/\r?\n/); - const tools = []; - let collecting = false; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) continue; - - if (collecting) { - if (trimmed.startsWith('- ')) { - tools.push(trimmed.slice(2).trim()); - continue; - } - collecting = false; - } - - if (trimmed === 'tools:' || trimmed === 'allowed-tools:') { - collecting = true; - continue; - } - - if (trimmed.startsWith('tools:') || trimmed.startsWith('allowed-tools:')) { - const value = trimmed.slice(trimmed.indexOf(':') + 1).trim(); - if (value) { - for (const tool of value.split(',')) { - const name = tool.trim(); - if (name) tools.push(name); - } - } else { - collecting = true; - } - } - } - - return tools; -} - -function addKimiAgentDiagnostic(diagnostics, code, message, value, source = null) { - diagnostics.push({ - level: 'warning', - code, - message, - value, - source, - }); -} - -function mapKimiAgentContractTools(toolNames, diagnostics, sourceName) { - const result = mapClaudeToolsToKimiTools(toolNames, { source: sourceName }); - diagnostics.push(...result.diagnostics); - return result.tools; -} - -function neutralizeKimiAgentPrompt(content) { - const { frontmatter, body } = extractFrontmatterAndBody(content); - let prompt = frontmatter ? body : content; - prompt = neutralizeAgentReferences(prompt, 'AGENTS.md'); - prompt = prompt.replace(/~\/\.claude\/msd-core\b/g, 'MSD core'); - prompt = prompt.replace(/\$HOME\/\.claude\/msd-core\b/g, 'MSD core'); - return prompt.replace(/^\s*\r?\n/, ''); -} - -function pushKimiToolsYaml(lines, indent, tools) { - const prefix = ' '.repeat(indent); - if (!Array.isArray(tools) || tools.length === 0) { - lines.push(`${prefix}tools: []`); - return; - } - lines.push(`${prefix}tools:`); - for (const tool of tools) { - lines.push(`${prefix} - ${yamlQuote(tool)}`); - } -} - -function buildKimiRootAgentYaml({ description, tools, subagents }) { - const lines = [ - 'version: 1', - 'agent:', - ' name: msd', - ` description: ${yamlQuote(toSingleLine(description || 'Run MSD workflows in Kimi CLI.'))}`, - ' extend: default', - ' system_prompt_path: ./msd.md', - ]; - pushKimiToolsYaml(lines, 2, tools); - - if (subagents.length > 0) { - lines.push(' subagents:'); - for (const subagent of subagents) { - lines.push(` ${subagent.name}:`); - lines.push(` path: ./subagents/${subagent.name}.yaml`); - lines.push(` description: ${yamlQuote(toSingleLine(subagent.description))}`); - } - } - - return `${lines.join('\n')}\n`; -} - -function buildKimiSubagentYaml({ name, description, tools }) { - const lines = [ - 'version: 1', - 'agent:', - ` name: ${name}`, - ` description: ${yamlQuote(toSingleLine(description || `Run ${name}.`))}`, - ` system_prompt_path: ./${name}.md`, - ]; - pushKimiToolsYaml(lines, 2, tools); - return `${lines.join('\n')}\n`; -} - -// DEFECT.GENERATIVE-FIX: this body is mirrored in -// src/runtime-artifact-conversion.cts's buildKimiAgentArtifacts (dead for the -// live install path, which routes here via runtime-artifact-layout.cts's -// kimiAgentsKind — see its `conversionExports['buildKimiAgentArtifacts']` -// dynamic lookup against the compiled runtime-artifact-conversion.cjs; kept -// for bin/install.js's own module-level export/test surface). Neither copy -// re-exports the other — mirror any behavior change into both, including the -// kimi_cli.tools.agent:Agent grant that enables background dispatch -// (#2095 Upgrade 2). Guarded by the output-parity test in -// tests/runtime-converters.test.cjs (#2095). -function buildKimiAgentArtifacts({ - rootAgent = '', - subagents = [], - requestedSubagents = null, -} = {}) { - const diagnostics = []; - const rootSource = parseKimiAgentSource(rootAgent) || { path: null, content: '' }; - const { frontmatter: rootFrontmatter } = extractFrontmatterAndBody(rootSource.content); - const rootDescription = rootFrontmatter - ? extractFrontmatterField(rootFrontmatter, 'description') || 'Run MSD workflows in Kimi CLI.' - : 'Run MSD workflows in Kimi CLI.'; - - const subagentSources = Array.isArray(subagents) ? subagents : []; - if (!Array.isArray(subagents)) { - addKimiAgentDiagnostic( - diagnostics, - 'kimi_unsupported_subagents_input', - 'Subagents input must be an array of Markdown strings or source objects.', - typeof subagents, - null - ); - } - - const subagentMap = new Map(); - for (const source of subagentSources) { - const parsed = parseKimiAgentSource(source); - if (!parsed) { - addKimiAgentDiagnostic( - diagnostics, - 'kimi_unsupported_subagent_input', - 'Subagent source must be a Markdown string or an object with content.', - typeof source, - null - ); - continue; - } - - const { frontmatter } = extractFrontmatterAndBody(parsed.content); - const fallbackName = parsed.path ? path.basename(parsed.path, path.extname(parsed.path)) : null; - const name = frontmatter - ? extractFrontmatterField(frontmatter, 'name') || fallbackName - : fallbackName; - if (!name || !KIMI_CANONICAL_MSD_AGENT_RE.test(name)) { - addKimiAgentDiagnostic( - diagnostics, - 'kimi_invalid_subagent_name', - 'Subagent source does not use a canonical msd-* Kimi agent name.', - name || '(missing)', - parsed.path - ); - continue; - } - - const description = frontmatter - ? extractFrontmatterField(frontmatter, 'description') || `Run ${name}.` - : `Run ${name}.`; - const tools = mapKimiAgentContractTools(parseFrontmatterTools(frontmatter), diagnostics, name); - subagentMap.set(name, { - name, - description, - tools, - prompt: neutralizeKimiAgentPrompt(parsed.content), - }); - } - - const requested = Array.isArray(requestedSubagents) && requestedSubagents.length > 0 - ? requestedSubagents - : [...subagentMap.keys()]; - const selectedSubagents = []; - for (const requestedName of requested) { - if (subagentMap.has(requestedName)) { - selectedSubagents.push(subagentMap.get(requestedName)); - continue; - } - addKimiAgentDiagnostic( - diagnostics, - 'kimi_unknown_subagent', - 'Requested subagent was not generated and will not be emitted in Kimi YAML.', - requestedName, - null - ); - } - - const rootTools = mapKimiAgentContractTools(parseFrontmatterTools(rootFrontmatter), diagnostics, 'msd'); - if (selectedSubagents.length > 0 && !rootTools.includes('kimi_cli.tools.agent:Agent')) { - rootTools.push('kimi_cli.tools.agent:Agent'); - } - - return { - root: { - name: 'msd', - yamlPath: 'agents/msd.yaml', - promptPath: 'agents/msd.md', - yaml: buildKimiRootAgentYaml({ - description: rootDescription, - tools: rootTools, - subagents: selectedSubagents, - }), - prompt: neutralizeKimiAgentPrompt(rootSource.content), - }, - subagents: selectedSubagents.map((subagent) => ({ - name: subagent.name, - yamlPath: `agents/subagents/${subagent.name}.yaml`, - promptPath: `agents/subagents/${subagent.name}.md`, - yaml: buildKimiSubagentYaml(subagent), - prompt: subagent.prompt, - })), - diagnostics, - }; -} - -/** - * Convert a Claude agent (.md) to a Copilot agent (.agent.md). - * Applies tool mapping + deduplication, formats tools as JSON array. - * CONV-04: JSON array format. CONV-05: Tool name mapping. - */ -function convertClaudeAgentToCopilotAgent(content, isGlobal = false) { - const converted = convertClaudeToCopilotContent(content, isGlobal); - const { frontmatter, body } = extractFrontmatterAndBody(converted); - if (!frontmatter) return converted; - - const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; - const description = extractFrontmatterField(frontmatter, 'description') || ''; - const color = extractFrontmatterField(frontmatter, 'color'); - const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || ''; - - // CONV-04 + CONV-05: Map tools, deduplicate, format as JSON array - const claudeTools = toolsRaw.split(',').map(t => t.trim()).filter(Boolean); - const mappedTools = claudeTools.map(t => convertCopilotToolName(t)); - const uniqueTools = [...new Set(mappedTools)]; - const toolsArray = uniqueTools.length > 0 - ? "['" + uniqueTools.join("', '") + "']" - : '[]'; - - // Reconstruct frontmatter in Copilot format. Quote description (#2876) - // so a leading YAML flow indicator (`[BETA] …`, `{ … }`, etc.) doesn't - // crash the Copilot frontmatter loader. - let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\ntools: ${toolsArray}\n`; - if (color) fm += `color: ${color}\n`; - fm += '---'; - - return `${fm}\n${body}`; -} - /** * Apply Antigravity-specific content conversion — path replacement + command name conversion. * Path mappings depend on install mode: @@ -2682,1148 +1815,6 @@ function convertClaudeAgentToCursorAgent(content) { return `${cleanFrontmatter}\n${body}`; } -// --- Windsurf converters --- -// #2931 (ADR-1508): single-sourced in runtimeArtifactConversion, bound near -// the top of this file alongside the #1675 Augment family. This block -// previously carried byte-identical local duplicates of -// convertSlashCommandsToWindsurfSkillMentions, convertClaudeToWindsurfMarkdown, -// getWindsurfSkillAdapterHeader, convertClaudeCommandToWindsurfSkill, -// convertClaudeCommandToWindsurfWorkflow, and convertClaudeAgentToWindsurfAgent, -// plus an unused claudeToWindsurfTools table. Deleted here; the two -// unexported helpers (getWindsurfSkillAdapterHeader, -// convertSlashCommandsToWindsurfSkillMentions) now live only in the -// conversion module, with no other caller in this file. - -// --- Augment converters --- -// Augment uses a tool set similar to Cursor/Windsurf. -// Config lives in .augment/ (local) and ~/.augment/ (global). - -// #1675 (ADR-1508): the augment converter family below was a byte-identical -// duplicate of runtime-artifact-conversion.cjs: -// convertSlashCommandsToAugmentSkillMentions, convertClaudeToAugmentMarkdown, -// getAugmentSkillAdapterHeader, convertClaudeCommandToAugmentSkill, -// convertClaudeAgentToAugmentAgent -// Deleted here and bound from runtimeArtifactConversion above (single source). -// The DEFECT.GENERATIVE-FIX parity guard in -// tests/enh-1511-rewrite-engine-relocation.test.cjs asserts reference identity. - -function convertSlashCommandsToTraeSkillMentions(content) { - return content.replace(/\/msd:([a-z0-9-]+)/g, (_, commandName) => { - return `/msd-${commandName}`; - }); -} - -function convertClaudeToTraeMarkdown(content) { - let converted = convertSlashCommandsToTraeSkillMentions(filterRuntimeNotesForTarget(content, 'trae')); - converted = converted.replace(/\bBash\(/g, 'Shell('); - converted = converted.replace(/\bEdit\(/g, 'StrReplace('); - // Replace general-purpose subagent type with Trae's equivalent "general_purpose_task" - converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general_purpose_task"'); - converted = converted.replace(/\$ARGUMENTS\b/g, '{{MSD_ARGS}}'); - // #2658: full-path forms (with a leading dot-claude-slash prefix) MUST be - // replaced before the bare Claude-instruction-file pattern and before the - // generic dot-claude-slash rewrite below — otherwise the bare pattern - // consumes only the instruction-filename tail, leaving that prefix stale - // in place, and the generic rewrite then mutates the stale leftover too, - // producing a doubled trae-prefix segment ahead of the rules path instead - // of a single clean one. (Deliberately never spelling the instruction - // filename as one contiguous "CLAUDE" + dot + "md" token, and never - // spelling either malformed shape out as a literal contiguous string, in - // ANY comment in this function: this file ships verbatim into local - // `--trae` installs, where it is itself run through this same class of - // find/replace — a literal instruction-filename token sitting in a - // comment gets "fixed" right along with real code, and the emitted-content - // regression test added alongside this fix asserts neither malformed - // shape appears anywhere in the installed tree, comments included; this - // bit the fix itself twice during development.) All forms converge on the - // same concrete file (never a bare directory) so this stays in parity - // with the `trae.js` RUNTIME_CONTENT_DISPATCH entry. - converted = converted.replace(/`\.\/\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`'); - converted = converted.replace(/\.\/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md'); - converted = converted.replace(/`\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`'); - converted = converted.replace(/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md'); - // #2658 (found via the end-to-end install regression test, not the static - // trace above): `copyWithPathReplacement` runs a GENERIC dot-claude-slash - // -> runtime-config-dir rewrite on every .md file before calling this - // converter — for `~/.claude/`, `$HOME/.claude/`, AND `./.claude/` alike — - // substituting a runtime-appropriate `pathPrefix` this function is never - // given and cannot itself compute (it differs per install invocation: a - // relative `./.trae/` for a project-local install, an arbitrary absolute - // path for a local install rooted elsewhere, `~/.trae/` for a global one). - // So for source using any of those prefixed forms, the patterns above - // never fire here — this converter only ever sees the ALREADY-rewritten - // "/" + instruction-filename shape, with whatever - // prefix the install actually used. The generic pattern below preserves - // that prefix verbatim (via the capture group) and only fixes the - // filename suffix, rather than assuming a fixed `./.trae/` shape — a - // narrower fixed-prefix version of this pattern shipped first and still - // left the doubled-prefix defect live for the `$HOME/.claude/` and - // `~/.claude/` forms specifically (found the same way, one regression-test - // run later). Scoped to a `.trae/` tail so it cannot also swallow the - // unprefixed `./CLAUDE.md` form the very next pattern handles differently - // (discarding the prefix entirely, not preserving it). Must run before - // the bare pattern for the same consume-the-full-match-first reason. - converted = converted.replace(/`([^\s`]*\.trae\/)CLAUDE\.md`/g, '`$1rules/rules.md`'); - converted = converted.replace(/([^\s`]*\.trae\/)CLAUDE\.md/g, '$1rules/rules.md'); - converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/rules.md`'); - converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/rules.md'); - converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/rules.md`'); - converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/rules.md'); - converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/'); - converted = converted.replace(/\.\/\.claude\//g, './.trae/'); - converted = converted.replace(/\.claude\//g, '.trae/'); - // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite. - // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore. - converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.trae'); - converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.trae'); - // Environment variable name rewrite - converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'TRAE_CONFIG_DIR'); - converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); - converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); - // #2284(b): skips comparison-table content (protected region). - converted = applyClaudeCodeBrandSwap(converted, 'Trae'); - return converted; -} - -// DEFECT.GENERATIVE-FIX: this body is mirrored in -// src/runtime-artifact-conversion.cts's convertClaudeCommandToTraeSkill (used -// by src/install-engine.cts's skills-install path via -// SKILLS_CONVERTER_REGISTRY). This bin/install.js copy is dead for the live -// skills-install path — kept for this file's own module-level export/test -// surface. Neither copy re-exports the other — mirror any behavior change -// into both. Guarded by the output-parity test in -// tests/runtime-converters.test.cjs (#2094). -function convertClaudeCommandToTraeSkill(content, skillName) { - const converted = convertClaudeToTraeMarkdown(content); - const { frontmatter, body } = extractFrontmatterAndBody(converted); - let description = `Run MSD workflow ${skillName}.`; - if (frontmatter) { - const maybeDescription = extractFrontmatterField(frontmatter, 'description'); - if (maybeDescription) { - description = maybeDescription; - } - } - description = toSingleLine(description); - const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; - // #2876: quote so YAML flow indicators (`[BETA] …`) don't break Trae's - // frontmatter parser. - let fm = `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n`; - // #2094: emit `stage:` so Trae's SOLO agent can auto-invoke MSD skills at - // the corresponding stage (docs.trae.ai/ide/agent). The field name/schema - // is not formally documented (thin SPA docs) — descriptor-driven, single - // fixed MSD-side value (runtime.hostBehaviors.soloStageMetadata), inferred/ - // best-effort. - const soloStage = _hostBehaviors('trae').soloStageMetadata; - if (soloStage) fm += `stage: ${soloStage}\n`; - fm += '---'; - return `${fm}\n${body}`; -} - -function convertClaudeAgentToTraeAgent(content) { - let converted = convertClaudeToTraeMarkdown(content); - - const { frontmatter, body } = extractFrontmatterAndBody(converted); - if (!frontmatter) return converted; - - const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; - const description = extractFrontmatterField(frontmatter, 'description') || ''; - - const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; - - return `${cleanFrontmatter}\n${body}`; -} - -function convertSlashCommandsToCodebuddySkillMentions(content) { - return content.replace(/\/msd:([a-z0-9-]+)/g, (_, commandName) => { - return `/msd-${commandName}`; - }); -} - -function convertClaudeToCodebuddyMarkdown(content) { - let converted = convertSlashCommandsToCodebuddySkillMentions(filterRuntimeNotesForTarget(content, 'codebuddy')); - // CodeBuddy uses the same tool names as Claude Code (Bash, Edit, Read, Write, etc.) - // No tool name conversion needed - converted = converted.replace(/\$ARGUMENTS\b/g, '{{MSD_ARGS}}'); - converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`CODEBUDDY.md`'); - converted = converted.replace(/\.\/CLAUDE\.md/g, 'CODEBUDDY.md'); - converted = converted.replace(/`CLAUDE\.md`/g, '`CODEBUDDY.md`'); - converted = converted.replace(/\bCLAUDE\.md\b/g, 'CODEBUDDY.md'); - converted = converted.replace(/\.claude\/skills\//g, '.codebuddy/skills/'); - converted = converted.replace(/\.\/\.claude\//g, './.codebuddy/'); - converted = converted.replace(/\.claude\//g, '.codebuddy/'); - converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); - converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); - // #2284(b): skips comparison-table content (protected region). - converted = applyClaudeCodeBrandSwap(converted, 'CodeBuddy'); - return converted; -} - -function convertClaudeCommandToCodebuddySkill(content, skillName) { - const converted = convertClaudeToCodebuddyMarkdown(content); - const { frontmatter, body } = extractFrontmatterAndBody(converted); - let description = `Run MSD workflow ${skillName}.`; - if (frontmatter) { - const maybeDescription = extractFrontmatterField(frontmatter, 'description'); - if (maybeDescription) { - description = maybeDescription; - } - } - description = toSingleLine(description); - const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; - // #2876: quote so YAML flow indicators (`[BETA] …`) don't break - // CodeBuddy's frontmatter parser. - // - // #789: mark user-invocable:false so the skill is NOT shown in CodeBuddy's - // '/' menu (it defaults to true). The commands/ surface (#789) is the sole - // '/' entry point; skills remain model-invocable background knowledge, - // avoiding a duplicated /msd-* entry per workflow. - return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n${body}`; -} - -/** - * Convert a Claude Code slash-command (.md) to a CodeBuddy slash-command (.md). - * - * CodeBuddy reads user-level slash commands from ~/.codebuddy/commands/.md - * (https://www.codebuddy.ai/docs/cli/slash-commands). The filename determines the - * command name (msd-help.md → /msd-help), so the Claude-specific `name: msd:` - * frontmatter field is dropped. CodeBuddy command frontmatter supports - * `description` and `argument-hint`; both are preserved when present. The body is - * brand/path-converted via convertClaudeToCodebuddyMarkdown. - * - * @param {string} content raw Claude command markdown - * @param {string} commandName installed command name (e.g. 'msd-help') - * @returns {string} - */ -function convertClaudeCommandToCodebuddyCommand(content, commandName) { - const converted = convertClaudeToCodebuddyMarkdown(content); - const { frontmatter, body } = extractFrontmatterAndBody(converted); - let description = `Run MSD workflow ${commandName}.`; - let argumentHint = ''; - if (frontmatter) { - const maybeDescription = extractFrontmatterField(frontmatter, 'description'); - if (maybeDescription) description = maybeDescription; - const maybeArgHint = extractFrontmatterField(frontmatter, 'argument-hint'); - if (maybeArgHint) argumentHint = maybeArgHint; - } - description = toSingleLine(description); - const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; - // #2876: quote values so YAML flow indicators (`[BETA] …`, `[name]`) don't - // break CodeBuddy's frontmatter parser. - const lines = ['---', `description: ${yamlQuote(shortDescription)}`]; - if (argumentHint) lines.push(`argument-hint: ${yamlQuote(toSingleLine(argumentHint))}`); - lines.push('---', body.trimStart()); - return lines.join('\n'); -} - -function convertClaudeAgentToCodebuddyAgent(content) { - let converted = convertClaudeToCodebuddyMarkdown(content); - - const { frontmatter, body } = extractFrontmatterAndBody(converted); - if (!frontmatter) return converted; - - const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; - const description = extractFrontmatterField(frontmatter, 'description') || ''; - - const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; - - return `${cleanFrontmatter}\n${body}`; -} - -// ── Cline converters ──────────────────────────────────────────────────────── - -function convertClaudeToCliineMarkdown(content) { - let converted = filterRuntimeNotesForTarget(content, 'cline'); - // Cline uses the same tool names as Claude Code — no tool name conversion needed - converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.clinerules`'); - converted = converted.replace(/\.\/CLAUDE\.md/g, '.clinerules'); - converted = converted.replace(/`CLAUDE\.md`/g, '`.clinerules`'); - converted = converted.replace(/\bCLAUDE\.md\b/g, '.clinerules'); - // Slash forms first (most specific — superset of bare forms) - converted = converted.replace(/\.claude\/skills\//g, '.cline/skills/'); - converted = converted.replace(/\.\/\.claude\//g, './.cline/'); - converted = converted.replace(/\.claude\//g, '.cline/'); - // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite - converted = converted.replace(/~\/\.claude\b/g, '~/.cline'); - converted = converted.replace(/\$HOME\/\.claude\b/g, '$HOME/.cline'); - // Environment variable name rewrite - converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'CLINE_CONFIG_DIR'); - converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); - converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); - // #2284(b): skips comparison-table content (protected region). - converted = applyClaudeCodeBrandSwap(converted, 'Cline'); - return converted; -} - -function convertClaudeAgentToClineAgent(content) { - let converted = convertClaudeToCliineMarkdown(content); - const { frontmatter, body } = extractFrontmatterAndBody(converted); - if (!frontmatter) return converted; - const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; - const description = extractFrontmatterField(frontmatter, 'description') || ''; - const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; - return `${cleanFrontmatter}\n${body}`; -} - -/** - * Convert a Claude command (.md) to a Cline skill (SKILL.md). - * Emits ONLY name + description frontmatter per the Cline skills spec - * (https://docs.cline.bot/customization/skills) — no allowed-tools, - * argument-hint, agent, or other Claude-specific fields. - * Body is hyphen-normalised then converted via convertClaudeToCliineMarkdown - * (.claude/→.cline/, "Claude Code"→"Cline", etc.). - * Cline uses Claude-Code-compatible tool names, so no adapter header is needed. - * Targets ~/.cline/skills//SKILL.md for Cline >= v3.48.0. - */ -function convertClaudeCommandToClineSkill(content, skillName, runtime = null, cmdNames = null) { - const { frontmatter, body } = extractFrontmatterAndBody(content); - if (!frontmatter) return content; - - // Hyphen-normalise /msd: → msd- references in the body, then - // apply Cline-specific markdown rewrites (.claude/→.cline/, etc.). - const names = cmdNames || readMsdCommandNames(); - const normalizedBody = transformContentToHyphen(body, names); - const clineBody = convertClaudeToCliineMarkdown(normalizedBody); - - // Extract description; fall back to a generic string if absent. - let description = extractFrontmatterField(frontmatter, 'description'); - if (!description) description = `Run MSD workflow ${skillName}.`; - description = toSingleLine(description); - // #4324: same reason as the Claude skill converter above. - description = transformContentToHyphen(description, names); - // Cline documented max is 1024 code points (not UTF-16 code units). - // Use Array.from to iterate by code point so that multibyte characters - // (e.g. emoji, astral-plane chars) are never split, which would produce - // lone surrogates and corrupt the YAML output. - const cp = Array.from(description); - const shortDescription = cp.length > 1024 - ? cp.slice(0, 1021).join('') + '...' - : description; - - const fm = `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---`; - return `${fm}\n${clineBody}`; -} - -// ── End Cline converters ───────────────────────────────────────────────────── - -// ── Hermes converters (#2284) ──────────────────────────────────────────────── -// -// Hermes exposes `delegate_task` for subagent dispatch, not the Claude-shaped -// `Agent(...)` tool the host-neutral `msd-core/workflows/*.md` corpus assumes. -// Prior to this fix, the hermes `.md` hook (RUNTIME_CONTENT_DISPATCH.hermes) -// only brand-swapped "Claude Code" → "Hermes Agent" via -// hostBehaviors.brandingRewrites, leaving the false "Agent tool IS available" -// assertion and literal `Agent(...)` call syntax installed verbatim. -// -// `projectNamedDispatchToStructuralDelegate` is GENERIC projection machinery: -// it branches ENTIRELY on the runtime's documentation-sourced -// `hostIntegration.dispatch` facts (read via `_hostIntegrationDispatch`, -// capabilities//capability.json — never hardcoded here) and a -// `toolConfig` that supplies only the target primitive's own vocabulary (its -// call name + native parameter names — not a capability claim; there is no -// `dispatch` axis for "the call's own parameter names", so that vocabulary is -// necessarily supplied by the caller, exactly as every other runtime's -// converter supplies its own tool-name vocabulary, e.g. Trae's `Shell(`). -// -// Hermes-specific facts consumed (capabilities/hermes/capability.json, -// docs/reference/host-integration-capability-matrix.md:244-249 — UNCHANGED by -// this fix): -// - dispatch.namedDispatch: false — Hermes's delegate_task has no named- -// agent lookup ("Subagents are identified only by role ('leaf' or -// 'orchestrator')"). MSD resolves the referenced msd-* role itself -// (fail-closed against the staged agents/ dir) and embeds the loaded -// PROMPT CONTENT into the delegate_task payload. -// - dispatch.background: true — `delegate_task(background=true)` "returns a -// handle immediately"; Claude's `run_in_background=` maps onto Hermes's -// own `background=` parameter, preserving the async-handle / no-busy-poll -// / resume-on-completion wording already used throughout these workflows. -// - dispatch.subagentToolkit: "read-only" / dispatch.maxDepth: 1 — dispatched -// roles never themselves further delegate, so no nested-delegation -// instruction is ever emitted toward them. - -/** - * Resolve the set of msd-* role-prompt stems actually shipped in this - * package's `agents/` directory (the FULL source set, not profile-staged — - * `--minimal`/`--profile=core` intentionally excludes many agents from a - * given install without those workflows being unreachable, so validating - * against the profile-filtered subset would fail every restricted-profile - * Hermes install; validating against the shipped source catches genuine - * authoring bugs — a stale/typo'd role reference — without that regression). - * Returns `null` if the directory cannot be resolved (fail-closed: callers - * must refuse to install rather than skip validation). - */ -function _resolveAvailableMsdRoles() { - try { - const agentsDir = path.join(__dirname, '..', 'agents'); - return new Set( - fs.readdirSync(agentsDir, { withFileTypes: true }) - .filter((e) => e.isFile() && e.name.endsWith('.md')) - .map((e) => e.name.slice(0, -3)), - ); - } catch (_e) { - return null; - } -} - -/** - * Fail-closed validation (#2284 AC: "Missing role prompts fail closed" / - * "never emit a workflow referencing an unresolvable role"). A single literal - * `msd-*` role value must resolve to a real `agents/.md` file — throws - * an explicit Error otherwise, aborting the install (the standard - * `copyWithPathReplacement` failure path already used for its own - * confinement-violation throws). Called per extracted role value from EVERY - * call-syntax form (`subagent_type=`, `subagent_type:`, post-rename - * `msd_role=`) — the check operates on the resolved value, independent of - * which source syntax produced it. Non-literal / dynamic expressions (e.g. - * `research_hook.ref.agent`) are not quoted strings and are never passed - * here; they carry their own runtime resolution + fail-closed instruction via - * the injected per-call resolution line. - */ -function _assertRoleResolvable(role, availableRoles, runtime, sourceDescription) { - if (!availableRoles) { - throw new Error( - `${runtime} workflow install: could not resolve the shipped agents/ directory to validate named-role ` + - 'dispatch references — refusing to install (fail-closed, #2284)', - ); - } - if (role.startsWith('msd-') && !availableRoles.has(role)) { - throw new Error( - `${runtime} workflow install: dispatch references role "${role}" via ${sourceDescription}, but no ` + - `matching agents/${role}.md prompt file is shipped — refusing to install a workflow that dispatches ` + - 'an unresolvable role (fail-closed, #2284)', - ); - } -} - -/** - * Segment `text` into 'code' and 'string' runs (recognizes `"..."`, `'...'`, - * and Python-style `"""..."""`, with backslash-escaping). Required because - * the real corpus embeds unescaped parens inside quoted prompt bodies (e.g. - * discuss-phase-assumptions.md's `(e.g., "Technical Approach")` inside a - * `"""`-quoted prompt) — naive paren/keyword scanning across raw text would - * desync on these. Downstream call-span detection and header-token - * extraction operate on a same-length MASK derived from this segmentation - * (see `maskStringLiterals`) so string content can never be mistaken for - * call structure. - */ -function _segmentCodeAndStrings(text) { - const segments = []; - let i = 0; - let segStart = 0; - const flushCode = (end) => { if (end > segStart) segments.push({ type: 'code', start: segStart, end }); }; - while (i < text.length) { - const ch = text[i]; - if (ch === '"' && text[i + 1] === '"' && text[i + 2] === '"') { - flushCode(i); - const strStart = i; - i += 3; - while (i < text.length && !(text[i] === '"' && text[i + 1] === '"' && text[i + 2] === '"')) { - i += text[i] === '\\' ? 2 : 1; - } - i = Math.min(i + 3, text.length); - segments.push({ type: 'string', start: strStart, end: i, quoteLen: 3 }); - segStart = i; - continue; - } - // Only `"` is recognized as a single-char string delimiter — NOT `'`. - // The corpus is markdown prose, not code: apostrophes are routine English - // contractions/possessives ("install's", "don't") and treating them as - // string delimiters would swallow everything up to the next unrelated - // apostrophe as "inside a string" (verified against the real corpus — - // this was a real, disqualifying bug during development of this fix). - // Every real call-argument value in the corpus uses `"`/`"""` only. - if (ch === '"') { - flushCode(i); - const strStart = i; - i += 1; - while (i < text.length && text[i] !== '"') { - i += text[i] === '\\' ? 2 : 1; - } - i = Math.min(i + 1, text.length); - segments.push({ type: 'string', start: strStart, end: i, quoteLen: 1 }); - segStart = i; - continue; - } - i += 1; - } - flushCode(text.length); - return segments; -} - -/** - * Same-length mask of `text` with the INTERIOR of every string literal - * replaced by a space (newlines preserved, so line-based regexes still work). - * The delimiting quote character(s) themselves (`"`, `'`, `"""`) are kept - * verbatim so a value-extraction regex like `key\s*[=:]\s*"[^"]*"` still - * matches correctly against the mask — only the STRING CONTENT is blanked, - * never the quote structure. Positions in the mask line up 1:1 with `text`, - * so match indices/offsets found against the mask are valid offsets into the - * original. - */ -function maskStringLiterals(text) { - let mask = ''; - for (const seg of _segmentCodeAndStrings(text)) { - const slice = text.slice(seg.start, seg.end); - if (seg.type === 'code') { mask += slice; continue; } - const q = seg.quoteLen; - if (slice.length <= q) { mask += slice; continue; } // truncated/unterminated — keep verbatim - const closeLen = Math.min(q, slice.length - q); - const open = slice.slice(0, q); - const close = slice.slice(slice.length - closeLen); - const interiorLen = slice.length - q - closeLen; - const interior = interiorLen > 0 ? slice.slice(q, q + interiorLen) : ''; - mask += open + interior.replace(/[^\n]/g, ' ') + close; - } - return mask; -} - -/** - * Locate every `(` / `({` call span in `text`. - * - * #2284 round-2 CRITICAL fix: this MUST NOT rely on whole-document quote - * parity. A markdown workflow file mixes prose, ```bash code fences (full of - * their own double-quoted strings), and shell quoting — there is no single - * document-wide quote grammar, so a `"`-heavy bash `echo` upstream of a real - * call (e.g. code-review.md's fenced `echo "..."` block before its - * `Agent(subagent_type="msd-code-reviewer", ...)` call) can desync a - * CUMULATIVE quote-state scan, making the scanner believe the real call's - * `Agent(` sits "inside a string" and silently skipping it entirely — the - * call then survives completely unnormalized. (Reproduced and root-caused - * against the real corpus.) - * - * Fixed shape: find each `(` occurrence via a PLAIN literal-text - * search (`indexOf`, immune to any prior document content), then run a - * balanced paren-matching scan whose quote-tracking state STARTS FRESH AT - * THE HEAD — local to this one call, never inherited from (or able to be - * corrupted by) anything earlier in the document. Handles all three real - * corpus shapes: multi-line one-key-per-line, single-line object-literal - * (`Agent({ ... })`), and single-line compact - * (`Agent(subagent_type="x", model="y", prompt="...")`) — including prompt - * bodies containing their own unescaped `()`/`{}` (skipped via the SAME - * span-local quote tracking, e.g. discuss-phase-assumptions.md's - * `"""`-quoted parenthetical prose). - * - * Returns `[{start, end, hasBraceWrapper}]` — `start`/`end` bound the FULL - * call INCLUDING the head word and the closing `)`/`})`. - */ -function findDispatchCallSpans(text, headWord) { - const spans = []; - const headToken = `${headWord}(`; - let searchFrom = 0; - for (;;) { - const start = text.indexOf(headToken, searchFrom); - if (start === -1) break; - const prevChar = start > 0 ? text[start - 1] : ''; - if (/[A-Za-z0-9_]/.test(prevChar)) { searchFrom = start + 1; continue; } // word-boundary guard - - let i = start + headToken.length; // just past the '(' - let j = i; - while (j < text.length && /\s/.test(text[j])) j++; - const hasBraceWrapper = text[j] === '{'; - - // LOCAL scan — quote/paren state is fresh here, never inherited from - // anything before `start` in the document. - let parenDepth = 1; - let inString = null; // null | '"' | 'triple' - let end = -1; - for (; i < text.length; i++) { - const ch = text[i]; - if (inString) { - if (ch === '\\') { i++; continue; } - if (inString === 'triple') { - if (ch === '"' && text[i + 1] === '"' && text[i + 2] === '"') { inString = null; i += 2; } - continue; - } - if (ch === inString) inString = null; - continue; - } - if (ch === '"' && text[i + 1] === '"' && text[i + 2] === '"') { inString = 'triple'; i += 2; continue; } - if (ch === '"') { inString = '"'; continue; } - if (ch === '(') { parenDepth++; continue; } - if (ch === ')') { - parenDepth--; - if (parenDepth === 0) { end = i + 1; break; } - continue; - } - } - if (end === -1) { searchFrom = start + 1; continue; } // unterminated — skip past, keep scanning - spans.push({ start, end, hasBraceWrapper }); - searchFrom = end; - } - return spans; -} - -/** - * Remove a call argument's `[matchStart, matchEnd)` token from `spanText`, - * consuming its surrounding comma/whitespace so no dangling `, ,` or trailing - * comment survives. When the argument owns its whole line, the whole line - * (including a trailing inline `# comment`) is removed; `consumeLeadingComments` - * additionally removes contiguous comment-only lines immediately ABOVE it — - * #2284 Finding 5: explanatory prose describing a now-removed conditional - * (e.g. execute-phase.md's "# Only include model= when ...") must not survive - * describing a branch that no longer exists. Inline (single-line-compact / - * object-literal) occurrences instead eat one adjacent comma. - */ -function _stripCallArgument(spanText, matchStart, matchEnd, { consumeLeadingComments = false } = {}) { - let end = matchEnd; - const afterRe = /^[ \t]*,?[ \t]*(#[^\n]*)?\r?\n?/; - const afterMatch = afterRe.exec(spanText.slice(end)); - const hadTrailingComma = !!(afterMatch && /,/.test(afterMatch[0])); - if (afterMatch) end += afterMatch[0].length; - - let start = matchStart; - const lineStart = spanText.lastIndexOf('\n', start - 1) + 1; - const ownLine = /^[ \t]*$/.test(spanText.slice(lineStart, start)); - if (ownLine) { - start = lineStart; - if (consumeLeadingComments) { - for (;;) { - const prevLineStart = start > 0 ? spanText.lastIndexOf('\n', start - 2) + 1 : 0; - const prevLine = spanText.slice(prevLineStart, start); - if (/^[ \t]*#[^\n]*\r?\n$/.test(prevLine)) { - start = prevLineStart; - if (prevLineStart === 0) break; - } else break; - } - } - } else if (!hadTrailingComma) { - // Inline form and this was the LAST arg (no trailing comma) — eat a - // leading comma so the previous arg doesn't dangle one. - const before = spanText.slice(0, start); - const cm = /,[ \t]*$/.exec(before); - if (cm) start -= cm[0].length; - } - return spanText.slice(0, start) + spanText.slice(end); -} - -/** - * Replace a named-role argument token's `[matchStart, matchEnd)` span - * (`subagent_type=`/`subagent_type:` + its value) with the projected - * `msd_role=` / role-prompt-resolution / structural-role argument group. - * Preserves the pretty multi-line one-arg-per-line style when the original - * token owned its own line; falls back to an inline, comma-joined group for - * the single-line-compact and object-literal forms. - */ -function _projectRoleArgument(spanText, matchStart, matchEnd, roleValueExpr, toolConfig, canOrchestrate) { - const { namedRoleParam, promptContentParam, structuralRoleParam, leafRoleValue } = toolConfig; - const lineStart = spanText.lastIndexOf('\n', matchStart - 1) + 1; - const startsOwnLine = /^[ \t]*$/.test(spanText.slice(lineStart, matchStart)); - - // Consume an immediately-following separator comma (+ same-line whitespace/ - // newline) into `end` — never leave it dangling AFTER an injected trailing - // `# comment` (a bare `,` after `#...` would sit on the comment's own line, - // outside any real argument list). - let end = matchEnd; - const afterRe = /^[ \t]*,[ \t]*\r?\n?/; - const afterMatch = afterRe.exec(spanText.slice(end)); - const hadTrailingComma = !!afterMatch; - if (afterMatch) end += afterMatch[0].length; - const ownLine = startsOwnLine && hadTrailingComma && /\n$/.test(afterMatch[0]); - - const promptContentPhrase = - `${promptContentParam}='; - - let replacement; - if (ownLine) { - const indent = spanText.slice(lineStart, matchStart); - const depthNote = canOrchestrate ? '' : ' # nested delegation is unavailable at this dispatch depth/toolkit'; - replacement = - `${namedRoleParam}=${roleValueExpr},\n` + - `${indent}${promptContentPhrase},\n` + - `${indent}${structuralRoleParam}="${leafRoleValue}",${depthNote}\n`; - } else { - // Inline forms never carry a trailing `#` comment mid-argument-list (it - // would silently "comment out" the remainder of the call), so the - // depth/toolkit caveat is only ever emitted in the pretty own-line form. - // Re-emit exactly the separator that originally followed this argument - // (a comma if more args follow; nothing if it was the last one). - replacement = - `${namedRoleParam}=${roleValueExpr}, ${promptContentPhrase}, ${structuralRoleParam}="${leafRoleValue}"` + - (hadTrailingComma ? ', ' : ''); - } - return spanText.slice(0, matchStart) + replacement + spanText.slice(end); -} - -// Matches a `subagent_type`/`model` argument's key+delimiter+value across all -// three corpus forms: quoted-string values ("msd-planner", "{model}") and -// bare dynamic-expression values (ref.agent, research_hook.ref.agent, -// executor_model). The captured group is always the value (a suffix of the -// whole match), so its start offset is `match.index + match[0].length - -// match[1].length` — avoids needing the regex `d` (indices) flag. -function _callArgValueRe(key) { - return new RegExp(`\\b${key}\\s*[=:]\\s*("(?:[^"\\\\]|\\\\.)*"|[A-Za-z_][\\w.]*)`); -} - -/** - * Returns the literal role name from a captured role-argument value EXPR - * (e.g. `"msd-planner"`) — or `null` when it is not a genuine static - * literal: a bare dynamic expression (`ref.agent`), OR a quoted value that - * still contains `{...}` template interpolation (the corpus's own - * placeholder convention, e.g. `model="{researcher_model}"` — and, - * critically, `subagent_type: "msd-{agent}"` in - * msd-core/references/universal-anti-patterns.md, a DOCUMENTATION template - * illustrating the naming pattern, never a concrete role to resolve). - * Fail-closed validation only ever runs on a genuine static literal; a - * template/dynamic value still gets the full role-prompt-resolution - * projection treatment (the resolve+fail-closed instruction applies equally - * once a template is substituted at runtime) — only the STATIC CHECK is - * skipped, never the projection itself. - */ -function _literalRoleValue(roleValueExpr) { - const m = /^"([^"]*)"$/.exec(roleValueExpr); - if (!m) return null; - if (/[{}]/.test(m[1])) return null; - return m[1]; -} - -/** - * `maskStringLiterals` PLUS `#`-to-end-of-line comment blanking (comments are - * never string literals, so they survive string-masking as literal `#...` - * text). Header-token searches (subagent_type/model/run_in_background) must - * use THIS mask, not the string-only one — verified necessary against the - * real corpus: execute-phase.md's explanatory comment "# Only include - * model= when executor_model is..." literally contains the substring - * "model= when", which a comment-blind `model` regex mismatches as a real - * `model=when` argument, corrupting the comment AND missing the real - * `model="{executor_model}"` line beneath it. Scoped to call-span text only - * (never the whole document), so markdown `#`/`##` headings elsewhere are - * unaffected. - */ -function _maskStringsAndComments(text) { - return maskStringLiterals(text).replace(/#[^\n]*/g, (m) => ' '.repeat(m.length)); -} - -/** - * Normalize ONE `Agent(...)`/`Agent({...})` call span (already isolated by - * `findDispatchCallSpans`) onto the target's real dispatch primitive. Every - * behavioral branch reads `dispatch` (the runtime's sourced - * `hostIntegration.dispatch` facts) — none is hardcoded. Handles all three - * corpus call-argument shapes uniformly via string-aware token location - * (`maskStringLiterals` recomputed after each structural edit, since prior - * edits shift offsets). - */ -function _normalizeDispatchCallSpan(spanText, hasBraceWrapper, dispatch, toolConfig) { - const namedDispatch = dispatch.namedDispatch === true; - const backgroundCapable = dispatch.background === true; - const canOrchestrate = dispatch.subagentToolkit === 'full' - && (dispatch.maxDepth === -1 || (typeof dispatch.maxDepth === 'number' && dispatch.maxDepth > 1)); - const { toolName, backgroundParam, supportsPerCallModel, availableRoles, runtime } = toolConfig; - - let text = spanText; - - // 1. Named-role argument (subagent_type= / subagent_type:) — only when the - // target has no native named-agent lookup (dispatch.namedDispatch). - // Fail-closed validation runs on the extracted value REGARDLESS of - // which source syntax produced it (#2284 requirement 2). - if (!namedDispatch) { - const roleRe = _callArgValueRe('subagent_type'); - const rm = roleRe.exec(_maskStringsAndComments(text)); - if (rm) { - // Read the VALUE from the original (unmasked) text at the matched - // offset — `rm[1]` was captured against the mask, whose string - // INTERIOR is blanked, so it must never be used as the real value. - const roleValueExpr = text.slice(rm.index + rm[0].length - rm[1].length, rm.index + rm[0].length); - const literalRole = _literalRoleValue(roleValueExpr); - if (literalRole !== null) { - _assertRoleResolvable(literalRole, availableRoles, runtime, 'subagent_type'); - } else if (!availableRoles) { - // No literal value to check, but a null availableRoles still means - // the shipped agents/ dir couldn't be resolved at all — fail closed - // unconditionally rather than silently install an unverifiable call. - _assertRoleResolvable('', availableRoles, runtime, 'subagent_type'); - } - text = _projectRoleArgument(text, rm.index, rm.index + rm[0].length, roleValueExpr, toolConfig, canOrchestrate); - } - } - - // 2. Per-call model argument (model= / model:) — stripped entirely when the - // target has no per-call model-selection parameter (there is no - // `dispatch` axis for this — it is inherent tool vocabulary, like the - // parameter names themselves). Also removes now-dead explanatory - // comment lines directly above a `model=` line that owns its own line - // (#2284 Finding 5). - if (!supportsPerCallModel) { - const modelRe = _callArgValueRe('model'); - const mm = modelRe.exec(_maskStringsAndComments(text)); - if (mm) { - text = _stripCallArgument(text, mm.index, mm.index + mm[0].length, { consumeLeadingComments: true }); - } - } - - // 3. Background-dispatch flag (run_in_background= / run_in_background:) — - // maps onto the target's own background parameter ONLY when documented - // to support it; otherwise stripped rather than forwarding a parameter - // the primitive doesn't accept. - { - const bgRe = /\brun_in_background\s*[=:]\s*(?:true|false)/; - const bm = bgRe.exec(_maskStringsAndComments(text)); - if (bm) { - if (backgroundCapable) { - const matched = text.slice(bm.index, bm.index + bm[0].length); - const replaced = matched.replace(/^run_in_background(\s*[=:]\s*)/, `${backgroundParam}$1`); - text = text.slice(0, bm.index) + replaced + text.slice(bm.index + bm[0].length); - } else { - text = _stripCallArgument(text, bm.index, bm.index + bm[0].length); - } - } - } - - // 4. Call-syntax head rename + object-literal brace stripping. Hermes's - // delegate_task is a flat kwarg call — `Agent({...})`'s wrapper braces - // are dropped rather than carried through, so every projected call ends - // up in the same flat shape regardless of source syntax. - text = text.replace(/^Agent\(/, `${toolName}(`); - if (hasBraceWrapper) { - const openMask = maskStringLiterals(text); - const braceOpenIdx = openMask.indexOf('{'); - if (braceOpenIdx !== -1) text = text.slice(0, braceOpenIdx) + text.slice(braceOpenIdx + 1); - const closeMask = maskStringLiterals(text); - const braceCloseIdx = closeMask.lastIndexOf('}'); - if (braceCloseIdx !== -1) text = text.slice(0, braceCloseIdx) + text.slice(braceCloseIdx + 1); - } - - return text; -} - -/** - * Blank the interior (and delimiters) of every string literal inside - * `spanText` to spaces — same length, newlines preserved — using a fresh, - * LOCAL quote-tracking scan that starts at `spanText[0]` with NO inherited - * state. This is deliberately the SAME state-machine shape as the - * `inString`/`\\`/triple-quote handling inside `findDispatchCallSpans` - * (double-quoted and `"""`-triple-quoted, backslash-escape aware) — reused - * here so a call span's quoted argument VALUES (documentation prose, prompt - * bodies) never masquerade as real call syntax, without EVER falling back to - * a whole-document cumulative quote-parity mask (the round-2 defect - * documented on `findDispatchCallSpans` above). - */ -function _blankStringLiteralInteriors(spanText) { - let out = ''; - let inString = null; // null | '"' | 'triple' - for (let i = 0; i < spanText.length; i++) { - const ch = spanText[i]; - if (inString) { - if (ch === '\\') { - out += ' '; - i++; - if (i < spanText.length) out += (spanText[i] === '\n') ? '\n' : ' '; - continue; - } - if (inString === 'triple') { - if (ch === '"' && spanText[i + 1] === '"' && spanText[i + 2] === '"') { - inString = null; - out += ' '; - i += 2; - continue; - } - out += (ch === '\n') ? '\n' : ' '; - continue; - } - if (ch === inString) { inString = null; out += ' '; continue; } - out += (ch === '\n') ? '\n' : ' '; - continue; - } - if (ch === '"' && spanText[i + 1] === '"' && spanText[i + 2] === '"') { - inString = 'triple'; - out += ' '; - i += 2; - continue; - } - if (ch === '"') { inString = '"'; out += ' '; continue; } - out += ch; - } - return out; -} - -/** - * Quote-aware view of `content` for the completeness checks below: for every - * REAL call span located via `findDispatchCallSpans` (once per head word in - * `headWords`), the string-literal ARGUMENT VALUES inside that span are - * blanked via `_blankStringLiteralInteriors`; the call's own head word and - * bare (unquoted) argument tokens are left untouched. `headWords` is - * processed in order and each pass re-scans the PROGRESSIVELY-masked string - * — `toolName` first, then `'Agent'` — so a spurious `Agent(` that - * `findDispatchCallSpans('Agent')` would otherwise "find" purely because it - * sits inside an outer call's quoted string (e.g. a `description="...Agent() - * ...subagent_type=x"` argument value) has ALREADY been blanked away by the - * outer `toolName` pass by the time the `'Agent'` pass runs, so it is never - * mistaken for a real, independent call. A genuinely real (unquoted) `Agent(` - * — including one nested as a raw, un-renamed argument value — survives every - * pass and remains visible to the caller's regex checks. - */ -function _maskQuotedRegionsWithinCallSpans(content, headWords) { - let masked = content; - for (const headWord of headWords) { - const spans = findDispatchCallSpans(masked, headWord); - for (let i = spans.length - 1; i >= 0; i--) { - const { start, end } = spans[i]; - const maskedSpan = _blankStringLiteralInteriors(masked.slice(start, end)); - masked = masked.slice(0, start) + maskedSpan + masked.slice(end); - } - } - return masked; -} - -/** - * Post-projection guard (#2284 requirement 3 — belt-and-suspenders): after - * projection, assert the corpus form the projection could not anticipate - * never silently ships. Throws an explicit install error (fail-LOUD) rather - * than let an unprojected/incompletely-projected dispatch call install. - * - * #2284 round-2 CRITICAL fix: this is an INDEPENDENT check — it does NOT use - * `maskStringLiterals` over the whole document (the round-1 primitive whose - * cumulative, document-wide quote-parity tracking was the root cause of the - * round-2 defect: a `"`-heavy bash fence upstream of a real call desynced - * quote state and made `findDispatchCallSpans` blind to that call, shipping - * a Frankenstein `Agent(msd_role="...", model="...")` with no detection). - * - * #2284 round-3 fix: a BLUNT, mask-free literal check over the whole - * document (round-2's fix) over-throws — it cannot tell a real residual - * `Agent(`/`subagent_type` call from the SAME text appearing INSIDE a quoted - * string (documentation/prompt prose, e.g. `description="...Agent()..."`). - * The completeness checks (residual `subagent_type` / literal `Agent(`) now - * run against `_maskQuotedRegionsWithinCallSpans` — quote-aware, but scoped - * strictly to already-correctly-bounded, per-occurrence-LOCAL call spans - * (never a whole-document cumulative mask), so a real Frankenstein call - * (unquoted, real call syntax) still fires while a same-text mention genuinely - * inside a quoted string does not. - * - * The completeness checks also only apply when `namedDispatch` is false: when - * `dispatch.namedDispatch === true`, `_normalizeDispatchCallSpan` step 1 - * INTENTIONALLY leaves `subagent_type` unprojected (the target primitive - * resolves named agents itself) — a residual `subagent_type` in that case is - * the correct, intended output, not a defect. (The call HEAD is still renamed - * unconditionally regardless of `namedDispatch` — see step 4 there — so a - * literal `Agent(` residual is gated the same way purely for symmetry with - * the dispatch-facts-driven contract; it is never actually left unrenamed by - * the projection in practice.) - * - * The model-leak check is unaffected by either fix above — it is orthogonal - * to `namedDispatch` (gated only by `supportsPerCallModel`) and already - * bounds each real call via the independently-fixed, per-occurrence-local, - * non-cumulative `findDispatchCallSpans`, then does a raw substring check - * within that bound. - */ -function _assertProjectionComplete(content, toolConfig, namedDispatch = false) { - const { toolName, runtime, supportsPerCallModel } = toolConfig; - - if (!namedDispatch) { - const quoteAware = _maskQuotedRegionsWithinCallSpans(content, [toolName, 'Agent']); - if (/\bsubagent_type\s*[=:]/.test(quoteAware)) { - throw new Error( - `${runtime} workflow install: projection left a residual subagent_type reference — refusing to install ` + - '(fail-closed post-projection guard, #2284)', - ); - } - if (/\bAgent\(/.test(quoteAware)) { - throw new Error( - `${runtime} workflow install: projection left literal Agent( call syntax — refusing to install ` + - '(fail-closed post-projection guard, #2284)', - ); - } - } - - if (!supportsPerCallModel) { - for (const span of findDispatchCallSpans(content, toolName)) { - const rawSpanText = content.slice(span.start, span.end); - if (/\bmodel\s*[=:]/.test(rawSpanText)) { - throw new Error( - `${runtime} workflow install: projection left a leaked model= argument inside a ${toolName}(...) call ` + - '— refusing to install (fail-closed post-projection guard, #2284)', - ); - } - } - } -} - -/** - * Project host-neutral `Agent(...)` named-subagent dispatch prose onto a - * target runtime's real dispatch primitive. See the file-header comment above - * for the governing rule: every behavioral branch reads `dispatch` (the - * runtime's sourced `hostIntegration.dispatch` facts) — none is a hardcoded - * assumption about a specific runtime. Handles all three real corpus call - * forms (multi-line one-key-per-line, single-line object-literal, single-line - * compact) via string-aware call-span detection rather than three independent - * line-anchored regexes, and closes with a post-projection guard that fails - * loud on any form it did not anticipate (#2284). - * - * @param {string} content - * @param {{namedDispatch?: boolean, nested?: boolean, maxDepth?: number, background?: boolean, backgroundDispatch?: boolean, subagentToolkit?: string}} dispatch - * @param {{toolName: string, namedRoleParam: string, promptContentParam: string, structuralRoleParam: string, leafRoleValue: string, backgroundParam: string, supportsPerCallModel: boolean, availableRoles: Set|null, runtime: string}} toolConfig - */ -function projectNamedDispatchToStructuralDelegate(content, dispatch, toolConfig) { - const d = dispatch || {}; - const namedDispatch = d.namedDispatch === true; - const backgroundCapable = d.background === true; - const { toolName, promptContentParam } = toolConfig; - - let converted = content; - - // 1. The "Agent tool IS available" contract assertion (currently unique to - // plan-phase.md, matched generically in case of future reuse elsewhere). - const assertionRe = /The Agent tool IS available in a top-level ([^\n]+?) session\.\s+Always spawn\s+([\s\S]*?)\s+as separate Agent\(\) calls\./; - converted = converted.replace(assertionRe, (_m, sessionName, roster) => { - const rosterFlat = roster.replace(/\s+/g, ' ').trim(); - if (namedDispatch) { - return `The \`${toolName}\` tool IS available in a top-level ${sessionName} session. Always dispatch ${rosterFlat} as separate \`${toolName}()\` calls.`; - } - return ( - `${sessionName} has no \`Agent\` tool. It exposes \`${toolName}\`, which dispatches by structural role — ` + - 'it has no concept of a named subagent identity. MSD projects each named msd-* role onto this primitive ' + - `itself: resolve the role's prompt file from the active install, load its contents, and embed them in the ` + - `\`${toolName}\` payload via \`${promptContentParam}\` as the dispatched task's operating instructions. ` + - 'FAIL CLOSED — surface an explicit error and stop — if a referenced role prompt cannot be resolved; never ' + - `execute the role inline as a substitute. In a top-level ${sessionName} session, always dispatch ` + - `${rosterFlat} as separate \`${toolName}\` calls.` - ); - }); - - // 1b. Dispatch-depth-availability prose immediately adjacent to a renamed - // `Agent()` mention in the SAME sentence (plan-review-convergence.md - // ~lines 108, 347, 355) — a bare "Agent" left un-renamed right next to - // the projection's own `Agent()`→`${toolName}()` rename produced - // self-contradictory installed text (e.g. "...delegate_task(...)... - // with Agent available..."). Narrowly scoped to the EXACT known - // phrases the projection itself creates the inconsistency beside — - // never a broad bare-word `Agent` rename, which would corrupt - // legitimate `Agent`-adjacent prose elsewhere in the corpus (role - // names, "Agent Brief", agent-file references). - converted = converted.replace( - /\borchestrator runs at depth 0 with Agent available\b/g, - `orchestrator runs at depth 0 with ${toolName} available`, - ); - converted = converted.replace( - /\(bug #936: depth-1 Agent has no Agent tool\)/g, - `(bug #936: depth-1 ${toolName} has no nested ${toolName})`, - ); - - // 2. Per-call model-selection prose ("Model resolution:" paragraph, - // execute-phase.md) + inline backtick-quoted model-mention prose - // examples (not live call sites) — only rewritten when the target - // primitive has no per-call model parameter at all. - if (!toolConfig.supportsPerCallModel) { - const modelResolutionRe = /\*\*Model resolution:\*\* If `executor_model` is `"inherit"`, omit the `model=` parameter from all `Agent\(\)` calls — do NOT pass `model="inherit"` to Agent\. Omitting the `model=` parameter causes [^.]+\. Only set `model=` when `executor_model` is an explicit model name \(e\.g\., `"claude-sonnet-5"`, `"claude-opus-4-8"`\)\./; - converted = converted.replace( - modelResolutionRe, - `**Model resolution:** \`${toolName}\` has no per-call model-selection parameter — every dispatched role ` + - `always inherits the host session's active model. Never pass \`model=\` to \`${toolName}\`; drop the ` + - '`executor_model` value entirely for this runtime.', - ); - converted = converted.replace(/`model="[^"`\n]*"`,?\s*(?:and\s+)?/g, ''); - } - - // 3. Background-dispatch PROSE mentions outside any real call span (e.g. - // execute-phase.md:595,600 — `run_in_background: true` inline - // documentation, not a call argument) — #2284 Finding 3. Only rewritten - // when the target is documented to support background dispatch (a - // prose mention of an unsupported capability would be equally - // misleading as a real leaked argument). - if (backgroundCapable) { - converted = converted.replace( - /\brun_in_background(\s*[=:]\s*(?:true|false))/g, - `${toolConfig.backgroundParam}$1`, - ); - } - - // 4. Call-span-based normalization — the core of the fix. Every - // `Agent(...)`/`Agent({...})` occurrence (all three corpus forms) is - // located via string-aware balanced paren/brace matching, then - // normalized as a unit; spans are rebuilt right-to-left so earlier - // offsets stay valid while later ones are rewritten. - const spans = findDispatchCallSpans(converted, 'Agent'); - for (let i = spans.length - 1; i >= 0; i--) { - const { start, end, hasBraceWrapper } = spans[i]; - const rebuilt = _normalizeDispatchCallSpan(converted.slice(start, end), hasBraceWrapper, d, toolConfig); - converted = converted.slice(0, start) + rebuilt + converted.slice(end); - } - - // 5. "Agent tool" capability mentions (conditions gating parallel vs. - // sequential dispatch, e.g. map-codebase.md) → the real target primitive - // name, which resolves these conditions accurately since it IS a real, - // always-available dispatch primitive for this target. - converted = converted.replace(/\bAgent tool\b/g, toolName); - - // 5b. Catch-all: a `subagent_type` mention that is NOT part of any real - // `Agent(...)` call span (e.g. map-codebase.md's inline documentation - // prose ``Use Agent tool with `subagent_type="X"`, ...`` — disconnected - // example syntax, not a live call). Renamed for the same accuracy the - // real calls get; a literal quoted role value is still fail-closed - // validated even though there is no call structure to inject - // role-prompt/fail-closed guidance INTO. - if (!namedDispatch) { - converted = converted.replace( - /\bsubagent_type(\s*[=:]\s*"[^"]*")/g, - (_m, rest) => { - const literalRole = _literalRoleValue(rest.replace(/^\s*[=:]\s*/, '')); - if (literalRole !== null) { - _assertRoleResolvable(literalRole, toolConfig.availableRoles, toolConfig.runtime, 'subagent_type (prose mention)'); - } - return `${toolConfig.namedRoleParam}${rest}`; - }, - ); - converted = converted.replace(/\bsubagent_type(\s*[=:])/g, `${toolConfig.namedRoleParam}$1`); - } - - // 5c. Safety net (#2284 requirement 2): the PRIMARY mechanism for - // eliminating literal `Agent(` syntax is complete span detection (step - // 4) — this unconditional final rename exists only so that even a call - // span detection somehow misses at least loses its `Agent(` head - // rather than shipping the literal Claude-shaped tool name verbatim. - // A call caught only by this safety net is still INCOMPLETELY - // normalized (no role/model handling) and gets caught by the - // independent post-projection guard below via its OTHER invariants - // (residual subagent_type / leaked model=), which this safety net does - // not touch — the install still fails closed for a missed span. - converted = converted.replace(/\bAgent\(/g, `${toolName}(`); - - // 6. Post-projection guard (#2284 requirement 3): fail loud, never ship - // silently, on any residual/leaked form the projection above did not - // anticipate. `namedDispatch` gates the completeness checks — a - // residual subagent_type is INTENTIONAL, not a defect, when the target - // resolves named agents itself (see `_assertProjectionComplete`). - _assertProjectionComplete(converted, toolConfig, namedDispatch); - - return converted; -} - -const HERMES_DISPATCH_TOOL_CONFIG = Object.freeze({ - toolName: 'delegate_task', - namedRoleParam: 'msd_role', - promptContentParam: 'msd_role_prompt', - structuralRoleParam: 'role', - leafRoleValue: 'leaf', - backgroundParam: 'background', - supportsPerCallModel: false, -}); - -/** - * Hermes `.md` content converter (#2284): brand-swap (unchanged behavior, - * descriptor-driven per `hostBehaviors.brandingRewrites`) followed by the - * generic named-dispatch → `delegate_task` projection above, driven by - * `capabilities/hermes/capability.json`'s `hostIntegration.dispatch` (read - * via `_hostIntegrationDispatch`, values UNCHANGED by this fix — they are - * already documentation-sourced and correct). - */ -function convertClaudeToHermesMarkdown(content, ctx) { - const runtime = (ctx && ctx.runtime) || 'hermes'; - const b = _hostBehaviors(runtime).brandingRewrites; - let converted = content; - if (b) { - converted = converted.replace(/CLAUDE\.md/g, b['CLAUDE.md']); - // #2284(b): skips comparison-table content (protected region). - converted = applyClaudeCodeBrandSwap(converted, b['Claude Code']); - converted = converted.replace(/\.claude\//g, b['.claude/']); - } - const dispatch = _hostIntegrationDispatch(runtime); - const toolConfig = Object.assign({}, HERMES_DISPATCH_TOOL_CONFIG, { - availableRoles: _resolveAvailableMsdRoles(), - runtime, - }); - return projectNamedDispatchToStructuralDelegate(converted, dispatch, toolConfig); -} - -// ── End Hermes converters ──────────────────────────────────────────────────── - function convertSlashCommandsToCodexSkillMentions(content) { // Colon-style /msd: never appears as a filesystem path segment, so no boundary guard is needed (unlike the hyphen-style below). let converted = content.replace(/\/msd:([a-z0-9-]+)/gi, (_, commandName) => { @@ -4049,7 +2040,7 @@ purpose: ${toSingleLine(description)} * Codex agent `.toml` `model`. Two forms: (a) a bare Claude Agent-tool tier alias * (opus/sonnet/haiku/fable — the canonical CLAUDE_AGENT_ALIASES); (b) any Claude model * id in any provider namespacing — `claude-*`, `anthropic/claude-*`, `us.anthropic.claude-*` - * (the forms the catalog assigns to opencode/hermes/kilo, reachable on a Codex .toml via + * (the forms the catalog assigns to opencode, reachable on a Codex .toml via * the runtime-resolver path). No OpenAI/Codex model id contains "claude", so a * case-insensitive substring test is a safe, exhaustive guard for (b). Codex/ChatGPT * rejects all of these. @@ -4337,66 +2328,6 @@ function cleanupCodexSkillMetadataSidecars(skillsDir) { } } -/** - * Remove legacy Windsurf skill artifacts from .devin/skills/msd- directories. - * - * Pre-#1615 Windsurf installs wrote skills under .devin/ (Devin Desktop - * preferred dir, #1085). #1615 moved Windsurf to .windsurf/workflows/. - * Old .devin/skills/msd- dirs linger on disk indefinitely and confuse - * users who see two MSD trees. - * - * Preserves user-owned content: - * - non-msd-* dirs under .devin/skills/ (user-authored skills) - * - msd-dev-preferences/ (user-owned per #2973) - * - any files (not dirs) under .devin/skills/ - * - * @param {string} workspaceDir - workspace root (process.cwd() for local installs) - * @returns {number} count of removed legacy msd-* skill directories - */ -function cleanupWindsurfLegacyDevinSkills(workspaceDir) { - const legacySkillsDir = path.join(workspaceDir, '.devin', 'skills'); - if (!fs.existsSync(legacySkillsDir)) return 0; - - // Mirror the user-owned list from cleanupCodexSkillMetadataSidecars (#2973). - const _userOwnedSkillDirs = new Set(['msd-dev-preferences']); - let removed = 0; - - for (const entry of fs.readdirSync(legacySkillsDir, { withFileTypes: true })) { - if (!entry.isDirectory() || !entry.name.startsWith('msd-')) continue; - if (_userOwnedSkillDirs.has(entry.name)) continue; - - const dirToRemove = path.join(legacySkillsDir, entry.name); - try { - // Symlink guard: if the msd-* dir is itself a symlink pointing outside - // the .devin tree, deleting through it could escape the tree. Skip. - const stat = fs.lstatSync(dirToRemove); - if (stat.isSymbolicLink()) continue; - - fs.rmSync(dirToRemove, { recursive: true, force: true }); - removed++; - } catch (_err) { - // Fail open — a single bad dir must not block the install. - } - } - - // If .devin/skills/ is now empty, prune it. If .devin/ itself is then empty, - // prune that too — leaves the workspace clean for the new .windsurf/ layout. - // Never remove non-empty containers (user may have other Devin content). - try { - if (fs.existsSync(legacySkillsDir) && fs.readdirSync(legacySkillsDir).length === 0) { - fs.rmdirSync(legacySkillsDir); - const devinDir = path.join(workspaceDir, '.devin'); - if (fs.existsSync(devinDir) && fs.readdirSync(devinDir).length === 0) { - fs.rmdirSync(devinDir); - } - } - } catch (_err) { - // best-effort container cleanup - } - - return removed; -} - /** * Migrate a skills kind that moved to an alternate `home` (ADR-1239 split-home): * remove now-stale `*` skill dirs left at the OLD configDir-rooted @@ -4421,7 +2352,7 @@ function cleanupMovedSkillsOldLocation(oldSkillsDir, prefix) { const dirToRemove = path.join(oldSkillsDir, entry.name); try { - // Symlink guard (mirrors cleanupWindsurfLegacyDevinSkills): never delete + // Symlink guard: never delete // through a symlinked msd-* dir — it could escape the tree. const stat = fs.lstatSync(dirToRemove); if (stat.isSymbolicLink()) continue; @@ -6783,117 +4714,6 @@ function hasEnabledCodexHooksFeature(configContent) { }); } -/** - * Merge MSD instructions into copilot-instructions.md. - * Three cases: new file, existing with markers, existing without markers. - * @param {string} filePath - Full path to copilot-instructions.md - * @param {string} msdContent - Template content (without markers) - */ -function mergeCopilotInstructions(filePath, msdContent) { - const msdBlock = MSD_COPILOT_INSTRUCTIONS_MARKER + '\n' + - msdContent.trim() + '\n' + - MSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER; - - // Case 1: No file — create fresh - if (!fs.existsSync(filePath)) { - fs.writeFileSync(filePath, msdBlock + '\n'); - return; - } - - const existing = fs.readFileSync(filePath, 'utf8'); - const openIndex = existing.indexOf(MSD_COPILOT_INSTRUCTIONS_MARKER); - const closeIndex = existing.indexOf(MSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER); - - // Case 2: Has MSD markers — replace between markers - if (openIndex !== -1 && closeIndex !== -1) { - const before = existing.substring(0, openIndex).trimEnd(); - const after = existing.substring(closeIndex + MSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER.length).trimStart(); - let newContent = ''; - if (before) newContent += before + '\n\n'; - newContent += msdBlock; - if (after) newContent += '\n\n' + after; - newContent += '\n'; - fs.writeFileSync(filePath, newContent); - return; - } - - // Case 3: No markers — append at end - const content = existing.trimEnd() + '\n\n' + msdBlock + '\n'; - fs.writeFileSync(filePath, content); -} - -/** - * Strip MSD section from copilot-instructions.md content. - * Returns cleaned content, or null if file should be deleted (was MSD-only). - * @param {string} content - File content - * @returns {string|null} - Cleaned content or null if empty - */ -function stripMsdFromCopilotInstructions(content) { - const openIndex = content.indexOf(MSD_COPILOT_INSTRUCTIONS_MARKER); - const closeIndex = content.indexOf(MSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER); - - if (openIndex !== -1 && closeIndex !== -1) { - const before = content.substring(0, openIndex).trimEnd(); - const after = content.substring(closeIndex + MSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER.length).trimStart(); - const cleaned = (before + (before && after ? '\n\n' : '') + after).trim(); - if (!cleaned) return null; - return cleaned + '\n'; - } - - // No markers found — nothing to strip - return content; -} - -// ── Cline directory-form rules + hooks + AGENTS.md (issue #787) ──────────────── -// -// Cline v3.36 added a hooks system and a `.clinerules/` directory form. Because -// `.clinerules` cannot be both a file AND a directory, emitting hooks under -// `.clinerules/hooks/` requires migrating the rules content into the directory -// form (`.clinerules/msd.md`). Sources adjudicated: -// - https://cline.bot/blog/cline-v3-36-hooks -// - https://docs.cline.bot/customization/cline-rules - -const MSD_AGENTS_MD_MARKER = ''; -const MSD_AGENTS_MD_CLOSE_MARKER = ''; - -// #2876: buildClineRulesBody, buildClineAgentsMdBody, buildClinePreToolUseHook, -// and mergeMsdAgentsMd used to be re-bound here as one-line delegates to the -// equivalent hooksSurface.* implementations. None had an install.js internal -// caller or an export consumer (tests import all four directly from -// msd-core/bin/lib/runtime-hooks-surface.cjs), so the retired bindings were -// dead code with no reachable body — removed rather than kept as unreachable -// wrappers. - -/** - * Strip the MSD block from AGENTS.md content. Returns null if the file became - * empty (was MSD-only), the unchanged content if no markers were found, or the - * cleaned content otherwise. - */ -function stripMsdFromAgentsMd(content) { - const openIndex = content.indexOf(MSD_AGENTS_MD_MARKER); - const closeIndex = content.indexOf(MSD_AGENTS_MD_CLOSE_MARKER); - if (openIndex !== -1 && closeIndex !== -1) { - const before = content.substring(0, openIndex).trimEnd(); - const after = content.substring(closeIndex + MSD_AGENTS_MD_CLOSE_MARKER.length).trimStart(); - const cleaned = (before + (before && after ? '\n\n' : '') + after).trim(); - if (!cleaned) return null; - return cleaned + '\n'; - } - return content; -} - -/** - * Write the full Cline runtime artifact set (directory-form rules + PreToolUse - * hook) into targetDir, migrating a legacy single-file `.clinerules` if present. - * For global installs, also merge the cross-tool ~/.agents/AGENTS.md target. - * - * Returns the list of manifest-relative paths written under targetDir (so the - * caller can hash-track them). - */ -function writeClineArtifacts(targetDir, isGlobalInstall) { - return hooksSurface.writeClineArtifacts(targetDir, isGlobalInstall); -} - // ── Cursor hooks.json reconciler (issue #777) ──────────────────────────────── // // Cursor v2.4+ supports a hooks.json lifecycle hook system. MSD registers two @@ -6949,62 +4769,6 @@ function removeCursorHooksJson(targetDir) { return hooksSurface.removeCursorHooksJson(targetDir); } -/** - * #2100 Stage 2 — Write MSD-managed Windsurf/Cascade lifecycle hooks into - * /hooks.json. Both managed hook scripts - * (msd-windsurf-pre-write.js, msd-windsurf-pre-command.js) are copied from - * the MSD hooks/ source to /hooks/ first, so the hooks.json - * entries never reference a script that wasn't installed. Mirrors - * writeCursorHooksJson's structure; Cascade's blocking protocol (exit code 2) - * and entry shape (bare `command` string, no `type` field) are distinct from - * Cursor's. - * - * @param {string} targetDir - The Windsurf config dir (global: ~/.codeium/windsurf; local: .windsurf) - * @param {string} src - The MSD install source root (for copying hook scripts) - * @param {{ platform?: string }} opts - * @returns {{ hooksJsonPath: string, changed: boolean }} - */ -function writeWindsurfHooksJson(targetDir, src, opts) { - return hooksSurface.writeWindsurfHooksJson(targetDir, src, opts); -} - -/** - * Remove all MSD-managed Windsurf/Cascade lifecycle hook entries from - * hooks.json. User-owned entries are preserved. If the file becomes empty, - * it is removed. - * - * @param {string} targetDir - The Windsurf config dir - * @returns {{ changed: boolean }} - */ -function removeWindsurfHooksJson(targetDir) { - return hooksSurface.removeWindsurfHooksJson(targetDir); -} - -// #2876: buildCopilotHookConfig used to be re-bound here as a one-line -// delegate to hooksSurface.buildCopilotHookConfig. It had no install.js -// internal caller or export consumer (tests import it directly from -// msd-core/bin/lib/runtime-hooks-surface.cjs), so the retired binding was -// dead code with no reachable body — removed rather than kept as an -// unreachable wrapper. writeCopilotHookConfig below is unaffected — it still -// has an internal caller (finishInstall). - -/** - * #786 — Write the MSD-managed Copilot lifecycle hook config under the runtime - * config dir (`/hooks/msd-session.json`). For local installs - * targetDir is `.github` (→ `.github/hooks/`); for global installs it is - * `~/.copilot` (→ `~/.copilot/hooks/`) — both are valid Copilot hook locations. - * - * The managed file is fully owned by MSD, so it is overwritten wholesale on - * every install (idempotent). User-authored sibling `*.json` hook files in the - * same directory are untouched. - * - * @param {string} targetDir - The Copilot config dir - * @returns {string} The path the hook config was written to - */ -function writeCopilotHookConfig(targetDir) { - return hooksSurface.writeCopilotHookConfig(targetDir); -} - /** * Generate config.toml and per-agent .toml files for Codex. * Reads agent .md files from source, extracts metadata, writes .toml configs. @@ -7392,214 +5156,20 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve return `---\n${newFrontmatter}\n---${body}`; } -// Kilo CLI — same conversion logic as OpenCode, different config paths. -// DEFECT.GENERATIVE-FIX: this body is mirrored in -// src/runtime-artifact-conversion.cts's convertClaudeToKiloFrontmatter (used by -// src/install-engine.cts's install path). Neither copy re-exports the other — -// mirror any behavior change into both. Guarded by the output-parity test in -// tests/runtime-converters.test.cjs (#2093). -function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverride = null } = {}) { - // Replace tool name references in content (applies to all files) - let convertedContent = filterRuntimeNotesForTarget(content, 'kilo'); - convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question'); - convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill'); - convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite'); - // Replace /msd-command colon variant with /msd-command for Kilo (flat command structure) - convertedContent = convertedContent.replace(/\/msd:/g, '/msd-'); - // Replace ~/.claude and $HOME/.claude with Kilo's config location - convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/kilo'); - convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/kilo'); - convertedContent = convertedContent.replace(/\.\/\.claude\//g, './.kilo/'); - // Normalize both Claude skill directory variants to Kilo's canonical skills dir. - convertedContent = replaceRelativePathReference(convertedContent, '.claude/skills/', '.kilo/skills/'); - convertedContent = replaceRelativePathReference(convertedContent, '.agents/skills/', '.kilo/skills/'); - convertedContent = replaceRelativePathReference(convertedContent, '.claude/agents/', '.kilo/agents/'); - // Replace general-purpose subagent type with Kilo's equivalent "general" - convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"'); - // Runtime-neutral agent name replacement (#766) - convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md'); - - // Check if content has frontmatter - if (!convertedContent.startsWith('---')) { - return convertedContent; - } - - // Find the end of frontmatter - const endIndex = convertedContent.indexOf('---', 3); - if (endIndex === -1) { - return convertedContent; - } - - const frontmatter = convertedContent.substring(3, endIndex).trim(); - const body = convertedContent.substring(endIndex + 3); - - // Parse frontmatter line by line (simple YAML parsing) - const lines = frontmatter.split('\n'); - const newLines = []; - let inAllowedTools = false; - let inAgentTools = false; - let inSkippedArray = false; - const allowedTools = []; - const agentTools = []; - - for (const line of lines) { - const trimmed = line.trim(); - - // For agents: skip commented-out lines (e.g. hooks blocks) - if (isAgent && trimmed.startsWith('#')) { - continue; - } - - // Detect start of allowed-tools array - if (trimmed.startsWith('allowed-tools:')) { - inAllowedTools = true; - continue; - } - - if (isAgent && inAgentTools) { - if (trimmed.startsWith('- ')) { - const tool = runtimeArtifactConversion._decodeToolScalar(trimmed.substring(2)); - if (tool !== null) agentTools.push(tool); - continue; - } - if (trimmed && !trimmed.startsWith('-')) { - inAgentTools = false; - } - } - - // Detect inline tools: field (comma-separated string) - if (trimmed.startsWith('tools:')) { - if (isAgent) { - const toolsValue = trimmed.substring(6).trim(); - // A comment-only value (`tools: # note`) is not real inline content — - // fall through to the block-list scan instead of decoding the - // comment as a bogus tool name and dropping the list (mirrors - // src/runtime-artifact-conversion.cts's convertClaudeToKiloFrontmatter, #4032). - if (toolsValue && !toolsValue.startsWith('#')) { - const tools = runtimeArtifactConversion._splitToolScalars(toolsValue).map(runtimeArtifactConversion._decodeToolScalar).filter(tool => tool !== null); - agentTools.push(...tools); - } else { - inAgentTools = true; - } - continue; - } - const toolsValue = trimmed.substring(6).trim(); - if (toolsValue) { - // Parse comma-separated tools - const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t); - allowedTools.push(...tools); - } - continue; - } - - // For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:, disallowedTools: - if (isAgent && /^(skills|color|memory|maxTurns|permissionMode|disallowedTools):/.test(trimmed)) { - inSkippedArray = true; - continue; - } - - // Skip continuation lines of a stripped array/object field - if (inSkippedArray) { - if (trimmed.startsWith('- ') || trimmed.startsWith('#') || /^\s/.test(line)) { - continue; - } - inSkippedArray = false; - } - - // For commands: remove name: field (Kilo uses filename for command name) - // For agents: keep name: (required by Kilo agents) - if (!isAgent && trimmed.startsWith('name:')) { - continue; - } - - // Strip model: field — Kilo doesn't support Claude Code model aliases - // like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets Kilo use - // its configured default model. - if (trimmed.startsWith('model:')) { - continue; - } - - // Convert color names to hex for Kilo (commands only; agents strip color above) - if (trimmed.startsWith('color:')) { - const colorValue = trimmed.substring(6).trim().toLowerCase(); - const hexColor = colorNameToHex[colorValue]; - if (hexColor) { - newLines.push(`color: "${hexColor}"`); - } else if (colorValue.startsWith('#')) { - // Validate hex color format (#RGB or #RRGGBB) - if (/^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i.test(colorValue)) { - // Already hex and valid, keep as is - newLines.push(line); - } - // Skip invalid hex colors - } - // Skip unknown color names - continue; - } - - // Collect allowed-tools items - if (inAllowedTools) { - if (trimmed.startsWith('- ')) { - const tool = trimmed.substring(2).trim(); - if (isAgent) { - const decoded = runtimeArtifactConversion._decodeToolScalar(tool); - if (decoded !== null) agentTools.push(decoded); - } else { - allowedTools.push(tool); - } - continue; - } else if (trimmed && !trimmed.startsWith('-')) { - // End of array, new field started - inAllowedTools = false; - } - } - - // Keep other fields - if (!inAllowedTools) { - newLines.push(line); - } - } - - // For agents: add required Kilo agent fields - if (isAgent) { - newLines.push('mode: subagent'); - // Embed model override from ~/.msd/defaults.json so model_overrides is - // respected on Kilo (which uses static agent frontmatter, not inline - // Task() model parameters) — mirrors convertClaudeToOpencodeFrontmatter's - // model emission exactly (#2093 UPGRADE 2 / ADR-1239). See #2256. - if (modelOverride) { - newLines.push(['model:', modelOverride].join(' ')); - } - newLines.push(...buildKiloAgentPermissionBlock(agentTools)); - } - - // For commands: add tools object if we had allowed-tools or tools - if (!isAgent && allowedTools.length > 0) { - newLines.push('tools:'); - for (const tool of allowedTools) { - newLines.push(` ${convertToolName(tool)}: true`); - } - } - - // Rebuild frontmatter (body already has tool names converted) - const newFrontmatter = newLines.join('\n').trim(); - return `---\n${newFrontmatter}\n---${body}`; -} - -// convertClaudeCommandToOpencodeFamilySkill, convertClaudeCommandToOpencodeSkill, -// convertClaudeCommandToKiloSkill: moved to src/install-engine.cts (ADR-1239 Phase B). -// #2876 found no install.js internal caller for the latter two (tests import -// them directly from msd-core/bin/lib/install-engine.cjs) and retired the -// destructured bindings above. +// convertClaudeCommandToOpencodeFamilySkill, convertClaudeCommandToOpencodeSkill: +// moved to src/install-engine.cts (ADR-1239 Phase B). +// #2876 found no install.js internal caller for the latter (tests import +// it directly from msd-core/bin/lib/install-engine.cjs) and retired the +// destructured binding above. // applyOpencodeFamilyPathPrefix: moved to src/install-engine.cts (ADR-1239 Phase B). // #2876 found no install.js internal caller (never had one either — it was // never part of this module's export surface) and retired the destructured // binding above. // -// copyFlattenedCommands (OpenCode/Kilo flattened command/ writer): moved to +// copyFlattenedCommands (OpenCode flattened command/ writer): moved to // src/install-engine.cts as installOpencodeFamilyCommands (ADR-1239 / #2087). -// OpenCode/Kilo installs now route through installRuntimeArtifacts's +// OpenCode installs now route through installRuntimeArtifacts's // combinedFamilyInstall path (installOpencodeFamilyArtifacts) instead of the // bespoke inline block that used to call this function. @@ -7631,26 +5201,10 @@ function listCodexSkillNames(skillsDir, prefix = 'msd-') { -/** - * Copy Claude commands as Windsurf skills — one folder per skill with SKILL.md. - * Mirrors copyCommandsAsCursorSkills but uses Windsurf converters. - */ - - -/** - * Copy Claude commands as CodeBuddy skills — one folder per skill with SKILL.md. - * CodeBuddy uses the same tool names as Claude Code, but has its own config directory structure. - */ - -/** - * Copy Claude commands as Copilot skills — one folder per skill with SKILL.md. - * Applies CONV-01 (structure), CONV-02 (allowed-tools), CONV-06 (paths), CONV-07 (command names). - */ - /** * Copy Claude commands as Claude skills — one folder per skill with SKILL.md. * Claude Code 2.1.88+ uses skills/xxx/SKILL.md instead of commands/msd/xxx.md. - * Supports runtime='claude'|'qwen'|'hermes'; branding rewrites are applied via + * Branding rewrites are applied via * applyRuntimeContentRewritesInPlace inside _copyCommandsAsSkillsViaConverter. * @param {string} srcDir - Source commands directory * @param {string} skillsDir - Target skills directory @@ -7660,49 +5214,21 @@ function listCodexSkillNames(skillsDir, prefix = 'msd-') { * @param {boolean} isGlobal - Whether this is a global install (unused; kept for compat) */ -/** - * Write the Hermes "msd" category DESCRIPTION.md. - * Hermes' skill loader reads DESCRIPTION.md at the top of each skill category - * directory and surfaces it in the system prompt so the model knows when to - * reach for that category. Per spec in #2841 we collapse all 86 MSD commands - * under a single "msd" category to keep system-prompt overhead bounded. - */ -function writeHermesCategoryDescription(categoryDir) { - fs.mkdirSync(categoryDir, { recursive: true }); - const body = [ - '---', - 'name: msd', - `version: ${pkg.version}`, - 'description: MSD Core — Make Software Done. Disciplined planning, execution, and shipping workflows. Use any msd-* skill in this category to drive a project through new-project → discuss-phase → plan-phase → execute-phase → ship.', - '---', - '', - '# MSD Core', - '', - 'MSD is a structured development workflow. Skills in this category cover', - 'project initialization, phase planning, execution, code review, and shipping.', - '', - 'Invoke any `msd-*` skill in this category to drive the corresponding step.', - '', - ].join('\n'); - fs.writeFileSync(path.join(categoryDir, 'DESCRIPTION.md'), body); -} - /** * Recursively install MSD commands as Antigravity skills. * Each command becomes a skill-name/ folder containing SKILL.md. - * Mirrors copyCommandsAsCopilotSkills but uses Antigravity converters. * @param {string} srcDir - Source commands directory * @param {string} skillsDir - Target skills directory * @param {string} prefix - Skill name prefix (e.g. 'msd') * @param {boolean} isGlobal - Whether this is a global install */ -// USER_OWNED_ARTIFACTS, migrateLegacyDevPreferencesToSkill, _snapshotDir, +// USER_OWNED_ARTIFACTS, _snapshotDir, // installRuntimeArtifacts, installOpencodeFamilySkills, uninstallRuntimeArtifacts: // ALL moved to src/install-engine.cts (ADR-1239 Phase B). Imported from // installEngine above. _copyStaged, _removeMsdEntries, -// _runLegacyInstallMigrations, _runLegacyUninstallCleanup, _restoreDir, and -// _removeHermesBareStemDirs moved there too but #2876 found no install.js +// _runLegacyInstallMigrations, _runLegacyUninstallCleanup, and _restoreDir +// moved there too but #2876 found no install.js // internal caller for any of them and retired their destructured bindings. // --------------------------------------------------------------------------- @@ -7717,12 +5243,12 @@ const _stampNonClaudeRuntimeDefaults = runtimeArtifactConversion._stampNonClaude * Data-driven dispatch table for copyWithPathReplacement (ADR-1239 Phase B). * Keyed by runtime id. Each entry declares ONLY what that runtime does differently. * The DEFAULT (no entry, or entry with no md/js key) = identity transform after - * the uniform steps — covers claude, augment, codebuddy, kimi, etc. + * the uniform steps — covers claude, zcode, etc. * * Entry shape: - * mdSkipGenericRewrite?: boolean — skip the ~/.claude/ rewrite block (copilot, antigravity) + * mdSkipGenericRewrite?: boolean — skip the ~/.claude/ rewrite block (antigravity) * md?: (content, ctx) => string — per-runtime .md transform - * mdReattributeAfter?: boolean — re-run processAttribution after md() (copilot, antigravity) + * mdReattributeAfter?: boolean — re-run processAttribution after md() (antigravity) * mdTomlRenameOnCommand?: boolean — when isCommand, rename dest .md → .toml * (unused since the gemini runtime was removed, #1928; * kept as generic dispatch infra for a future TOML-command runtime) @@ -7734,18 +5260,9 @@ const RUNTIME_CONTENT_DISPATCH = { opencode: { md: (content) => convertClaudeToOpencodeFrontmatter(content), }, - kilo: { - md: (content) => convertClaudeToKiloFrontmatter(content), - }, codex: { md: (content) => convertClaudeToCodexMarkdown(content), }, - copilot: { - mdSkipGenericRewrite: true, - md: (content, ctx) => convertClaudeToCopilotContent(content, ctx.isGlobal), - mdReattributeAfter: true, - js: (content, ctx) => convertClaudeToCopilotContent(content, ctx.isGlobal), - }, antigravity: { mdSkipGenericRewrite: true, md: (content, ctx) => convertClaudeToAntigravityContent(content, ctx.isGlobal), @@ -7762,97 +5279,6 @@ const RUNTIME_CONTENT_DISPATCH = { return content; }, }, - windsurf: { - md: (content) => convertClaudeToWindsurfMarkdown(content), - js: (content) => { - // Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085). - content = content.replace(/msd:/gi, 'msd-'); - content = content.replace(/\.claude\/skills\//g, '.devin/skills/'); - content = content.replace(/CLAUDE\.md/g, '.devin/rules'); - content = content.replace(/\bClaude Code\b/g, 'Windsurf'); - return content; - }, - }, - trae: { - md: (content) => convertClaudeToTraeMarkdown(content), - js: (content) => { - content = content.replace(/\/msd:([a-z0-9-]+)/g, (_, commandName) => { - return `/msd-${commandName}`; - }); - content = content.replace(/\.claude\/skills\//g, '.trae/skills/'); - // #2658: the full dot-claude-slash-prefixed instruction-file path must - // be replaced before the bare instruction-filename fallback, or the - // bare regex only rewrites that filename and leaves the prefix stale - // in place, producing a malformed doubled-prefix path (see the longer - // note in convertClaudeToTraeMarkdown above — the instruction filename - // and either malformed shape are deliberately never spelled out - // contiguously here either, for the same reason: this file ships - // verbatim). Both forms target the same concrete file (never a bare - // directory), matching the `.md` converter (convertClaudeToTraeMarkdown) - // so js/cjs and md content agree on one canonical path. - content = content.replace(/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md'); - content = content.replace(/CLAUDE\.md/g, '.trae/rules/rules.md'); - content = content.replace(/\bClaude Code\b/g, 'Trae'); - return content; - }, - }, - cline: { - md: (content) => convertClaudeToCliineMarkdown(content), - js: (content) => { - content = content.replace(/\.claude\/skills\//g, '.cline/skills/'); - content = content.replace(/CLAUDE\.md/g, '.clinerules'); - content = content.replace(/\bClaude Code\b/g, 'Cline'); - return content; - }, - }, - // qwen/hermes: brand VALUES are descriptor-driven (ADR-1239 / #2092) via - // _hostBehaviors(ctx.runtime).brandingRewrites — EXACT regexes/ordering - // preserved from the prior hardcoded-literal versions (including the - // qwen-specific `.claude/skills/` -> `.qwen/skills/` pre-rewrite, whose - // target is derived as `${b['.claude/']}skills/`). - qwen: { - md: (content, ctx) => { - // Guarded (post-review #2092): degrade closed to a no-op if the - // registry fails to load, instead of throwing on `b['CLAUDE.md']`. - const b = _hostBehaviors(ctx.runtime).brandingRewrites; - if (b) { - content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']); - // #2284(b): skips comparison-table content (protected region). - content = applyClaudeCodeBrandSwap(content, b['Claude Code']); - content = content.replace(/\.claude\//g, b['.claude/']); - } - return content; - }, - js: (content, ctx) => { - const b = _hostBehaviors(ctx.runtime).brandingRewrites; - if (b) { - content = content.replace(/\.claude\/skills\//g, `${b['.claude/']}skills/`); - content = content.replace(/\.claude\//g, b['.claude/']); - content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']); - content = content.replace(/\bClaude Code\b/g, b['Claude Code']); - } - return content; - }, - }, - hermes: { - // #2284: brand-swap alone left the false "Agent tool IS available" - // assertion + literal `Agent(...)` call syntax installed verbatim — see - // convertClaudeToHermesMarkdown / projectNamedDispatchToStructuralDelegate - // above (the Hermes converters section) for the full named-dispatch → - // `delegate_task` projection, driven by capabilities/hermes/capability.json's - // hostIntegration.dispatch facts. - md: (content, ctx) => convertClaudeToHermesMarkdown(content, ctx), - js: (content, ctx) => { - const b = _hostBehaviors(ctx.runtime).brandingRewrites; - if (b) { - content = content.replace(/\.claude\/skills\//g, `${b['.claude/']}skills/`); - content = content.replace(/\.claude\//g, b['.claude/']); - content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']); - content = content.replace(/\bClaude Code\b/g, b['Claude Code']); - } - return content; - }, - }, }; /** @@ -7916,11 +5342,11 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand const ctx = { isCommand, isGlobal, dirName, pathPrefix, entryName: entry.name, runtime }; // Replace ~/.claude/ and $HOME/.claude/ and ./.claude/ with runtime-appropriate paths - // Skip generic replacement for Copilot/Antigravity — their converters handle all paths + // Skip generic replacement for Antigravity — its converter handles all paths let content = fs.readFileSync(srcPath, 'utf8'); // #2930 (epic #1671 Phase 3): strip `` markers - // BEFORE any per-runtime rewrite so a `.claude/` -> `.windsurf/` regex + // BEFORE any per-runtime rewrite so a `.claude/` -> `.cursor/` regex // (or any other converter below) never reaches inside a marker // attribute and corrupts it. composeWorkflow is a no-op (byte-identical // return) for the 88+ workflows and every non-workflow .md that carries @@ -7992,12 +5418,6 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand content = content.replace(/~\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, '')); content = content.replace(/\$HOME\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, '')); content = content.replace(/\.\/\.claude\b/g, `./${dirName}`); - content = content.replace(/~\/\.qwen\//g, pathPrefix); - content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix); - content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`); - content = content.replace(/~\/\.hermes\//g, pathPrefix); - content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix); - content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`); // #3544: restore @-file-reference lines to the tilde form Claude Code // actually expands — the SAME correction #3133 already applies to // skill/command bodies via _applyRuntimeRewrites's 'claude' case (see @@ -8006,7 +5426,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand // emit path, which never had it: every @~/.claude/msd-core/… include // in a global install's workflows/references tree silently resolved // to nothing (54 includes across 22 files on a live install). - if (runtime === 'claude') { + if (_hostBehaviors(runtime).ownsClaudePaths) { content = runtimeArtifactConversion._restoreClaudeGlobalAtRefTilde(content, pathPrefix); } return content; @@ -8036,7 +5456,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand // Apply per-runtime .md converter (if any) if (dispatch.md) content = dispatch.md(content, ctx); - // Re-run attribution after converter for runtimes that need it (copilot, antigravity) + // Re-run attribution after converter for runtimes that need it (antigravity) if (dispatch.mdReattributeAfter) content = processAttribution(content, getCommitAttribution(runtime)); // Rename .md → .toml for command files (unused since gemini removal, #1928) @@ -8214,167 +5634,21 @@ function validateHookFields(settings) { */ const MSD_UNINSTALL_HOOKS = [..._HOOKS_TO_COPY, 'msd-check-update.cmd']; -/** - * Whether two paths denote the SAME directory — used to stop a reclaim from - * deleting the very root the current install just wrote (#3031). - * - * A plain `path.resolve` comparison is not enough here, because both roots come - * from user-controlled env vars (`KIMI_SHARE_DIR`, `KIMI_CODE_HOME`) and two - * different strings routinely name one directory: - * - case-insensitive filesystems (macOS, Windows): `~/Kimi` vs `~/kimi` - * - symlinks / bind mounts: `~/link-to-kimi` vs the real target - * Getting this wrong is not cosmetic — it is the difference between skipping a - * reclaim and deleting a live install's own hooks. - * - * Strategy, cheapest-first: string equality after `resolve`, then identity by - * `dev`+`ino` (definitive when both exist and the platform reports them), then - * `realpath` string equality (resolves symlinks AND canonicalizes case). Any - * rung answering "same" wins; a path that does not exist cannot be the root we - * just wrote, so a failed stat simply falls through. - * - * @returns {boolean} true only when both paths are proven to be one directory. - */ -function isSameDirectory(a, b) { - if (path.resolve(a) === path.resolve(b)) return true; - try { - const sa = fs.statSync(a); - const sb = fs.statSync(b); - // `ino` is 0 on some Windows filesystems; only trust a positive match. - if (sa.ino && sb.ino && sa.dev === sb.dev && sa.ino === sb.ino) return true; - } catch (_) { /* one side missing — fall through to realpath */ } - try { - return fs.realpathSync.native(a) === fs.realpathSync.native(b); - } catch (_) { - return false; - } -} - -/** - * Remove every MSD-owned artifact from a Kimi hooks root (`~/.kimi` for kimi, - * `~/.kimi-code` for kimi-code — resolveKimiHooksTomlDir, #2755): the managed - * `[[hooks]]` block in the native config.toml, the hook scripts, hooks/lib/, - * and the CommonJS marker at both its current (hooks/) and pre-#2544 (root) - * locations. - * - * This root is Kimi's own native config home — SHARED space that may hold the - * user's real config.toml, providers and their own scripts — so only exact - * MSD-owned filenames are removed and directories are pruned only when that - * removal leaves them empty. - * - * TWO callers, deliberately one implementation (#3031). `uninstall()` calls it - * for the runtime being uninstalled; the opt-in `--reclaim-kimi-legacy` path in - * `install()` calls it for the LEGACY `~/.kimi` root a pre-#2755 `--kimi-code` - * install orphaned. Duplicating this sequence for the second caller would be - * exactly the generative-divergence hazard the repo bans — the reclaim must - * remove precisely what a real uninstall removes, forever, by construction. - * - * @param {string} kimiHooksRoot - Absolute path to the Kimi hooks root. - * @returns {number} count of removal steps performed (0 when nothing matched). - */ -function reclaimKimiHooksRoot(kimiHooksRoot) { - let steps = 0; - const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml'); - const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath); - if (kimiHooksCleanup.changed) { - steps++; - console.log(` ${green}✓${reset} Removed MSD hooks from ${kimiHooksTomlPath}`); - } - - // Kimi's shared hook scripts + CommonJS package.json marker are installed - // into this SAME ~/.kimi root (installSharedHooksBundle, install()'s - // kimi-hooks-toml branch) rather than under targetDir — mirror steps "4. - // Remove MSD hooks" / "5. Remove MSD package.json" below, but scoped to - // kimiHooksRoot. ~/.kimi is Kimi's own native config home (shared space — - // may hold the user's real config.toml/providers), so only the exact - // MSD-owned filenames are removed, and directories are pruned only if left - // empty by that removal. - const kimiHooksDir = path.join(kimiHooksRoot, 'hooks'); - if (fs.existsSync(kimiHooksDir)) { - let kimiHookCount = 0; - for (const hook of MSD_UNINSTALL_HOOKS) { - const hookPath = path.join(kimiHooksDir, hook); - if (fs.existsSync(hookPath)) { - fs.unlinkSync(hookPath); - kimiHookCount++; - } - } - if (kimiHookCount > 0) { - steps++; - console.log(` ${green}✓${reset} Removed ${kimiHookCount} MSD hooks from ${kimiHooksDir}`); - } - - const kimiHooksLibDir = path.join(kimiHooksDir, 'lib'); - if (fs.existsSync(kimiHooksLibDir)) { - let removedKimiLibFiles = 0; - for (const file of MSD_HOOK_LIB_FILES) { - try { - fs.unlinkSync(path.join(kimiHooksLibDir, file)); - removedKimiLibFiles++; - } catch (_) { /* best-effort */ } - } - try { fs.rmdirSync(kimiHooksLibDir); } catch (_) { /* not empty or other error — leave it */ } - if (removedKimiLibFiles > 0) { - steps++; - console.log(` ${green}✓${reset} Removed ${removedKimiLibFiles} hooks/lib/ helper(s) from ${kimiHooksLibDir}`); - } - } - - // #2544: the marker now lives inside kimi's hooks/ dir — remove it - // before the emptiness check below, or the dir would never prune. - if (removeCommonJsMarker(kimiHooksDir)) { - steps++; - console.log(` ${green}✓${reset} Removed MSD package.json from ${kimiHooksDir}`); - } - - try { - if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir); - } catch (_) { /* not empty — leave it */ } - } - - // Retire the pre-#2544 marker at kimi's root (~/.kimi), where the bundle - // used to write it. Exact content match — a user's own package.json in - // kimi's native config home is never touched. - if (removeCommonJsMarker(kimiHooksRoot)) { - steps++; - console.log(` ${green}✓${reset} Removed MSD package.json from ${kimiHooksRoot} (pre-#2544 marker)`); - } - return steps; -} - /** * Uninstall MSD from the specified directory for a specific runtime * Removes only MSD-specific files/directories, preserves user content * @param {boolean} isGlobal - Whether to uninstall from global or local - * @param {string} runtime - Target runtime ('claude', 'opencode', 'codex', 'copilot') + * @param {string} runtime - Target runtime ('claude', 'opencode', 'codex', ...) */ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) { - // #2093: isKilo dropped — the Kilo permission-cleanup branch below is - // descriptor-driven (resolveInstallPlan(runtime).finishPermissionWriter), - // not gated on this flag. - // #2094: isTrae dropped — unused in this function after the - // skipSharedHooksInstall fold (was never referenced here besides the - // destructure). #2095: isKimi likewise dropped — kimi is now a hooks/ - // consumer, so its former `&& !isKimi` uninstall guards were removed. - // #2096: isAntigravity dropped — unused in this function. - // #2098: isCodebuddy dropped — unused in this function. - // #2099: isCopilot dropped — both Copilot side-effect branches below are now - // gated on resolveInstallPlan(runtime).installSurface === 'copilot-instructions'. - // #2100: isWindsurf dropped — unused in this function. - const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime); + const { isOpencode, isCodex, isCursor } = runtimeFlags(runtime); const dirName = getDirName(runtime); - // Get the target directory based on runtime and install type. Cline local - // installs write to the project root (.clinerules/ lives at the root, not in - // a .cline/ subdir), mirroring the install() path resolution (#787). - // Descriptor-driven (ADR-1239 / #2090): cline local installs write to the - // project root (.clinerules/ lives at the root, not in a .cline/ subdir), - // mirroring the install() path resolution (#787). Folded from a hardcoded - // `runtime === 'cline'` branch into hostBehaviors.localTargetIsProjectRoot. + // Get the target directory based on runtime and install type, mirroring the + // install() path resolution. const targetDir = isGlobal ? getGlobalConfigDir(runtime, explicitConfigDir) - : _hostBehaviors(runtime).localTargetIsProjectRoot - ? process.cwd() - : path.join(process.cwd(), dirName); + : path.join(process.cwd(), dirName); const locationLabel = isGlobal ? targetDir.replace(os.homedir(), '~') @@ -8386,30 +5660,6 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) { console.log(` Uninstalling MSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`); - // #786: AGENTS.md lives at the repo root (outside targetDir) for local Copilot - // installs, so its cleanup must run even when .github (targetDir) was already - // removed — i.e. BEFORE the "target directory missing" early-return below. - // #2099: descriptor-driven via resolveInstallPlan(runtime).installSurface === - // 'copilot-instructions' (was hardcoded `isCopilot`). Mirrors the install-time - // gate at the 'copilot-instructions' branch below (~line 10471 equivalent), - // which writes this same repo-root AGENTS.md only for local ('!isGlobal') - // installs — 'copilot-instructions' is unique to copilot's descriptor, so - // this is byte-parity. - if (resolveInstallPlan(runtime).installSurface === 'copilot-instructions' && !isGlobal) { - const agentsMdPath = path.join(process.cwd(), 'AGENTS.md'); - if (fs.existsSync(agentsMdPath)) { - const content = fs.readFileSync(agentsMdPath, 'utf8'); - const cleaned = stripMsdFromCopilotInstructions(content); - if (cleaned === null) { - fs.unlinkSync(agentsMdPath); - console.log(` ${green}✓${reset} Removed AGENTS.md (was MSD-only)`); - } else if (cleaned !== content) { - fs.writeFileSync(agentsMdPath, cleaned); - console.log(` ${green}✓${reset} Cleaned MSD section from AGENTS.md`); - } - } - } - // Check if target directory exists if (!fs.existsSync(targetDir)) { console.log(` ${yellow}⚠${reset} Directory does not exist: ${locationLabel}`); @@ -8541,105 +5791,6 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) { } } - // 1a-kimi. Non-layout Kimi side-effect (#2095 EoS/kimi Upgrade 1): kimi's - // native config.toml lives outside targetDir entirely (resolveKimiHooksTomlDir - // resolves ~/.kimi for kimi and ~/.kimi-code for kimi-code (#2755), a sibling - // of targetDir's ~/.config/agents), so its - // cleanup can't be driven by anything under targetDir the way every other - // hook surface above is. - if (resolveInstallPlan(runtime).hooksSurface === 'kimi-hooks-toml') { - removedCount += reclaimKimiHooksRoot(resolveKimiHooksTomlDir({ runtime })); - } - - // 1b. Non-layout Copilot side-effect: copilot-instructions.md cleanup - // #2099: descriptor-driven via resolveInstallPlan(runtime).installSurface === - // 'copilot-instructions' (was hardcoded `isCopilot`), mirroring the same - // gate used at the install-time 'copilot-instructions' branch. - if (resolveInstallPlan(runtime).installSurface === 'copilot-instructions') { - const instructionsPath = path.join(targetDir, 'copilot-instructions.md'); - if (fs.existsSync(instructionsPath)) { - const content = fs.readFileSync(instructionsPath, 'utf8'); - const cleaned = stripMsdFromCopilotInstructions(content); - if (cleaned === null) { - fs.unlinkSync(instructionsPath); - removedCount++; - console.log(` ${green}✓${reset} Removed copilot-instructions.md (was MSD-only)`); - } else if (cleaned !== content) { - fs.writeFileSync(instructionsPath, cleaned); - removedCount++; - console.log(` ${green}✓${reset} Cleaned MSD section from copilot-instructions.md`); - } - } - - // #786: remove the MSD-managed Copilot lifecycle hook config and prune the - // hooks dir if we left it empty. - const hookPath = path.join(targetDir, 'hooks', MSD_COPILOT_HOOK_FILE); - if (fs.existsSync(hookPath)) { - fs.unlinkSync(hookPath); - removedCount++; - console.log(` ${green}✓${reset} Removed Copilot lifecycle hook (${MSD_COPILOT_HOOK_FILE})`); - try { - const hooksDir = path.join(targetDir, 'hooks'); - if (fs.existsSync(hooksDir) && fs.readdirSync(hooksDir).length === 0) { - fs.rmdirSync(hooksDir); - } - } catch { /* non-fatal: leave a non-empty/locked hooks dir in place */ } - } - // Note: AGENTS.md (repo root) is cleaned earlier, before the targetDir - // existence early-return, since it lives outside targetDir (#786). - } - - // 1b-cline. Non-layout Cline side-effects (issue #787): remove the - // directory-form rules + PreToolUse hook, and strip the MSD block from the - // global cross-tool ~/.agents/AGENTS.md target. - // Descriptor-driven (ADR-1239 / #2090): folded from `runtime === 'cline'` - // into hostBehaviors.clineRulesSurface. - if (_hostBehaviors(runtime).clineRulesSurface) { - const clinerulesDir = path.join(targetDir, '.clinerules'); - for (const rel of ['msd.md', path.join('hooks', 'PreToolUse')]) { - const p = path.join(clinerulesDir, rel); - try { - if (fs.existsSync(p)) { - fs.unlinkSync(p); - removedCount++; - } - } catch { /* best-effort */ } - } - // Also remove a legacy single-file .clinerules left by pre-#787 installs. - try { - if (fs.existsSync(clinerulesDir) && fs.statSync(clinerulesDir).isFile()) { - fs.unlinkSync(clinerulesDir); - removedCount++; - } - } catch { /* best-effort */ } - // Prune now-empty MSD-created directories (leave any user-added rule files). - for (const dir of [path.join(clinerulesDir, 'hooks'), clinerulesDir]) { - try { - if (fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.readdirSync(dir).length === 0) { - fs.rmdirSync(dir); - } - } catch { /* best-effort */ } - } - if (isGlobal) { - const agentsPath = path.join(os.homedir(), '.agents', 'AGENTS.md'); - try { - if (fs.existsSync(agentsPath)) { - const content = fs.readFileSync(agentsPath, 'utf8'); - const cleaned = stripMsdFromAgentsMd(content); - if (cleaned === null) { - fs.unlinkSync(agentsPath); - removedCount++; - console.log(` ${green}✓${reset} Removed ~/.agents/AGENTS.md (was MSD-only)`); - } else if (cleaned !== content) { - fs.writeFileSync(agentsPath, cleaned); - removedCount++; - console.log(` ${green}✓${reset} Cleaned MSD section from ~/.agents/AGENTS.md`); - } - } - } catch { /* best-effort */ } - } - } - // 1b-cursor. Descriptor-driven hook-bus cleanup (ADR-1239 / #2089): remove // MSD-managed hook entries from hooks.json and clean up the managed hook // scripts. Gated by the hostBehaviors.hooksJsonSurface descriptor axis, not a @@ -8670,38 +5821,6 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) { } catch { /* best-effort */ } } - // 1b-windsurf. Descriptor-driven hook-bus cleanup (ADR-1239 / #2100 Stage 2): - // remove MSD-managed Cascade hook entries from hooks.json and clean up the - // managed hook scripts. Gated on resolveInstallPlan(runtime).hooksSurface - // === 'windsurf-hooks-json' (mirrors the kimi-hooks-toml gate above) — - // NOT the shared hostBehaviors.hooksJsonSurface flag the Cursor block above - // uses, since that flag drives Cursor's own remove function + script list - // and is not (and must not be) set for Windsurf. - if (resolveInstallPlan(runtime).hooksSurface === 'windsurf-hooks-json') { - const windsurfHooksJsonCleanup = removeWindsurfHooksJson(targetDir); - if (windsurfHooksJsonCleanup.changed) { - removedCount++; - console.log(` ${green}✓${reset} Removed MSD-managed Windsurf hooks from hooks.json`); - } - // Remove all MSD-managed hook scripts (pre_write_code, pre_run_command). - const windsurfHooksDir = path.join(targetDir, 'hooks'); - for (const script of MSD_WINDSURF_HOOK_SCRIPTS) { - const p = path.join(windsurfHooksDir, script); - try { - if (fs.existsSync(p)) { - fs.unlinkSync(p); - removedCount++; - } - } catch { /* best-effort */ } - } - // Prune hooks/ if empty. - try { - if (fs.existsSync(windsurfHooksDir) && fs.readdirSync(windsurfHooksDir).length === 0) { - fs.rmdirSync(windsurfHooksDir); - } - } catch { /* best-effort */ } - } - // 1c. Claude local: remove flat msd-*.md commands from commands/ (current layout, // #1367 fix). Also remove legacy commands/msd/ subdirectory from prior installs. if (!isGlobal && _hostBehaviors(runtime).localInstallStyle === 'legacy-flat') { @@ -8765,93 +5884,6 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) { } } - // 1d. Qwen/Hermes: migrate dev-preferences.md from legacy commands/msd/ location - // during uninstall. _runLegacyUninstallCleanup (called by uninstallRuntimeArtifacts) - // removes the directory; we must preserve/restore user artifacts before that path. - // This block runs AFTER uninstallRuntimeArtifacts, so we check if the directory - // was already removed and skip if so (idempotent). - if (_hostBehaviors(runtime).legacyCommandsMsdCleanup === true) { - // dev-preferences may have survived in skills/ as SKILL.md — nothing to do for - // that case. If a stale commands/msd/ still exists (e.g. legacy was not removed), - // attempt migration. In practice _runLegacyUninstallCleanup removes it first, - // so this is a best-effort guard. - const legacyDir = path.join(targetDir, 'commands', 'msd'); - if (fs.existsSync(legacyDir)) { - // #2875 (#1874-F19): staged DURABLY to disk before the wipe below, - // instead of an in-memory Map only — a crash between the wipe and the - // restore-on-failure branch below now survives via - // recoverOrphanedUserArtifacts on the next run. - // #2875 defect fix: DEGRADE, never abort uninstall, when the staging - // root cannot be resolved — skip this legacy-migration block entirely - // (leave the stale dir in place) rather than wipe without a durable - // backup. - const stagingRoot = _tryResolveUserArtifactStagingRoot(targetDir); - if (stagingRoot !== null) { - const stagedLegacyArtifacts = stageUserArtifacts(legacyDir, ['dev-preferences.md'], stagingRoot); - fs.rmSync(legacyDir, { recursive: true }); - removedCount++; - console.log(` ${green}✓${reset} Removed legacy commands/msd/`); - const _uninstallScope = scope; - // migrateLegacyDevPreferencesToSkill's Map contract is - // unchanged — read the staged content back from disk (not an in-memory - // value held across the wipe above). - // - // #2875 defect fix (readFileSync following a staged symlink) — matches - // install-engine.cts's _runLegacyInstallMigrations call site 1 exactly: - // readFileSync ALWAYS follows a symlink, so a staged artifact that is - // itself a symlink (user-artifact-staging.cts's "Symlink safety": a - // symlinked user artifact is recreated AS a symlink in the staging - // tree, never copied by content) would have its REFERENT's bytes read - // here and land in SKILL.md. Excluded from migration below and - // restored to its original location unchanged instead. - // #2875 defect fix (regression closed — was previously unguarded and - // BRICKED uninstall, the very command that should recover from this): - // legacyDir was already removed above, so stagedLegacyArtifacts is - // the only surviving copy. migrateLegacyDevPreferencesToSkill - // correctly THROWS when it finds a planted/dangling symlink at the - // skill-file leaf (security fix); a raw `fs.lstatSync` in the loop - // below can also throw on a TOCTOU-vanished staged file. Either one, - // left unguarded, propagated straight out of uninstall, aborting it - // WITHOUT ever reaching the restore-or-discard branch below — the - // staged batch was orphaned on disk and every retry hit the same - // throw again. Degrade identically to every other #2875 staging step - // in this function: catch, warn once, and treat the batch as - // unmigrated so the restore branch below always fires. - let _legacyMigrated = false; - let migratableLegacyNames = []; - try { - const savedLegacyArtifacts = new Map(); - for (const name of stagedLegacyArtifacts.names) { - const stagedPath = path.join(stagedLegacyArtifacts.filesDir, name); - if (fs.lstatSync(stagedPath).isSymbolicLink()) continue; - savedLegacyArtifacts.set(name, fs.readFileSync(stagedPath, 'utf8')); - migratableLegacyNames.push(name); - } - _legacyMigrated = migrateLegacyDevPreferencesToSkill(targetDir, savedLegacyArtifacts, runtime, _uninstallScope); - } catch (err) { - console.warn(` ${yellow}!${reset} dev-preferences.md migration skipped (${err.message}) — restoring the legacy copy instead.`); - _legacyMigrated = false; - migratableLegacyNames = []; - } - if (_legacyMigrated && migratableLegacyNames.length === stagedLegacyArtifacts.names.length) { - // Compute the actual path written so the log line is accurate per-runtime - const _layout = resolveRuntimeArtifactLayout(runtime, targetDir, _uninstallScope); - const _sk = _layout.kinds.find((k) => k.kind === 'skills'); - const _stem = _sk && _sk.prefix === '' ? 'dev-preferences' : 'msd-dev-preferences'; - const _skillRelPath = _sk ? `${_sk.destSubpath}/${_stem}/SKILL.md` : 'skills/msd-dev-preferences/SKILL.md'; - console.log(` ${green}✓${reset} Migrated dev-preferences.md → ${_skillRelPath} (#2973)`); - discardStagedUserArtifacts(stagedLegacyArtifacts); - } else { - // Migration failed, already exists, or a symlinked name was excluded - // above — restore the WHOLE batch to the legacy location so no user - // content is silently lost. - restoreStagedUserArtifacts(legacyDir, stagedLegacyArtifacts); - discardStagedUserArtifacts(stagedLegacyArtifacts); - } - } - } - } - // 2. Remove msd-core directory const msdDir = path.join(targetDir, 'msd-core'); if (fs.existsSync(msdDir)) { @@ -8969,7 +6001,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) { // MSD's exact content — a user-authored package.json is never deleted. // // #2717 reaches the runtimes that stage .js hooks via dedicated paths - // (cursor/windsurf/codex); #2544 reaches the shared-bundle runtimes, whose + // (cursor/codex); #2544 reaches the shared-bundle runtimes, whose // marker this PR moves out of the config root and into hooks/. Both land in // the same directory, so one guarded call covers both. try { @@ -8980,9 +6012,9 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) { } catch { /* best-effort */ } } - // 4z. Remove the native plugin adapter (#1914, extended to Kilo by #2093). + // 4z. Remove the native plugin adapter (#1914). // Descriptor-driven via hostBehaviors.nativePlugin — covers every runtime - // that declares the block (OpenCode, Kilo, ...), not just OpenCode. Only + // that declares the block (OpenCode, ...), not just OpenCode. Only // MSD's own plugin file is removed; the plugins/ dir is pruned only if it // becomes empty, preserving any user-authored plugins for that host. const _np = _hostBehaviors(runtime).nativePlugin; @@ -9106,8 +6138,8 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) { // Remove MSD hooks from settings — per-hook granularity to preserve // user hooks that share an entry with a MSD hook (#1755 followup). - // Includes the 3 Qwen-only events added in #788 (SubagentStop, Stop, - // PreCompact, also registered for Claude in #770), the 3 Antigravity-only + // Includes the 3 events added in #788 (SubagentStop, Stop, + // PreCompact, registered for Claude in #770), the 3 Antigravity-only // events added in #776 (BeforeAgent, AfterAgent, BeforeModel), and the // Claude-only FileChanged event added in #770 — safe to iterate for all // runtimes; installs that don't register these events simply find no @@ -9217,21 +6249,6 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) { } } - // #2097 UPGRADE 3 — Remove the MCP companion entry from settings.json for - // runtimes that host MCP there (Augment), symmetric to the mcp_config.json - // removal for Antigravity below. Only the MSD-owned mcpServers.msd key is - // removed — any other user-configured MCP servers are preserved. - if (_hostBehaviors(runtime).mcpCompanion === 'settings-json' && - settings.mcpServers && typeof settings.mcpServers === 'object' && - settings.mcpServers.msd !== undefined) { - delete settings.mcpServers.msd; - if (Object.keys(settings.mcpServers).length === 0) { - delete settings.mcpServers; - } - settingsModified = true; - console.log(` ${green}✓${reset} Removed MSD MCP companion server from settings.json`); - } - if (settingsModified) { writeSettings(settingsPath, settings); removedCount++; @@ -9279,49 +6296,6 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) { } } - // 7. For Kilo, clean up permissions from kilo.json or kilo.jsonc - // #2093: descriptor-driven via resolveInstallPlan(runtime).finishPermissionWriter, - // mirroring the OpenCode branch above (was hardcoded `isKilo`). - if (resolveInstallPlan(runtime).finishPermissionWriter === 'kilo') { - const configPath = resolveKiloConfigPath(targetDir); - if (fs.existsSync(configPath)) { - try { - const config = parseJsonc(fs.readFileSync(configPath, 'utf8')); - let modified = false; - - // Remove MSD permission entries - if (config.permission) { - for (const permType of ['read', 'external_directory']) { - if (config.permission[permType]) { - const keys = Object.keys(config.permission[permType]); - for (const key of keys) { - if (key.includes('msd-core')) { - delete config.permission[permType][key]; - modified = true; - } - } - // Clean up empty objects - if (Object.keys(config.permission[permType]).length === 0) { - delete config.permission[permType]; - } - } - } - if (Object.keys(config.permission).length === 0) { - delete config.permission; - } - } - - if (modified) { - fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); - removedCount++; - console.log(` ${green}✓${reset} Removed MSD permissions from ${path.basename(configPath)}`); - } - } catch (e) { - // Ignore JSON parse errors - } - } - } - // 8. For Antigravity, remove the MCP companion entry from mcp_config.json // (#2096 Phase B Upgrade 2). Only the MSD-owned mcpServers.msd key is // removed — any other user-configured MCP servers are preserved. @@ -9524,83 +6498,9 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) { console.log(` ${green}✓${reset} Configured read permission for MSD docs`); } -/** - * Configure Kilo permissions to allow reading MSD reference docs - * This prevents permission prompts when MSD accesses the msd-core directory - * @param {boolean} isGlobal - Whether this is a global or local install - * @param {string|null} configDir - Resolved config directory when already known - */ -function configureKiloPermissions(isGlobal = true, configDir = null) { - // For local installs, use ./.kilo/ - // For global installs, use ~/.config/kilo/ - const kiloConfigDir = configDir || (isGlobal - ? getGlobalConfigDir('kilo', explicitConfigDir) - : path.join(process.cwd(), '.kilo')); - // Ensure config directory exists - fs.mkdirSync(kiloConfigDir, { recursive: true }); - - const configPath = resolveKiloConfigPath(kiloConfigDir); - - // Read existing config or create empty object - let config = {}; - if (fs.existsSync(configPath)) { - try { - const content = fs.readFileSync(configPath, 'utf8'); - config = parseJsonc(content); - } catch (e) { - // Cannot parse - DO NOT overwrite user's config - const configFile = path.basename(configPath); - console.log(` ${yellow}⚠${reset} Could not parse ${configFile} - skipping permission config`); - console.log(` ${dim}Reason: ${e.message}${reset}`); - console.log(` ${dim}Your config was NOT modified. Fix the syntax manually if needed.${reset}`); - return; - } - } - - // Ensure permission structure exists - if (!config.permission || typeof config.permission !== 'object') { - config.permission = {}; - } - - // Build the MSD path using the actual config directory - // Use ~ shorthand if it's in the default location, otherwise use full path - const defaultConfigDir = path.join(os.homedir(), '.config', 'kilo'); - const msdPath = kiloConfigDir === defaultConfigDir - ? '~/.config/kilo/msd-core/*' - : `${kiloConfigDir.replace(/\\/g, '/')}/msd-core/*`; - - let modified = false; - - // Configure read permission - if (!config.permission.read || typeof config.permission.read !== 'object') { - config.permission.read = {}; - } - if (config.permission.read[msdPath] !== 'allow') { - config.permission.read[msdPath] = 'allow'; - modified = true; - } - - // Configure external_directory permission (the safety guard for paths outside project) - if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') { - config.permission.external_directory = {}; - } - if (config.permission.external_directory[msdPath] !== 'allow') { - config.permission.external_directory[msdPath] = 'allow'; - modified = true; - } - - if (!modified) { - return; // Already configured - } - - // Write config back - fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); - console.log(` ${green}✓${reset} Configured read permission for MSD docs`); -} - /** * Convert an absolute path to a `~`-relative form when it lives under the - * user's home directory (generalizes configureKiloPermissions' + * user's home directory (generalizes configureOpencodePermissions' * single-default-dir shorthand to Antigravity's three probed sibling config * dirs — antigravity/antigravity-ide/antigravity-cli under ~/.gemini — none of * which is a single fixed "default"). @@ -9633,13 +6533,13 @@ function buildAntigravityAllowRules(configDir) { /** * Configure Antigravity permissions to allow reading/executing MSD's installed * tree without per-call approval prompts (#2096 Phase B Upgrade 1 — mirrors - * configureKiloPermissions/configureOpencodePermissions). + * configureOpencodePermissions). * * Antigravity's permission schema (antigravity.google/docs/cli/permissions) is * `{"permissions":{"allow":[...],"deny":[...],"ask":[...]}}`, living in the * SAME settings.json MSD's own hook registration writes for this runtime * (installSurface: 'settings-json', writesSharedSettings: true) — unlike - * Kilo/OpenCode, which write a separate native config file. This function + * OpenCode, which writes a separate native config file. This function * re-reads the file (already containing MSD's hooks by the time finishInstall * reaches this call) and only appends to permissions.allow. * @@ -9716,7 +6616,7 @@ function configureAntigravityPermissions(isGlobal = true, configDir = null) { * OWN workspace-scope convention exactly (`.agents/mcp_config.json`, which IS * MSD's local configDir for antigravity), keeping global/local symmetric and * consistent with the configDir-relative convention every other MSD - * permission writer (kilo/opencode) already uses. + * permission writer (opencode) already uses. * * Non-destructive + idempotent: only adds mcpServers.msd when entirely absent; * any other user-configured mcpServers entries (or a user's OWN "msd" override) @@ -9764,27 +6664,6 @@ function configureAntigravityMcpConfig(isGlobal = true, configDir = null) { console.log(` ${green}✓${reset} Configured Antigravity MCP companion server (msd)`); } -/** - * #2097 (ADR-1239 transport:mcp): register the MSD companion MCP server inside a - * runtime's settings.json (Augment hosts MCP in settings.json.mcpServers, unlike - * Antigravity's standalone mcp_config.json). Mutates the in-memory settings object - * that finishInstall already writes — non-destructive + idempotent: only sets - * mcpServers.msd, preserving any user-defined servers (a user's own `msd` override - * is respected — Hyrum's Law). - * @param {object} settings - the in-memory settings object finishInstall will write - */ -function mergeMsdMcpServerIntoSettings(settings) { - if (!settings.mcpServers || typeof settings.mcpServers !== 'object' || Array.isArray(settings.mcpServers)) { - settings.mcpServers = {}; - } - if (settings.mcpServers.msd === undefined) { - settings.mcpServers.msd = { - command: 'npx', - args: ['-y', '-p', PACKAGE_NAME, 'msd-mcp-server'], - }; - } -} - /** * Verify a directory exists and contains files */ @@ -9894,32 +6773,17 @@ function resolveInstallRelativePath(baseDir, relPath) { * Write file manifest after installation for future modification detection */ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) { - // #2093: isKilo dropped — unused in this function. - // #2094: isTrae dropped — was only used in the hooks-tracking conditional - // above, now covered by hostBehaviors.skipSharedHooksInstall. - // #2095: isKimi dropped — kimi is now a hooks/ consumer like every other - // settings-json-adjacent runtime, so the `&& !isKimi` term below was removed. - // #2096: isAntigravity dropped — unused in this function. - // #2098: isCodebuddy dropped — unused in this function. - // #2099: isCopilot dropped — was only used in the hooks-tracking conditional - // above, now covered by hostBehaviors.skipSharedHooksInstall. - // #2100: isWindsurf dropped — was only used in the hooks-tracking conditional - // above, now covered by hostBehaviors.skipSharedHooksInstall. - const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime); + const { isOpencode, isCodex, isCursor } = runtimeFlags(runtime); const msdDir = path.join(configDir, 'msd-core'); // #1367: Claude local now writes flat msd-*.md files at commands/ (not commands/msd/). // Claude local uses flatCommandsDir instead for manifest recording. const flatCommandsDir = path.join(configDir, 'commands'); const opencodeCommandDir = path.join(configDir, _hostBehaviors(runtime).flatCommandDir || 'command'); - // Hermes nests MSD skills under skills/msd/ as a single category (#2841) — - // already encoded in its layout descriptor's destSubpath ('skills/msd'). - // All other runtimes that use the Codex-style skills layout use a flat skills/ root. // ADR-1239 upgrade 3 (#2088): honor a skills-kind `home` override (e.g. Codex // skills -> $HOME/.agents/skills instead of configDir/skills) via the same // descriptor-driven helper used by the snapshot/rollback/verification paths, // so the manifest records what's actually on disk. _resolveSkillsRootDir already - // resolves destSubpath (which includes hermes's 'skills/msd' nesting) — do not - // re-append 'msd' or the hermes dir gets double-nested to skills/msd/msd. + // resolves destSubpath — do not re-append anything. // #2872 (ADR-2866 Phase 3): the scope used to pick the skills root and the // scope RECORDED in the manifest are one value, resolved once. Two reads of // `options.scope` could drift; one cannot. @@ -9932,7 +6796,6 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) { // manifest blind to the whole agents surface the moment the override landed — // no drift detection, no patch backup. Falls back to /agents. const agentsDir = _kindDestDirSafe(runtime, configDir, resolvedScope, 'agents') - || _kindDestDirSafe(runtime, configDir, resolvedScope, 'kimi-agents') || path.join(configDir, 'agents'); const manifest = { // Schema version of this DOCUMENT (#2872) — distinct from `version` @@ -9997,7 +6860,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) { } } if (!_hostBehaviors(runtime).skipCodexSkillsManifest && fs.existsSync(codexSkillsDir)) { - // All runtimes (including Hermes post-#947) use the canonical 'msd-' prefix. + // All runtimes use the canonical 'msd-' prefix. const skillListPrefix = 'msd-'; for (const skillName of listCodexSkillNames(codexSkillsDir, skillListPrefix)) { const skillRoot = path.join(codexSkillsDir, skillName); @@ -10006,56 +6869,18 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) { manifest.files[`${codexSkillsManifestPrefix}${skillName}/${rel}`] = hash; } } - // Descriptor-driven (#2090): hash the category DESCRIPTION.md so reinstall detects drift. - if (_hostBehaviors(runtime).trackCategoryDescription) { - const descPath = path.join(codexSkillsDir, 'DESCRIPTION.md'); - if (fs.existsSync(descPath)) { - manifest.files['skills/msd/DESCRIPTION.md'] = fileHash(descPath); - } - } } - if (_hostBehaviors(runtime).agentManifestStyle === 'kimi-nested' && fs.existsSync(agentsDir)) { - const agentHashes = generateManifest(agentsDir); - for (const [rel, hash] of Object.entries(agentHashes)) { - const isRootAgent = rel === 'msd.yaml' || rel === 'msd.md'; - const isSubagent = /^subagents\/msd-[^/]+\.(yaml|md)$/.test(rel); - if (isRootAgent || isSubagent) { - manifest.files['agents/' + rel] = hash; - } - } - } else if (fs.existsSync(agentsDir)) { + if (fs.existsSync(agentsDir)) { for (const file of fs.readdirSync(agentsDir)) { if (file.startsWith('msd-') && (file.endsWith('.md') || file.endsWith('.toml'))) { manifest.files['agents/' + file] = fileHash(path.join(agentsDir, file)); } } } - // Track Cline directory-form artifacts in the manifest (issue #787): the - // rules file and the PreToolUse hook. (~/.agents/AGENTS.md is tracked via its - // marker block, not the per-configDir manifest, since it lives outside it.) - // Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into - // hostBehaviors.clineRulesSurface. - if (_hostBehaviors(runtime).clineRulesSurface) { - for (const rel of ['.clinerules/msd.md', '.clinerules/hooks/PreToolUse']) { - const dest = path.join(configDir, rel); - if (fs.existsSync(dest)) { - manifest.files[rel] = fileHash(dest); - } - } - } - // Track hook files so saveLocalPatches() can detect user modifications - // Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline) - // Descriptor-driven (ADR-1239 / #2089+#2090): cline's exclusion is via - // hostBehaviors.skipSharedHooksInstall (was hardcoded !isCline). - // #2094: Trae's exclusion is likewise descriptor-driven (trae declares - // skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed. - // #2095: kimi is now a hooks/ consumer (native config.toml [[hooks]] bus) — - // the redundant `&& !isKimi` was removed so its hook files are tracked too. - // #2099: Copilot's exclusion is likewise descriptor-driven (copilot declares - // skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed. - // #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares - // skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed. + // Hooks are only installed for runtimes that use settings.json (not Codex). + // Descriptor-driven (ADR-1239 / #2089): exclusions via + // hostBehaviors.skipSharedHooksInstall. if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) { // #3023: manifest keys must track the bundle wherever the descriptor put it, // or uninstall/saveLocalPatches silently orphan the tree. @@ -10604,42 +7429,7 @@ function reportInstallerMigrationResult(result) { } function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { - // #2093: isKilo dropped — Kilo's agent/model-override handling below reads - // _hostBehaviors(runtime).frontmatterDialect === 'kilo' instead of this flag. - // #2095: isKimi dropped — kimi is now a hooks/ consumer like every other - // settings-json-adjacent runtime; the two `&& !isKimi` hooks-copy guards - // below were removed, leaving isKimi unused in this function (the kimi - // local-install-deferred branch above already reads - // _hostBehaviors(runtime).localInstallDeferred instead of this flag). - // #2096: isAntigravity dropped — antigravity's agents were already - // descriptor-driven (installRuntimeArtifacts), so its two legacy-agent-loop - // branches (the path-rewrite skip and the converter dispatch) were - // unreachable dead code; both were removed rather than re-gated on - // hostBehaviors. (#2875 Part 2 later deleted that legacy loop and its - // `_DESCRIPTOR_AGENTS_RUNTIMES` gate entirely — EVERY runtime is now - // descriptor-driven for agents, not just this subset.) - // #2098: isCodebuddy dropped — codebuddy's agents were likewise already - // descriptor-driven, so its legacy converter-dispatch branch (the - // `isCodebuddy` arm calling convertClaudeAgentToCodebuddyAgent) was - // unreachable dead code and was removed rather than re-gated. - // #2099: isCopilot dropped — copilot's agents were likewise already - // descriptor-driven, so its three legacy-agent-loop branches (the - // path-rewrite skip, the converter dispatch, and the .agent.md destName - // ternary) were unreachable dead code and were removed rather than - // re-gated; the .agent.md suffix now lives on - // hostBehaviors.agentFileExtension in src/install-engine.cts, and the - // skipSharedHooksInstall check above no longer needs `&& !isCopilot`. - // #2100: isWindsurf dropped — its four former isWindsurf-gated branches - // (legacy .devin/skills/msd-* cleanup, the #1629 command-bodies copy, the - // workflow-verification report, and the shared-hooks-install exclusion) are - // now descriptor-driven via hostBehaviors.legacyDevinSkillsCleanup, - // hostBehaviors.installsCommandBodiesForWorkflowDelegation, - // hostBehaviors.verificationStyle === 'windsurf-workflows', and - // hostBehaviors.skipSharedHooksInstall respectively; its legacy-agent-loop - // converter arm was likewise unreachable dead code (windsurf's agents were - // already descriptor-driven) and was removed above. - // #2101: isZcode dropped — folded onto hostBehaviors.skipSharedHooksInstall. - const { isOpencode, isCodex, isCursor, isAugment, isTrae, isQwen, isHermes, isCline } = runtimeFlags(runtime); + const { isOpencode, isCodex, isCursor } = runtimeFlags(runtime); const plan = resolveInstallPlan(runtime); const dirName = getDirName(runtime); const src = path.join(__dirname, '..'); @@ -10650,27 +7440,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { // re-running install()) can warn again if the same condition recurs. _codexResolverModelOmittedWarned = false; - if (_hostBehaviors(runtime).localInstallDeferred && !isGlobal) { - console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 2.`); - console.log(` No .kimi-code/skills or .agents/skills project artifacts were written.`); - console.log(` Project-level Kimi install semantics remain deferred.`); - return { - runtime, - skipped: true, - reason: 'kimi_local_deferred', - configDir: null, - settingsPath: null, - settings: null, - statuslineCommand: null, - updateBannerCommand: null, - rollbackInstallerMigrations: () => {}, - }; - } - // #2870: scope id resolved ONCE here and reused at every use below (was 10 - // independent isGlobal-derived re-derivations). Placed AFTER the kimi - // local-deferred early return above so that return path does no extra - // work. Routed through the Install Scope Module (src/install-scope.cts) + // independent isGlobal-derived re-derivations). Routed through the Install Scope Module (src/install-scope.cts) // when the capability registry is available; degrades to the plain id on // failure (unknown/non-installable runtime, broken bundle) so this // function's plain scope-id uses — which never depended on registry @@ -10711,19 +7482,13 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { }; // Get the target directory based on runtime and install type. - // Descriptor-driven (ADR-1239 / #2090): cline local installs write to the - // project root (like Claude Code) — .clinerules lives at the root, not inside - // a .cline/ subdirectory. Folded from `isCline` into - // hostBehaviors.localTargetIsProjectRoot. // #791: antigravity local installs write to .agents/ (canonical). The legacy .agent/ // directory is recognized by RUNTIME_DIRS (update-context) and _LEGACY_SCAN_SUBDIR_NAMES // but NOT auto-removed here; legacy .agent/ msd artifacts are recognized but not // auto-removed on reinstall (dual-read fallback per issue #791 spec). const targetDir = isGlobal ? getGlobalConfigDir(runtime, explicitConfigDir) - : _hostBehaviors(runtime).localTargetIsProjectRoot - ? process.cwd() - : path.join(process.cwd(), dirName); + : path.join(process.cwd(), dirName); // #3664: a --config-dir destination holding foreign agent files gets an // explicit install-time warning — never a silent Claude-shaped emit. @@ -10844,7 +7609,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { // copyWithPathReplacement, i.e. the one actually written into every // emitted command/skill/workflow body — the rewrite-engine seams below // handle re-applied surfaces, not the first install. - localDirName: _hostBehaviors(runtime).localTargetIsProjectRoot === true ? undefined : getDirName(runtime), + localDirName: getDirName(runtime), }); // runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239 @@ -11357,7 +8122,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { // Artifact install dispatcher — routes to layout-driven path for all // skills-based runtimes (both full and minimal/core profiles); keeps - // back-compat paths for commands-based runtimes (OpenCode/Kilo/ + // back-compat paths for commands-based runtimes (OpenCode/ // Claude-local). // // installRuntimeArtifacts handles legacy migration + skill/agent staging @@ -11365,22 +8130,18 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { // the user's --profile=core / --minimal choice. // // Non-layout side-effects preserved inline: - // Hermes: writeHermesCategoryDescription (not a layout kind) - // Cline global: skills emitted via layout; .clinerules still written below (#782) - // Cline local: no skills (only .clinerules) — falls through to cline-rules surface // Claude local: copyWithPathReplacement + stale-skills cleanup // Layout-driven path for all skills-based runtimes (full and minimal modes). // applyRuntimeContentRewritesInPlace (called inside installRuntimeArtifacts) - // handles per-runtime path + branding rewrites, including Qwen/Hermes. - // Cline global: emit skills to ~/.cline/skills/ (Cline >= v3.48.0 — #782). + // handles per-runtime path + branding rewrites. // Descriptor-driven (ADR-1016 / ADR-1239): a runtime takes the layout-driven // installRuntimeArtifacts path when its scoped artifactLayout is non-empty - // (it declares any skills/commands/agents/kimi-agents kind for this scope). - // This replaces the prior hardcoded `isCodex || isCopilot || ...` roster so a + // (it declares any skills/commands/agents kind for this scope). + // This replaces the prior hardcoded `isCodex || ...` roster so a // newly-added runtime with an artifact layout installs without a per-runtime - // branch — the add-a-host tax ADR-1239 Phase B retires. OpenCode/Kilo now - // route through this SAME path too: their hostBehaviors.combinedFamilyInstall + // branch — the add-a-host tax ADR-1239 Phase B retires. OpenCode now + // routes through this SAME path too: its hostBehaviors.combinedFamilyInstall // flag makes installRuntimeArtifacts (in src/install-engine.cts) delegate to // installOpencodeFamilyArtifacts for the combined commands+skills+native-plugin // install (ADR-1239 / #2087), replacing the bespoke inline block this comment @@ -11388,17 +8149,6 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { // (copyWithPathReplacement + stale-skills cleanup). const _isSkillsRuntime = (() => { if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086) - // #2875 Part 2 defect fix: a runtime whose LOCAL commands are embedded in a - // rules file rather than materialized as files (hostBehaviors.localCommandsViaRules - // — cline is the only declarant, capabilities/cline/capability.json) must not - // flip into this skills/commands-reporting branch merely because its local - // artifactLayout now also declares an `agents` kind (#2875 Part 2 cline-local - // agents regression fix). That branch's own verification reporting expects a - // skills/ or commands/ directory this runtime never writes locally and would - // spuriously fail; the `localCommandsViaRules` branch below (unchanged - // messaging) and the unconditional agents-materialization block further down - // (installAgentsKindStandalone) already cover this runtime/scope correctly. - if (!isGlobal && _hostBehaviors(runtime).localCommandsViaRules) return false; const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime]; const layout = cap && cap.runtime && cap.runtime.artifactLayout; if (!layout) return false; @@ -11537,82 +8287,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { } } - // #1629 Finding B: Windsurf local only — remove legacy .devin/skills/msd-* - // dirs from pre-#1615 installs. #1615 moved Windsurf to .windsurf/workflows/ - // but never cleaned up the old .devin/skills/ layout (#1085). User-owned - // content is preserved (non-msd- dirs, msd-dev-preferences, symlinks). - // Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into - // hostBehaviors.legacyDevinSkillsCleanup (windsurf is the only runtime that - // declares it, so this is byte-parity). - if (_hostBehaviors(runtime).legacyDevinSkillsCleanup && !isGlobal) { - const removedCount = cleanupWindsurfLegacyDevinSkills(process.cwd()); - if (removedCount > 0) { - console.log(` ${green}✓${reset} Removed ${removedCount} legacy .devin/skills/msd-* dir(s) (pre-#1615 Windsurf layout)`); - } - } - - // Descriptor-driven (#2090): write DESCRIPTION.md for the msd/ category after layout install - if (_hostBehaviors(runtime).writeCategoryDescription) { - writeHermesCategoryDescription(path.join(targetDir, 'skills', 'msd')); - } - // Verify installed artifacts and report - if (_hostBehaviors(runtime).reportSkillsCount) { - const hermesSkillsDir = path.join(targetDir, 'skills', 'msd'); - if (fs.existsSync(hermesSkillsDir)) { - // Hermes layout uses prefix: 'msd-' (#947) — skill dirs have msd- names - const count = fs.readdirSync(hermesSkillsDir, { withFileTypes: true }) - .filter(e => e.isDirectory() && e.name.startsWith('msd-')).length; - if (count > 0) { - console.log(` ${green}✓${reset} Installed ${count} skills to skills/msd/`); - } else { - failures.push('skills/msd/*'); - } - } else { - failures.push('skills/msd/*'); - } - } else if (_hostBehaviors(runtime).verificationStyle === 'kimi') { - const skillsDir = path.join(targetDir, 'skills'); - const rootAgentPath = path.join(targetDir, 'agents', 'msd.yaml'); - if (fs.existsSync(skillsDir)) { - const count = fs.readdirSync(skillsDir, { withFileTypes: true }) - .filter(e => e.isDirectory() && e.name.startsWith('msd-')).length; - if (count > 0) { - console.log(` ${green}✓${reset} Installed ${count} Kimi skills to skills/`); - } else { - failures.push('skills/msd-*'); - } - } else { - failures.push('skills/msd-*'); - } - if (fs.existsSync(rootAgentPath)) { - console.log(` ${green}✓${reset} Generated Kimi root agent: ${rootAgentPath}`); - console.log(` Launch with: kimi --agent-file ${rootAgentPath}`); - } else { - failures.push('agents/msd.yaml'); - } - // Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into - // hostBehaviors.verificationStyle === 'windsurf-workflows' (extends the - // same mechanism the 'kimi' verificationStyle branch above uses; windsurf - // is the only runtime that declares this value, so this is byte-parity). - } else if (_hostBehaviors(runtime).verificationStyle === 'windsurf-workflows') { - if (isGlobal) { - console.log(` ${green}✓${reset} Windsurf global install skipped workflow artifacts (workspace-only)`); - } else { - const workflowsDir = path.join(targetDir, 'workflows'); - if (fs.existsSync(workflowsDir)) { - const workflowCount = fs.readdirSync(workflowsDir) - .filter(f => f.startsWith('msd-') && f.endsWith('.md')).length; - if (workflowCount > 0) { - console.log(` ${green}✓${reset} Installed ${workflowCount} workflows to workflows/`); - } else { - failures.push('workflows/msd-*'); - } - } else { - failures.push('workflows/msd-*'); - } - } - } else { + { const skillsDir = _skillsRootDir; if (fs.existsSync(skillsDir)) { const count = fs.readdirSync(skillsDir, { withFileTypes: true }) @@ -11625,58 +8301,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { } else { failures.push('skills/msd-*'); } - // Augment: also verify commands/ (emitted alongside skills/) - if (isAugment) { - const commandsDir = path.join(targetDir, 'commands'); - if (fs.existsSync(commandsDir)) { - const cmdCount = fs.readdirSync(commandsDir) - .filter(f => f.startsWith('msd-') && f.endsWith('.md')).length; - if (cmdCount > 0) { - console.log(` ${green}✓${reset} Installed ${cmdCount} commands to commands/`); - } else { - failures.push('commands/msd-*'); - } - } else { - failures.push('commands/msd-*'); - } - } - - // Descriptor-driven commands/ output report (currently CodeBuddy). - // Cursor retired this parallel surface in #2644 because its skills are - // already slash-menu entries as well as model-invocable context. - if (_hostBehaviors(runtime).reportCommandsDir) { - const commandsDir = path.join(targetDir, 'commands'); - if (fs.existsSync(commandsDir)) { - const cmdCount = fs.readdirSync(commandsDir) - .filter(f => f.startsWith('msd-') && f.endsWith('.md')).length; - if (cmdCount > 0) { - console.log(` ${green}✓${reset} Installed ${cmdCount} slash commands to commands/`); - } else { - failures.push('commands/msd-*'); - } - } else { - failures.push('commands/msd-*'); - } - } } - } else if (_hostBehaviors(runtime).localCommandsViaRules) { - // Cline local install: rules-based only — commands are embedded in .clinerules (generated below). - // No skills/commands directory needed for local installs. - // Global installs are handled above by _isSkillsRuntime (#782). - // Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into - // hostBehaviors.localCommandsViaRules. - console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`); - } else if (_hostBehaviors(runtime).pluginOnlyInstall) { - // pi (ADR-1239 / #2102 Stage 1): plugin-only install — pi's /msd command is - // registered programmatically by the native extension (pi/msd.cjs → - // extensions/msd.js, staged separately below; the dest suffix must be - // .ts/.js or pi's auto-discovery skips it silently — #2470) and dispatches in-process - // through the embedded msd-core command-routing hub. pi has no host-read - // markdown surface (unlike Claude/OpenCode/etc., which scan commands/ or - // command/ directories), so writing flat msd-.md files here would be - // dead weight the extension never reads. Skip the flat-commands fallback - // entirely for pluginOnlyInstall runtimes. - console.log(` ${green}✓${reset} pi: /msd registered via native extension (no declarative command files)`); } else { // Claude Code local: flat msd-.md layout — Claude Code registers // commands from .claude/commands/ using the filename stem as the command @@ -11766,45 +8391,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { } } - // Native-extension/plugin staging for runtimes OUTSIDE the layout-driven - // _isSkillsRuntime branch above (ADR-1239 / #2102 Stage 1: pi). OpenCode/Kilo - // already get their nativePlugin file from installOpencodeFamilyArtifacts - // (called inside the _isSkillsRuntime branch, since both declare a non-empty - // artifactLayout) — guard on `!_isSkillsRuntime` so this standalone call never - // double-stages their plugin file. A runtime like pi, whose artifactLayout is - // intentionally empty for both scopes (`_isSkillsRuntime` is false), still - // needs its declared hostBehaviors.nativePlugin file copied into targetDir. - if (!_isSkillsRuntime && _hostBehaviors(runtime).nativePlugin) { - _installNativePluginIfDeclared(runtime, targetDir, _hostBehaviors(runtime), src); - } - // #2624: the .msd-source marker is now written by _writeMsdSourceMarker() // BEFORE staging reads it (see the early call above the _isSkillsRuntime // block). The former write lived here — AFTER staging — which on an upgrade // let staging read a stale prior-version marker and silently install // old-version skill content. Moved up; this site intentionally left empty. - // #1629 critical fix: Windsurf workflow wrappers (convertClaudeCommandToWindsurfWorkflow) - // delegate to command bodies at /msd-core/commands/msd/${stem}.md via a - // hardcoded @~/.claude/msd-core/commands/msd/ path that _applyRuntimeRewrites rewrites - // to the install target. The source msd-core/ dir does NOT ship with commands/ — - // the canonical command source lives at the package root (commands/msd/). Without - // this copy, every /msd-* workflow in Cascade references a missing file and the LLM - // cannot execute the command body. Surfaced by the #1629 regression test after the - // original adversarial review of #1622 missed it. - // Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into - // hostBehaviors.installsCommandBodiesForWorkflowDelegation (windsurf is the - // only runtime that declares it, so this is byte-parity — the #1629 fix - // itself is unchanged). - if (_hostBehaviors(runtime).installsCommandBodiesForWorkflowDelegation && !isGlobal) { - const commandsSrc = path.join(src, 'commands', 'msd'); - const commandsDest = path.join(skillDest, 'commands', 'msd'); - if (fs.existsSync(commandsSrc)) { - copyWithPathReplacement(commandsSrc, commandsDest, pathPrefix, runtime, true, isGlobal, targetDir); - console.log(` ${green}✓${reset} Installed command bodies to msd-core/commands/msd/ (workflow delegation targets)`); - } - } - // Copy shared manifests into the msd-core payload // at the co-located path that CJS modules resolve first: // msd-core/bin/shared/*.json @@ -11839,7 +8431,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { // this function, the `_isSkillsRuntime` branch above) already wrote // agents/ for any runtime whose capability.json declares an `agents` kind, // via convertedAgentsKind/agentsKind (generic layout loop) or - // installAgentsKindStandalone (OpenCode/Kilo's combinedFamilyInstall + // installAgentsKindStandalone (OpenCode's combinedFamilyInstall // branch, called from within installOpencodeFamilyArtifacts) — both reuse // the SAME stageAgentsForRuntimeWithConverter pipeline (path-rewrite → // attribution → converter → frontmatter extensions → normalize) the @@ -11851,18 +8443,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { // the ten runtimes cut over before this change: via resolvedProfile.agents // at staging time, not a separate branch here. // - // `!_isSkillsRuntime` runtimes (claude-local's legacy-flat local path, and - // pi) never reach that loop at all — installAgentsKindStandalone is called + // `!_isSkillsRuntime` runtimes (claude-local's legacy-flat local path) + // never reach that loop at all — installAgentsKindStandalone is called // here explicitly to cover them (install-engine.cts's own doc comment // explains why; a regression here was caught by the install-tree golden - // fixture, tests/fixtures/install-tree/claude-local.json). It is a no-op - // for pi: its capability.json declares an EMPTY artifactLayout for both - // scopes (programmatic dispatch, no named-dispatch subagent toolkit, no - // host-read markdown surface), so the resolved layout has no `agents` kind - // to stage and the function returns `null` without writing anything. - if (_hostBehaviors(runtime).pluginOnlyInstall) { - console.log(` ${green}✓${reset} pi: no subagent files (programmatic dispatch, no named-dispatch toolkit)`); - } else if (_isSkillsRuntime) { + // fixture, tests/fixtures/install-tree/claude-local.json). + if (_isSkillsRuntime) { console.log(` ${dim}↳${reset} Agents installed via descriptor-driven layout (${runtime})`); } else { const _standaloneProjectDir = isGlobal ? process.cwd() : targetDir; @@ -11956,33 +8542,24 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { // Reusable: copy hooks/dist/ + hooks/lib/ into destRootDir, writing the // CommonJS package.json marker alongside them. Used below for the generic - // configDir install path (guarded by hostBehaviors.skipSharedHooksInstall), - // and — since #2095 — for Kimi's OWN native hook-install root (~/.kimi, - // resolved by resolveKimiHooksTomlDir), a directory entirely separate from - // Kimi's configDir/agents-root. Kimi's contract forbids hooks/ or - // package.json under its generic Agent-Skills root (see - // capabilities/kimi/capability.json hostBehaviors.skipSharedHooksInstall - // and the kimi-hooks-toml branch further below), so its shared-hooks bundle - // is installed into its own root via this same helper instead. + // configDir install path (guarded by hostBehaviors.skipSharedHooksInstall). // Returns false when hooks/dist/ exists but failed to verify post-copy (a // genuine failure the caller should surface); true otherwise (including // when hooks/dist/ is absent from the package — nothing to verify). function installSharedHooksBundle(destRootDir) { // destRootDir already exists for the generic call site (targetDir — created - // earlier in install() by the skills/agents writes above). It does NOT yet - // exist for kimi's call site (~/.kimi, resolved by resolveKimiHooksTomlDir): - // a fresh install has never created that dir before. mkdirSync recursive is - // a safe no-op when the dir is already present. + // earlier in install() by the skills/agents writes above). mkdirSync + // recursive is a safe no-op when the dir is already present. fs.mkdirSync(destRootDir, { recursive: true }); // #3023: the bundle's directory NAME is descriptor-driven — a host that - // reserves `hooks/` (pi) must be able to opt out. Resolved once here so the + // reserves `hooks/` must be able to opt out. Resolved once here so the // stage / lib / marker sites can never disagree about where the bundle is. const sharedHooksDirName = resolveSharedHooksDirName(runtime); // #2544: the CommonJS marker is NOT written here (destRootDir is the - // runtime's shared config root — user-writable territory on OpenCode and - // Kilo, where it is the documented place to declare local-plugin npm + // runtime's shared config root — user-writable territory on OpenCode, + // where it is the documented place to declare local-plugin npm // dependencies). It is written into hooks/ below, the directory MSD // creates and fills with its own .js scripts, once that directory exists. @@ -12009,16 +8586,6 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { content = content.replace(/'\.claude'/g, configDirReplacement); content = content.replace(/\/\.claude\//g, `/${getDirName(runtime)}/`); content = content.replace(/\.claude\//g, `${getDirName(runtime)}/`); - // Descriptor-driven (ADR-1239 / #2092): folded from separate - // `isQwen` / hermes-hardcoded branches into a single read of - // runtime.hostBehaviors.brandingRewrites. This site only - // rewrites the two brand-name keys (no `.claude/` here — the - // config-dir replace above already handled path fragments). - const _b2 = _hostBehaviors(runtime).brandingRewrites; - if (_b2) { - content = content.replace(/CLAUDE\.md/g, _b2['CLAUDE.md']); - content = content.replace(/\bClaude Code\b/g, _b2['Claude Code']); - } // #376: rewrite msd: → msd- for hyphen-namespace runtimes if (shouldNormalizeHyphenNamespaceInAgentBody(runtime)) { content = content.replace(/msd:/gi, 'msd-'); @@ -12075,15 +8642,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { } // Gate hooks/lib/ install on the same set of runtimes that receive hooks/. - // Codex/Copilot/Cursor/Windsurf/Trae/Cline do not use the shared - // hooks/lib/ helpers (Cursor uses standalone .js hook scripts registered - // via hooks.json — gated descriptor-driven via - // hostBehaviors.skipSharedHooksInstall, #2089; Cline likewise #2090; - // Trae likewise #2094; Codex uses hooks.json directly; - // the others skip hooks entirely); ZCode also skips hooks entirely - // (hooksSurface:'none' with no plugin surface — #1821). Kilo is NOT - // excluded since #2305: its native plugin adapter (#2093) spawns the - // staged hooks/*.js scripts, same as OpenCode. None of the + // Codex/Cursor do not use the shared hooks/lib/ helpers (Cursor uses + // standalone .js hook scripts registered via hooks.json — gated + // descriptor-driven via hostBehaviors.skipSharedHooksInstall, #2089; + // Codex uses hooks.json directly); ZCode also skips hooks entirely + // (hooksSurface:'none' with no plugin surface — #1821). None of the // excluded runtimes must receive the hooks/lib/ helpers — otherwise the // Codex comment downstream ("we deliberately do *not* copy hooks/lib/ for // Codex") is contradicted in practice. (Gating lives at the call sites @@ -12149,30 +8712,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { // OpenCode also declares hooksSurface:'none' but is deliberately NOT excluded: // its native plugin adapter (#1914, installed above under plugins/msd-core.js) // spawns the staged hooks/*.js scripts via OpenCode's event bus and needs both - // them and the CommonJS package.json marker written below. Kilo is the same - // shape since #2093 (a nativePlugin spawning the staged hooks), so it must - // NOT skip either — declaring skipSharedHooksInstall:true alongside a - // nativePlugin left every guard the plugin spawns a silent no-op (#2305). + // them and the CommonJS package.json marker written below. // #2089: Cursor's exclusion is now descriptor-driven via // hostBehaviors.skipSharedHooksInstall (was hardcoded !isCursor). - // #2090: Cline's exclusion is likewise descriptor-driven (cline declares - // skipSharedHooksInstall:true) — the redundant `&& !isCline` was removed. - // #2093/#2305: Kilo's former exclusion (descriptor-driven via - // skipSharedHooksInstall:true) was removed in #2305 — see above. - // #2094: Trae's exclusion is likewise descriptor-driven (trae declares - // skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed. // #2101: ZCode's exclusion is likewise descriptor-driven (zcode declares // skipSharedHooksInstall:true) — the redundant `&& !isZcode` was removed. - // #2095: Kimi's exclusion is likewise descriptor-driven (kimi declares - // skipSharedHooksInstall:true) — kimi's shared hooks/ + package.json marker - // are instead installed into its OWN native hook root (~/.kimi, resolved by - // resolveKimiHooksTomlDir) via installSharedHooksBundle, at the - // kimi-hooks-toml branch further below — never under the generic - // Agent-Skills configDir MSD installs skills/agents into for kimi. - // #2099: Copilot's exclusion is likewise descriptor-driven (copilot declares - // skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed. - // #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares - // skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed. if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) { if (!installSharedHooksBundle(targetDir)) { failures.push('hooks'); @@ -12777,7 +9321,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { // writes for the other runtimes. Without it, a ~/.codex/package.json // declaring {"type":"module"} makes Node load msd-check-update.js / // msd-context-monitor.js as ESM and their require() calls fail silently. - // Reuses the same helper the Cursor/Windsurf writers call so the marker + // Reuses the same helper the Cursor writer calls so the marker // content + user-file-preservation contract is identical everywhere. // // #2544: gated on codexStagedHooks, mirroring installSharedHooksBundle's @@ -12975,47 +9519,6 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints, rollbackInstallerMigrations, rollbackPreInstallSnapshot: restoreCodexSnapshot }; } - if (plan.installSurface === 'copilot-instructions') { - // Generate copilot-instructions.md - const templatePath = path.join(targetDir, 'msd-core', 'templates', 'copilot-instructions.md'); - const instructionsPath = path.join(targetDir, 'copilot-instructions.md'); - if (fs.existsSync(templatePath)) { - const template = fs.readFileSync(templatePath, 'utf8'); - mergeCopilotInstructions(instructionsPath, template); - console.log(` ${green}✓${reset} Generated copilot-instructions.md`); - // #786: also emit AGENTS.md, which Copilot CLI reads as primary - // instructions from the repository root. AGENTS.md is a repo-root concept - // (no documented user-scope home), so emit it only for local installs; - // global scope is already covered by ~/.copilot/copilot-instructions.md. - if (!isGlobal) { - const agentsMdPath = path.join(process.cwd(), 'AGENTS.md'); - mergeCopilotInstructions(agentsMdPath, template); - console.log(` ${green}✓${reset} Generated AGENTS.md`); - } - } - // #786: emit a self-contained Copilot lifecycle hook (sessionStart). Copilot - // command hooks run inline bash/powershell, so this needs no separate hook - // script and cannot dangle. Repo scope → .github/hooks/, user → ~/.copilot/hooks/. - // The hook is a required install artifact, so a write failure is fatal (it - // propagates) rather than silently producing a "successful" install missing - // the feature. - writeCopilotHookConfig(targetDir); - console.log(` ${green}✓${reset} Configured Copilot lifecycle hook (sessionStart)`); - persistActiveProfileMarker(); - // #4249: `[]`, not omitted — every Copilot hook is an inline `printf` - // one-liner (MSD_COPILOT_*_HOOK_BASH/PWSH in src/runtime-hooks-surface.cts), - // so this runtime genuinely launches no MSD-managed script and has no - // interpreter to resolve. Stated explicitly like every other branch rather - // than leaning on installAllRuntimes' `|| []` defence. - // #4249 (antigravity review): `rollbackInstallerMigrations` was missing here - // — every other branch returns it. This PR's own aggregate entrypoint gate - // is what makes the gap reachable: an unrelated runtime's invalid entrypoint - // now triggers rollbackFinalizedInstallerMigrations for every result in the - // batch, and a Copilot result with no rollback function silently skips - // reverting Copilot's own installer migrations. - return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: [], rollbackInstallerMigrations }; - } - if (plan.installSurface === 'cursor-hooks-json') { // ADR-1239 / #2089: Cursor hooks.json driven by the descriptor-managed hook-bus // adapter. Registers all 6 managed events (sessionStart, postToolUse, preToolUse, @@ -13040,146 +9543,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { } if (plan.installSurface === 'profile-marker-only') { - // Windsurf/Trae use artifact-only surfaces — no config.toml or settings.json - // hooks needed. Kimi is also artifact-only for its INSTALL surface (skills + - // kimi-agents, no settings.json) but #2095 Upgrade 1 gives it its own - // independent hooksSurface: kimi's native config.toml [[hooks]] array, which - // lives outside targetDir entirely (resolveKimiHooksTomlDir resolves the - // per-runtime root — ~/.kimi for kimi, ~/.kimi-code for kimi-code, #2755 — - // a sibling of targetDir's ~/.config/agents) — hence writing it here, inside - // this early-return, rather than requiring installSurface to change. - // - // GATED TO GLOBAL ONLY (belt-and-suspenders): kimi local installs already - // return early at the top of install() via hostBehaviors.localInstallDeferred, - // long before this point is ever reached — so `isGlobal` is always true here - // in practice. The explicit check documents that invariant and fails closed - // if that early-return is ever refactored away. - // - // Kimi's contract forbids hooks/ or package.json under its generic - // Agent-Skills configDir (targetDir) — capabilities/kimi/capability.json - // declares hostBehaviors.skipSharedHooksInstall:true, which excludes it from - // the shared installSharedHooksBundle(targetDir) call above. Kimi still needs - // those SAME hook scripts + the CommonJS package.json marker, but SELF- - // CONTAINED under its own native hook root instead — so install them there, - // and point buildHookCommand (via writeKimiHooksToml's second arg) at that - // same root so the generated [[hooks]] command paths reference - // ~/.kimi/hooks/