@opengsd/gsd-core 1.9.1 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +27 -3
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +12 -10
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +46 -0
- package/agents/gsd-planner.md +49 -54
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +26 -73
- package/bin/install.js +1272 -1238
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +2 -2
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/plan-phase.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1009 -115
- package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
- package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
- package/gsd-core/bin/lib/api-coverage.cjs +123 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +926 -202
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-registry.cjs +608 -148
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +507 -24
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +114 -38
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +94 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +665 -99
- package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +76 -0
- package/gsd-core/bin/lib/config.cjs +22 -2
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +217 -40
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +239 -32
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1325 -169
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +805 -264
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +160 -57
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
- package/gsd-core/bin/lib/io.cjs +38 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +821 -109
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +507 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +258 -58
- package/gsd-core/bin/lib/phase.cjs +891 -156
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +405 -84
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
- package/gsd-core/bin/lib/smart-entry.cjs +154 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +424 -105
- package/gsd-core/bin/lib/state.cjs +1927 -401
- package/gsd-core/bin/lib/surface.cjs +35 -10
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
- package/gsd-core/bin/lib/uat.cjs +706 -64
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +33 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -20
- package/gsd-core/bin/lib/verify.cjs +368 -880
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
- package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
- package/gsd-core/bin/lib/workstream.cjs +8 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +8 -7
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +33 -70
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +74 -166
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +160 -95
- package/gsd-core/workflows/debug.md +16 -17
- package/gsd-core/workflows/diagnose-issues.md +56 -8
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +103 -187
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +7 -6
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +35 -47
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +27 -240
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +89 -209
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +45 -159
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +55 -405
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +41 -13
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +41 -20
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +106 -13
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +53 -31
- package/gsd-core/workflows/ui-phase.md +13 -12
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +19 -8
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +17 -65
- package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
- package/hooks/dist/gsd-check-update-worker.js +64 -12
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +45 -24
- package/hooks/dist/gsd-statusline.js +90 -6
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +277 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +517 -0
- package/hooks/gsd-check-update-worker.js +64 -12
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +607 -26
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +45 -24
- package/hooks/gsd-statusline.js +90 -6
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +277 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +31 -10
- package/pi/gsd.cjs +71 -12
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +68 -6
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +439 -0
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +150 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +434 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +40 -3
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +52 -6
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +2 -2
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +2 -2
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +1 -2
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -577
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/gen-emitted-baseline.cjs +0 -145
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1050
package/bin/install.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
projectPathActionProjection,
|
|
15
15
|
projectPortableHookBaseDir,
|
|
16
16
|
projectPersistentPathExportActions,
|
|
17
|
+
PATH_ACTION_REASON,
|
|
17
18
|
projectShellCommandText,
|
|
18
19
|
projectCodexHookTomlCommand,
|
|
19
20
|
shellHookOmitsBashRunner,
|
|
@@ -33,11 +34,20 @@ const {
|
|
|
33
34
|
getGlobalConfigDir,
|
|
34
35
|
getGlobalSkillsBase,
|
|
35
36
|
resolveKimiHooksTomlDir,
|
|
37
|
+
isRegisteredRuntimeId,
|
|
36
38
|
} = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
|
39
|
+
// #2870: the Install Scope Module — turns a bare 'global' | 'local' scope id
|
|
40
|
+
// plus a runtime into one resolved value (configHome, settingsFile,
|
|
41
|
+
// consentRequired, hostPrecedenceRank) instead of the id being re-derived
|
|
42
|
+
// and re-interpreted at each call site. See src/install-scope.cts.
|
|
43
|
+
const { resolveScope } = require('../gsd-core/bin/lib/install-scope.cjs');
|
|
37
44
|
// getDirName (runtime -> local config dir name) is relocated out of this
|
|
38
45
|
// installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the
|
|
39
46
|
// conversion module's rewrite engine can consume it without importing
|
|
40
|
-
// bin/install.js.
|
|
47
|
+
// bin/install.js. Imported here for install.js's own internal call sites
|
|
48
|
+
// (getConfigDirFromHome and the runtime-content-rewrite loops below) — #2876
|
|
49
|
+
// retired the re-export; tests now import getDirName directly from
|
|
50
|
+
// gsd-core/bin/lib/runtime-name-policy.cjs.
|
|
41
51
|
const { getDirName, getRuntimeLabel, getGlobalConfigHomeFragment, runtimeFlags, getRuntimeNewProjectCommand } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
|
42
52
|
const {
|
|
43
53
|
applyWorktreeBaseRef,
|
|
@@ -45,7 +55,23 @@ const {
|
|
|
45
55
|
} = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
|
|
46
56
|
const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
|
|
47
57
|
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
|
|
58
|
+
// #2930 (epic #1671 Phase 3): strips `<!-- gsd:section -->` markers from
|
|
59
|
+
// workflow .md content at emit time, before any per-runtime rewrite runs.
|
|
60
|
+
const { composeWorkflow } = require('../gsd-core/bin/lib/workflow-fragments.cjs');
|
|
61
|
+
// #3072: THE shared composition-scope predicate (also consumed by the served
|
|
62
|
+
// MCP catalog, src/mcp-catalog.cts) — see the comment at its call site below.
|
|
63
|
+
const { shouldCompose } = require('../gsd-core/bin/lib/mcp-catalog.cjs');
|
|
48
64
|
const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
|
|
65
|
+
const { escapeRegex: escapeRegExp } = require('../gsd-core/bin/lib/pattern.cjs');
|
|
66
|
+
// #2873: cross-scope shadow detection — reports (never fails) when a
|
|
67
|
+
// GSD-owned scope shadows another on this machine (design doc:
|
|
68
|
+
// .gsd/phase/feat-2873-cross-scope-shadowing/40-design.md).
|
|
69
|
+
const { buildShadowReport, renderShadowReport } = require('../gsd-core/bin/lib/install-shadow-report.cjs');
|
|
70
|
+
// #2544: the CommonJS marker's single source of truth. classifyMarker() backs
|
|
71
|
+
// BOTH ensureCommonJsMarker() (install) and removeCommonJsMarker() (uninstall),
|
|
72
|
+
// so the write side can no longer clobber a package.json the remove side would
|
|
73
|
+
// correctly refuse to delete.
|
|
74
|
+
const { ensureCommonJsMarker, removeCommonJsMarker } = require('../gsd-core/bin/lib/commonjs-marker.cjs');
|
|
49
75
|
// Canonical set of hook files shipped to users. Imported here so writeManifest()
|
|
50
76
|
// records exactly the same set that build-hooks.js copies to hooks/dist/, making
|
|
51
77
|
// the manifest and the installed hooks/ dir structurally identical. Avoids the
|
|
@@ -53,9 +79,14 @@ const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-
|
|
|
53
79
|
const { HOOKS_TO_COPY: _HOOKS_TO_COPY } = require('../scripts/build-hooks.js');
|
|
54
80
|
const INSTALLED_HOOK_FILES = new Set(_HOOKS_TO_COPY);
|
|
55
81
|
|
|
56
|
-
// ADR-857 phase 5f-1: hook-surface writer functions extracted to a dedicated
|
|
57
|
-
//
|
|
58
|
-
// (require('../bin/install.js').writeCursorHooksJson etc.)
|
|
82
|
+
// ADR-857 phase 5f-1: hook-surface writer functions extracted to a dedicated
|
|
83
|
+
// module. install.js used to re-export the whole hooksSurface surface so
|
|
84
|
+
// existing callers (require('../bin/install.js').writeCursorHooksJson etc.)
|
|
85
|
+
// kept working — #2876 found zero production/test consumers of any of those
|
|
86
|
+
// re-exports (tests import runtime-hooks-surface.cjs directly) and retired
|
|
87
|
+
// them from module.exports. install.js still requires hooksSurface below for
|
|
88
|
+
// its own internal call sites (writeCursorHooksJson, writeClineArtifacts,
|
|
89
|
+
// resolveNodeRunner, applySettingsJsonHooks, etc.).
|
|
59
90
|
const hooksSurface = require('../gsd-core/bin/lib/runtime-hooks-surface.cjs');
|
|
60
91
|
|
|
61
92
|
/**
|
|
@@ -283,13 +314,26 @@ const GSD_COPILOT_SESSION_HOOK_PWSH =
|
|
|
283
314
|
// subagentStart → gsd-cursor-subagent-start.js (subagent context injection)
|
|
284
315
|
// subagentStop → gsd-cursor-subagent-stop.js (subagent completion reminder)
|
|
285
316
|
// Cursor docs: https://cursor.com/docs/hooks
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
//
|
|
317
|
+
//
|
|
318
|
+
// These script-name/marker constants used to be independently re-declared
|
|
319
|
+
// here with their own string literals — a second, unlinked copy of exactly
|
|
320
|
+
// the values runtime-hooks-surface.cts also defines for its own internal use
|
|
321
|
+
// (buildCursorHookEntry, writeCursorHooksJson, etc.). #2876's code review
|
|
322
|
+
// found tests reading the constant from install.js's copy while calling
|
|
323
|
+
// functions built from hooksSurface's copy, with nothing guarding the two
|
|
324
|
+
// staying equal — the same unlinked-duplicate-value hazard the ADR-1508
|
|
325
|
+
// dedup elsewhere in this file exists to prevent. Fixed at the root: these
|
|
326
|
+
// are now bare references to hooksSurface's own exports, so there is exactly
|
|
327
|
+
// one literal definition of each value, full stop.
|
|
328
|
+
const GSD_CURSOR_SESSION_HOOK_SCRIPT = hooksSurface.GSD_CURSOR_SESSION_HOOK_SCRIPT;
|
|
329
|
+
const GSD_CURSOR_POST_TOOL_HOOK_SCRIPT = hooksSurface.GSD_CURSOR_POST_TOOL_HOOK_SCRIPT;
|
|
330
|
+
const GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT = hooksSurface.GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT;
|
|
331
|
+
const GSD_CURSOR_STOP_HOOK_SCRIPT = hooksSurface.GSD_CURSOR_STOP_HOOK_SCRIPT;
|
|
332
|
+
const GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT = hooksSurface.GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT;
|
|
333
|
+
const GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT = hooksSurface.GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT;
|
|
334
|
+
// All GSD-managed Cursor hook scripts (used by uninstall cleanup). Not
|
|
335
|
+
// independently defined in hooksSurface — built here from the bare
|
|
336
|
+
// references above, so it can never drift from them either.
|
|
293
337
|
const GSD_CURSOR_HOOK_SCRIPTS = [
|
|
294
338
|
GSD_CURSOR_SESSION_HOOK_SCRIPT,
|
|
295
339
|
GSD_CURSOR_POST_TOOL_HOOK_SCRIPT,
|
|
@@ -299,7 +343,7 @@ const GSD_CURSOR_HOOK_SCRIPTS = [
|
|
|
299
343
|
GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT,
|
|
300
344
|
];
|
|
301
345
|
// Marker comment embedded in managed hook entries so GSD can find+remove them.
|
|
302
|
-
const GSD_CURSOR_HOOK_MARKER =
|
|
346
|
+
const GSD_CURSOR_HOOK_MARKER = hooksSurface.GSD_CURSOR_HOOK_MARKER;
|
|
303
347
|
|
|
304
348
|
// #2100 Stage 2 — Windsurf/Cascade lifecycle hook constants.
|
|
305
349
|
// Windsurf/Cascade reads hook configs from <project-root>/.windsurf/hooks.json
|
|
@@ -315,15 +359,17 @@ const GSD_CURSOR_HOOK_MARKER = 'gsd-managed';
|
|
|
315
359
|
// have no Windsurf counterpart and are deliberately NOT ported.
|
|
316
360
|
// Cascade hooks docs (reference): https://docs.windsurf.com/llms-full.txt ,
|
|
317
361
|
// https://docs.devin.ai/desktop/cascade/hooks
|
|
318
|
-
|
|
319
|
-
|
|
362
|
+
//
|
|
363
|
+
// Same #2876 fix as the Cursor block above: bare references to hooksSurface's
|
|
364
|
+
// own exports instead of a second, unlinked literal copy.
|
|
365
|
+
const GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT = hooksSurface.GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT;
|
|
366
|
+
const GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT = hooksSurface.GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT;
|
|
320
367
|
// All GSD-managed Windsurf hook scripts (used by uninstall cleanup).
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
];
|
|
368
|
+
// hooksSurface independently defines the same array — bare reference here so
|
|
369
|
+
// the two can never drift.
|
|
370
|
+
const GSD_WINDSURF_HOOK_SCRIPTS = hooksSurface.GSD_WINDSURF_HOOK_SCRIPTS;
|
|
325
371
|
|
|
326
|
-
// GSD-managed files under hooks/lib/ (helpers required by gsd-*.
|
|
372
|
+
// GSD-managed files under hooks/lib/ (helpers required by gsd-*.js hooks).
|
|
327
373
|
// git-cmd.js does not start with "gsd-" (shared classifier for #3129), gsd-graphify-rebuild.sh does.
|
|
328
374
|
// cursor-workspace.js (#2587) is required by the Cursor lifecycle hooks. Those
|
|
329
375
|
// are staged individually by writeCursorHooksJson (Cursor sets
|
|
@@ -331,7 +377,87 @@ const GSD_WINDSURF_HOOK_SCRIPTS = [
|
|
|
331
377
|
// copy below) — that function stages this helper alongside them. Listing it
|
|
332
378
|
// here keeps uninstall and the manifest managing it for every OTHER runtime
|
|
333
379
|
// that does receive hooks/lib.
|
|
334
|
-
|
|
380
|
+
// injection-patterns.js (#3504) is required by gsd-prompt-guard.js and
|
|
381
|
+
// gsd-read-injection-scanner.js — the shared prompt-injection pattern list the
|
|
382
|
+
// two guards require so their copies cannot drift.
|
|
383
|
+
const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh', 'cursor-workspace.js', 'injection-patterns.js'];
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Directory name GSD stages its shared hook bundle under, inside a runtime's
|
|
387
|
+
* install root. Defaults to 'hooks' — the name every runtime used before #3023.
|
|
388
|
+
*
|
|
389
|
+
* pi (pi.dev) reserves `hooks/` as its own now-deprecated extension location and
|
|
390
|
+
* prints a migration warning on every startup when one exists, so pi overrides
|
|
391
|
+
* this via hostBehaviors.sharedHooksDirName. Following pi's advised remediation
|
|
392
|
+
* (move it into extensions/) would break the adapter's path resolution AND expose
|
|
393
|
+
* GSD's .js helpers to pi's extension auto-discovery, so the bundle is renamed in
|
|
394
|
+
* place instead — same depth, so every `__dirname/..`-relative resolution inside
|
|
395
|
+
* the bundle (e.g. hooks/gsd-context-monitor.js reaching ../gsd-core/bin/) keeps
|
|
396
|
+
* working.
|
|
397
|
+
*/
|
|
398
|
+
const SHARED_HOOKS_DIR_DEFAULT = 'hooks';
|
|
399
|
+
|
|
400
|
+
// #3184 — GSD-managed file enumerations for scripts/changeset/ and scripts/lib/
|
|
401
|
+
// uninstall. The install-side copy of both directories is wholesale ("copy every
|
|
402
|
+
// file present"), so these enumerations MUST be kept in parity with the real
|
|
403
|
+
// directory contents or an added file ships on install and then orphans on
|
|
404
|
+
// uninstall (survives removal, keeps the dir non-empty, blocks its rmdir).
|
|
405
|
+
// Hoisted to module scope (and exported below) so tests/install.test.cjs can
|
|
406
|
+
// assert parity against fs.readdirSync(scripts/lib) / fs.readdirSync(scripts/changeset)
|
|
407
|
+
// without source-grepping this file.
|
|
408
|
+
const GSD_CHANGESET_FILES = [
|
|
409
|
+
'cli.cjs', 'parse.cjs', 'render.cjs', 'serialize.cjs',
|
|
410
|
+
'github-release-notes.cjs', 'lint.cjs', 'new.cjs',
|
|
411
|
+
'README.md', // documentation only — not user-authored
|
|
412
|
+
];
|
|
413
|
+
const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-scan.cjs', 'alias-drift-families.cjs'];
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Resolve a runtime's shared-hooks directory name from its descriptor.
|
|
417
|
+
*
|
|
418
|
+
* The value is a single path SEGMENT. This string is joined onto a user's config
|
|
419
|
+
* root and then written to and recursively read, so anything that is not a plain,
|
|
420
|
+
* non-empty, separator-free, non-dot segment is rejected back to the default —
|
|
421
|
+
* a descriptor typo must never let the installer write outside the install root.
|
|
422
|
+
*
|
|
423
|
+
* The "non-dot" part of that contract is enforced beyond the literal '.' / '..'
|
|
424
|
+
* segments: an all-dot (or dot-and-whitespace-only) segment is rejected as a
|
|
425
|
+
* meaningless name, a segment with a trailing dot or space is rejected because
|
|
426
|
+
* Windows silently strips it at directory-creation time (which would split the
|
|
427
|
+
* name the installer creates from the name callers probe for), and a Windows
|
|
428
|
+
* reserved device name (CON, PRN, AUX, NUL, COM1-9, LPT1-9, with or without an
|
|
429
|
+
* extension) is rejected because it cannot exist as a directory on Windows at
|
|
430
|
+
* all. These checks are unconditional on every platform: the descriptor is
|
|
431
|
+
* authored once and shipped everywhere, so a value invalid on Windows must be
|
|
432
|
+
* rejected identically on Linux/macOS, or the install and its fixtures disagree
|
|
433
|
+
* cross-platform.
|
|
434
|
+
*
|
|
435
|
+
* @param {string} runtime
|
|
436
|
+
* @returns {string}
|
|
437
|
+
*/
|
|
438
|
+
function resolveSharedHooksDirName(runtime) {
|
|
439
|
+
const raw = _hostBehaviors(runtime).sharedHooksDirName;
|
|
440
|
+
if (typeof raw !== 'string') return SHARED_HOOKS_DIR_DEFAULT;
|
|
441
|
+
const name = raw.trim();
|
|
442
|
+
if (name === '') return SHARED_HOOKS_DIR_DEFAULT;
|
|
443
|
+
if (name === '.' || name === '..') return SHARED_HOOKS_DIR_DEFAULT;
|
|
444
|
+
// All-dot or dot+whitespace segments ('...', '. .') are not meaningful
|
|
445
|
+
// directory names and are almost certainly a descriptor typo.
|
|
446
|
+
if (name.replace(/[.\s]/g, '') === '') return SHARED_HOOKS_DIR_DEFAULT;
|
|
447
|
+
// Windows silently strips a trailing dot or space at creation time, so the
|
|
448
|
+
// directory the installer creates would not match the name the adapter
|
|
449
|
+
// probes for — a split-brain that only reproduces off-Linux.
|
|
450
|
+
if (/[. ]$/.test(name)) return SHARED_HOOKS_DIR_DEFAULT;
|
|
451
|
+
// Windows reserved device names cannot exist as directories.
|
|
452
|
+
if (/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?$/i.test(name)) return SHARED_HOOKS_DIR_DEFAULT;
|
|
453
|
+
if (name.includes('/') || name.includes('\\')) return SHARED_HOOKS_DIR_DEFAULT;
|
|
454
|
+
// Belt-and-braces: reject anything path.basename() would reduce, and any
|
|
455
|
+
// Windows drive/UNC-flavoured value.
|
|
456
|
+
if (path.basename(name) !== name) return SHARED_HOOKS_DIR_DEFAULT;
|
|
457
|
+
if (path.isAbsolute(name)) return SHARED_HOOKS_DIR_DEFAULT;
|
|
458
|
+
if (name.includes('\0')) return SHARED_HOOKS_DIR_DEFAULT;
|
|
459
|
+
return name;
|
|
460
|
+
}
|
|
335
461
|
|
|
336
462
|
const CODEX_AGENT_SANDBOX = {
|
|
337
463
|
'gsd-executor': 'workspace-write',
|
|
@@ -375,14 +501,13 @@ const pkg = require('../package.json');
|
|
|
375
501
|
// of cwd, but keeping the require at the top makes the dependency explicit and
|
|
376
502
|
// surfaces resolution failures at process start instead of at first install call.
|
|
377
503
|
const _gsdLibDir = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib');
|
|
378
|
-
const { MODEL_PROFILES: GSD_MODEL_PROFILES } = require(path.join(_gsdLibDir, 'model-profiles.cjs'));
|
|
379
504
|
const {
|
|
380
505
|
RUNTIME_PROFILE_MAP: GSD_RUNTIME_PROFILE_MAP,
|
|
506
|
+
isAnthropicFlavoredModel: gsdIsAnthropicFlavoredModel,
|
|
381
507
|
} = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
} = require(path.join(_gsdLibDir, 'model-resolver.cjs'));
|
|
508
|
+
// #2875 Part 2: MODEL_PROFILES + resolveTierEntry are now consumed only by
|
|
509
|
+
// install-model-override-resolver.cjs's readGsdRuntimeProfileResolver
|
|
510
|
+
// (required below) — this installer no longer needs its own bindings.
|
|
386
511
|
|
|
387
512
|
// #2071 — install-time effort resolution (readGsdEffectiveEffortConfig /
|
|
388
513
|
// resolveInstallTimeEffort, plus their _getGsdEffortCatalog + _readGsdConfigFile
|
|
@@ -455,6 +580,17 @@ try {
|
|
|
455
580
|
// hardcoded string-equality branch) so behavior degrades CLOSED (safe), never open.
|
|
456
581
|
// The live descriptor (capabilities/claude/capability.json) remains the source of
|
|
457
582
|
// truth; this mirrors only the privacy-load-bearing subset. (ADR-1239 / #2086)
|
|
583
|
+
//
|
|
584
|
+
// #2870: NOT routed through the Install Scope Module (resolveScope,
|
|
585
|
+
// src/install-scope.cts) despite that module owning per-scope settings-file
|
|
586
|
+
// resolution elsewhere in this file. resolveScope's own descriptor lookup
|
|
587
|
+
// goes through the SAME capability registry require this floor exists to
|
|
588
|
+
// survive the failure of (see getRegistry() in install-scope.cts) — so on
|
|
589
|
+
// exactly the "registry failed to load" path this constant is for,
|
|
590
|
+
// resolveScope would throw too. Routing through it here would trade a
|
|
591
|
+
// graceful, documented degrade for a crash in the one case this floor was
|
|
592
|
+
// added to prevent. This hardcoded literal is the correct, honest answer,
|
|
593
|
+
// not an un-migrated leftover.
|
|
458
594
|
const FALLBACK_HOST_BEHAVIORS = Object.freeze({
|
|
459
595
|
claude: Object.freeze({
|
|
460
596
|
settingsFileByScope: Object.freeze({ local: 'settings.local.json', global: 'settings.json' }),
|
|
@@ -516,6 +652,22 @@ function _hostIntegrationDispatch(runtime) {
|
|
|
516
652
|
return dispatch || {};
|
|
517
653
|
}
|
|
518
654
|
|
|
655
|
+
/**
|
|
656
|
+
* #2870: shared install()/uninstall() scope resolution. Routes `id` through
|
|
657
|
+
* the Install Scope Module (src/install-scope.cts) and degrades to `null` on
|
|
658
|
+
* failure (unknown/non-installable runtime, broken registry bundle) instead
|
|
659
|
+
* of throwing, so each call site's own plain-id fallback keeps working
|
|
660
|
+
* exactly as it did before this migration. Both call sites previously carried
|
|
661
|
+
* their own copy of this try/catch; this is the one shared copy.
|
|
662
|
+
*/
|
|
663
|
+
function _resolveScopeSafe(id, runtime) {
|
|
664
|
+
try {
|
|
665
|
+
return resolveScope({ id, runtime });
|
|
666
|
+
} catch (_) {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
519
671
|
/**
|
|
520
672
|
* Resolve the ACTUAL on-disk skills-install directory for a runtime, honoring a
|
|
521
673
|
* skills-kind `home` override (ADR-1239 upgrade 3 / #2088: e.g. Codex skills ->
|
|
@@ -550,6 +702,7 @@ function _runtimeAdapter(runtime) {
|
|
|
550
702
|
const {
|
|
551
703
|
applyInstallerMigrationPlan,
|
|
552
704
|
discoverInstallerMigrations,
|
|
705
|
+
MANIFEST_SCHEMA_VERSION,
|
|
553
706
|
runInstallerMigrations,
|
|
554
707
|
} = require(path.join(_gsdLibDir, 'installer-migrations.cjs'));
|
|
555
708
|
const {
|
|
@@ -578,29 +731,78 @@ const {
|
|
|
578
731
|
// getCommitAttribution STAYS here (impure install-time config I/O); it is injected
|
|
579
732
|
// into the engine functions via the resolveAttribution parameter at each call site.
|
|
580
733
|
const installEngine = require(path.join(_gsdLibDir, 'install-engine.cjs'));
|
|
734
|
+
// #2876: _copyStaged, convertClaudeCommandToOpencodeSkill, and
|
|
735
|
+
// convertClaudeCommandToKiloSkill used to be destructured here too — all
|
|
736
|
+
// three had no install.js internal caller and no export consumer (tests
|
|
737
|
+
// import all three directly from gsd-core/bin/lib/install-engine.cjs), so
|
|
738
|
+
// the retired bindings were dead code. applyOpencodeFamilyPathPrefix,
|
|
739
|
+
// _runLegacyInstallMigrations, _runLegacyUninstallCleanup,
|
|
740
|
+
// _removeGsdEntries, _restoreDir, and _removeHermesBareStemDirs were the
|
|
741
|
+
// same — unused local bindings that were never part of this module's export
|
|
742
|
+
// surface either — found and retired in the same sweep.
|
|
581
743
|
const {
|
|
582
744
|
installRuntimeArtifacts,
|
|
583
745
|
uninstallRuntimeArtifacts,
|
|
584
746
|
installOpencodeFamilySkills,
|
|
747
|
+
installAgentsKindStandalone,
|
|
585
748
|
_installNativePluginIfDeclared,
|
|
586
|
-
_copyStaged,
|
|
587
749
|
hasExistingSymlinkBetween,
|
|
588
750
|
isSymlinkedDestOptIn,
|
|
589
|
-
preserveUserArtifacts,
|
|
590
|
-
restoreUserArtifacts,
|
|
591
751
|
migrateLegacyDevPreferencesToSkill,
|
|
592
|
-
applyOpencodeFamilyPathPrefix,
|
|
593
|
-
convertClaudeCommandToOpencodeSkill,
|
|
594
|
-
convertClaudeCommandToKiloSkill,
|
|
595
752
|
USER_OWNED_ARTIFACTS,
|
|
596
|
-
_runLegacyInstallMigrations,
|
|
597
|
-
_runLegacyUninstallCleanup,
|
|
598
|
-
_removeGsdEntries,
|
|
599
753
|
_snapshotDir,
|
|
600
|
-
_restoreDir,
|
|
601
|
-
_removeHermesBareStemDirs,
|
|
602
754
|
} = installEngine;
|
|
603
755
|
|
|
756
|
+
// #2875 (epic #2866 Phase 6): durable on-disk staging for USER_OWNED_ARTIFACTS
|
|
757
|
+
// across the preserve -> wipe -> restore window (#1874-F19). See
|
|
758
|
+
// src/user-artifact-staging.cts's module doc.
|
|
759
|
+
const {
|
|
760
|
+
stageUserArtifacts,
|
|
761
|
+
restoreStagedUserArtifacts,
|
|
762
|
+
discardStagedUserArtifacts,
|
|
763
|
+
recoverOrphanedUserArtifacts,
|
|
764
|
+
} = require(path.join(_gsdLibDir, 'user-artifact-staging.cjs'));
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Resolve the durable staging root for `configDir`, confined via
|
|
768
|
+
* `assertDestWithinConfigHome` and refused via `hasExistingSymlinkBetween`
|
|
769
|
+
* (test-matrix E1/E4) — mirrors install-engine.cts's
|
|
770
|
+
* `_resolveUserArtifactStagingRoot`, kept local here because bin/install.js's
|
|
771
|
+
* two call sites (uninstall's legacy-migration block, install's mainline
|
|
772
|
+
* gsd-core copy) are not inside that module.
|
|
773
|
+
*/
|
|
774
|
+
function _resolveUserArtifactStagingRoot(configDir) {
|
|
775
|
+
const stagingRoot = assertDestWithinConfigHome(configDir, path.posix.join('.gsd-staging', 'user-artifacts'));
|
|
776
|
+
if (hasExistingSymlinkBetween(path.resolve(configDir), stagingRoot, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
777
|
+
throw new Error(
|
|
778
|
+
`_resolveUserArtifactStagingRoot: staging root "${stagingRoot}" contains a symlink the install root "${configDir}" does not trust — refusing to stage. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
return stagingRoot;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Degrade-not-abort wrapper over `_resolveUserArtifactStagingRoot` — mirrors
|
|
786
|
+
* install-engine.cts's own `_tryResolveUserArtifactStagingRoot` (kept local
|
|
787
|
+
* here for the same reason the throwing version above is: bin/install.js's
|
|
788
|
+
* own call sites are not inside that module). A hostile/broken
|
|
789
|
+
* `.gsd-staging` path (or a symlinked configDir itself) must never brick
|
|
790
|
+
* `install()` or `uninstall()` — before this fix, `_resolveUserArtifactStagingRoot`
|
|
791
|
+
* was called UNGUARDED as the first statement of both, so
|
|
792
|
+
* `ln -s /nonexistent ~/.claude/.gsd-staging` killed both commands, including
|
|
793
|
+
* uninstall, the remedy for the first problem. Returns `null` (never throws),
|
|
794
|
+
* logging one warning; every call site MUST treat `null` as "skip the
|
|
795
|
+
* staging-dependent step for this run".
|
|
796
|
+
*/
|
|
797
|
+
function _tryResolveUserArtifactStagingRoot(configDir) {
|
|
798
|
+
try {
|
|
799
|
+
return _resolveUserArtifactStagingRoot(configDir);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
console.warn(` ${yellow}!${reset} user-artifact staging unavailable for "${configDir}" (${err.message}) — proceeding without durable staging for this step.`);
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
604
806
|
// Parse args
|
|
605
807
|
const args = process.argv.slice(2);
|
|
606
808
|
const hasGlobal = args.includes('--global') || args.includes('-g');
|
|
@@ -786,7 +988,8 @@ Then re-run: npx ${pkg.name}@latest
|
|
|
786
988
|
}
|
|
787
989
|
|
|
788
990
|
// getDirName (runtime -> local config dir name) now lives in
|
|
789
|
-
// runtime-name-policy.cjs (ADR-1508 / #1510 Phase 1); imported
|
|
991
|
+
// runtime-name-policy.cjs (ADR-1508 / #1510 Phase 1); imported above for
|
|
992
|
+
// install.js's own internal use only — #2876 retired the re-export.
|
|
790
993
|
|
|
791
994
|
/**
|
|
792
995
|
* Get the config directory path relative to home directory for a runtime
|
|
@@ -902,22 +1105,25 @@ if (hasUninstall) {
|
|
|
902
1105
|
|
|
903
1106
|
// Show help if requested
|
|
904
1107
|
if (hasHelp) {
|
|
905
|
-
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}--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}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\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 (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=<name>${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 \`gsd 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 CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\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}# Uninstall GSD 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, GSD creates ${cyan}~/.config/agents${reset}.\n Use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`);
|
|
1108
|
+
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}--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 GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\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 (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=<name>${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 \`gsd 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 CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\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}# Uninstall GSD 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, GSD creates ${cyan}~/.config/agents${reset}.\n Use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`);
|
|
906
1109
|
process.exit(0);
|
|
907
1110
|
}
|
|
908
1111
|
|
|
909
1112
|
// computePathPrefix: implementation moved to runtimeArtifactConversion._computePathPrefix
|
|
910
|
-
// (ADR-1508 / #1511 Phase 2 — single owner). The const binding above
|
|
911
|
-
//
|
|
1113
|
+
// (ADR-1508 / #1511 Phase 2 — single owner). The const binding above re-binds
|
|
1114
|
+
// it here for install.js's own internal call sites only — #2876 retired the
|
|
1115
|
+
// module.exports entry (zero consumers found; tests import
|
|
1116
|
+
// runtimeArtifactConversion._computePathPrefix directly).
|
|
912
1117
|
// Original doc: Compute the path prefix used for `@file` references in installed
|
|
913
1118
|
// command/skill markdown. For global installs under $HOME uses $HOME/... form;
|
|
914
1119
|
// OpenCode always uses the absolute path (#2376 Windows, #2831 macOS/Linux).
|
|
915
1120
|
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
|
|
1121
|
+
// resolveNodeRunner, resolveBashRunner, referencesHook are now owned by the
|
|
1122
|
+
// runtime-hooks-surface module. Import them here so install.js callers
|
|
1123
|
+
// continue to work and so there is a single implementation of these helpers.
|
|
1124
|
+
// (normalizeNodePath was re-bound here too until #2876 found bin/install.js
|
|
1125
|
+
// had no internal caller and no export consumer for it — hooksSurface owns
|
|
1126
|
+
// the single implementation now, used internally by resolveNodeRunner there.)
|
|
921
1127
|
const resolveNodeRunner = hooksSurface.resolveNodeRunner;
|
|
922
1128
|
const resolveBashRunner = hooksSurface.resolveBashRunner;
|
|
923
1129
|
// referencesHook: pure predicate over hook entry objects, shared between
|
|
@@ -935,138 +1141,50 @@ const removeKimiHooksToml = hooksSurface.removeKimiHooksToml;
|
|
|
935
1141
|
// callers continue to work and there is a single implementation. (All call
|
|
936
1142
|
// sites are below this line, so the const binding has no TDZ hazard.)
|
|
937
1143
|
const processAttribution = runtimeArtifactConversion.processAttribution;
|
|
938
|
-
// computePathPrefix
|
|
939
|
-
//
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
1144
|
+
// computePathPrefix: implementation lives in runtimeArtifactConversion
|
|
1145
|
+
// (ADR-1508 / #1511 Phase 2 — single owner). Re-bound here so install.js call
|
|
1146
|
+
// sites continue to work. #2876 retired the sibling
|
|
1147
|
+
// applyRuntimeContentRewritesInPlace / applyRuntimeContentRewritesForCommandsInPlace
|
|
1148
|
+
// re-bindings that used to sit alongside it, and the entire #1675 Augment
|
|
1149
|
+
// converter family re-binding (convertClaudeToAugmentMarkdown /
|
|
1150
|
+
// convertClaudeCommandToAugmentSkill / convertClaudeAgentToAugmentAgent) that
|
|
1151
|
+
// used to follow — bin/install.js had no internal caller for any of them (the
|
|
1152
|
+
// descriptor pipeline in runtimeArtifactConversion calls them directly).
|
|
1153
|
+
// (All call sites are below this line → no TDZ hazard.)
|
|
943
1154
|
const computePathPrefix = runtimeArtifactConversion._computePathPrefix;
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
//
|
|
1155
|
+
// #2931 (ADR-1508): the windsurf converter family is single-sourced in the
|
|
1156
|
+
// conversion module. install.js re-binds (does not re-define) the one member
|
|
1157
|
+
// it still calls internally so there is exactly one body — the
|
|
1158
|
+
// generative-drift hazard the dedup removes. #2876 retired the sibling
|
|
1159
|
+
// convertClaudeCommandToWindsurfSkill / convertClaudeCommandToWindsurfWorkflow /
|
|
1160
|
+
// convertClaudeAgentToWindsurfAgent re-bindings — bin/install.js had no
|
|
1161
|
+
// internal caller for any of them (the descriptor pipeline in
|
|
1162
|
+
// runtimeArtifactConversion calls them directly).
|
|
951
1163
|
// (All call sites are below this line → no TDZ hazard.)
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
1164
|
+
const convertClaudeToWindsurfMarkdown = runtimeArtifactConversion.convertClaudeToWindsurfMarkdown;
|
|
1165
|
+
// #2931 (ADR-1508): single-sourced in the conversion module — was a second,
|
|
1166
|
+
// unlinked verbatim copy here (used by the local Cursor/Trae/CodeBuddy/Cline
|
|
1167
|
+
// converters below), the exact drift class this PR exists to reduce. Verified
|
|
1168
|
+
// behaviorally identical (no block / one block / adjacent blocks / whole-
|
|
1169
|
+
// content block / unclosed opening tag / nested-looking tags / repeated
|
|
1170
|
+
// sequential calls for global-regex lastIndex leakage) before merging.
|
|
1171
|
+
// install.js re-binds (does not re-define) — RUNTIME_COMPATIBILITY_BLOCK_RE
|
|
1172
|
+
// is no longer duplicated here either. (All call sites are below this line
|
|
1173
|
+
// → no TDZ hazard.)
|
|
1174
|
+
const applyClaudeCodeBrandSwap = runtimeArtifactConversion.applyClaudeCodeBrandSwap;
|
|
955
1175
|
|
|
956
1176
|
function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
|
|
957
1177
|
return hooksSurface.rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts);
|
|
958
1178
|
}
|
|
959
1179
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
* Returns null when `absoluteRunner` is null so callers can warn-and-skip
|
|
969
|
-
* registration — emitting a broken bare-node hook is strictly worse than
|
|
970
|
-
* not registering one (the user can re-run install once node is on PATH).
|
|
971
|
-
*
|
|
972
|
-
* @param {string} targetDir - Resolved absolute Codex config dir (e.g. ~/.codex).
|
|
973
|
-
* @param {{ absoluteRunner: string|null, eol?: string }} opts
|
|
974
|
-
* absoluteRunner: result of resolveNodeRunner() — a JSON-stringified
|
|
975
|
-
* absolute node path with forward slashes (e.g. `"/usr/local/bin/node"`),
|
|
976
|
-
* or null when process.execPath was unavailable.
|
|
977
|
-
* eol: line ending to emit ('\n' or '\r\n') — caller passes
|
|
978
|
-
* detectLineEnding(configContent) so existing CRLF files stay CRLF.
|
|
979
|
-
* Defaults to '\n'.
|
|
980
|
-
* @returns {string|null} The toml block to append, or null on missing runner.
|
|
981
|
-
*/
|
|
982
|
-
function buildCodexHookBlock(targetDir, opts) {
|
|
983
|
-
return hooksSurface.buildCodexHookBlock(targetDir, opts);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/**
|
|
987
|
-
* Rewrite legacy bare-`node` managed-hook command lines in a Codex
|
|
988
|
-
* config.toml string to use the absolute Node runner. Mirror of
|
|
989
|
-
* rewriteLegacyManagedNodeHookCommands but for the toml surface (#3017).
|
|
990
|
-
*
|
|
991
|
-
* Only rewrites entries whose script basename matches CODEX_MANAGED_HOOK_BASENAMES
|
|
992
|
-
* (basename equality, not substring containment) — user-authored bare-node
|
|
993
|
-
* hooks pointing at scripts outside the managed allowlist are left alone.
|
|
994
|
-
*
|
|
995
|
-
* @param {string} content - Current config.toml contents.
|
|
996
|
-
* @param {string|null} absoluteRunner - Result of resolveNodeRunner().
|
|
997
|
-
* @returns {{ content: string, changed: boolean }}
|
|
998
|
-
*/
|
|
999
|
-
function rewriteLegacyCodexHookBlock(content, absoluteRunner, opts) {
|
|
1000
|
-
return hooksSurface.rewriteLegacyCodexHookBlock(content, absoluteRunner, opts);
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
/**
|
|
1004
|
-
* Generic reconcile helper: ensure hooks.json contains exactly one managed GSD
|
|
1005
|
-
* hook entry for `eventName`, while preserving all user-owned entries.
|
|
1006
|
-
*
|
|
1007
|
-
* Supports both known hooks.json shapes:
|
|
1008
|
-
* 1) { "<EventName>": [...] }
|
|
1009
|
-
* 2) { "hooks": { "<EventName>": [...] } }
|
|
1010
|
-
*
|
|
1011
|
-
* @param {string} targetDir - Codex config dir (e.g. ~/.codex or <project>/.codex).
|
|
1012
|
-
* @param {string} eventName - Codex hook event name (e.g. 'SessionStart', 'Stop').
|
|
1013
|
-
* @param {{ managedCommand?: string|null, commandWindows?: string|null, matcher?: string|null, timeout?: number|null }} opts
|
|
1014
|
-
* managedCommand: POSIX hook command string to register, or null to remove.
|
|
1015
|
-
* commandWindows: Windows .cmd shim path to emit as `commandWindows` field
|
|
1016
|
-
* (#772). When provided, Codex uses this path on Windows and `managedCommand`
|
|
1017
|
-
* on POSIX without needing per-platform config regeneration.
|
|
1018
|
-
* matcher: optional Codex MatcherGroup pattern (e.g. 'Bash|Edit|Write').
|
|
1019
|
-
* timeout: optional timeout in seconds.
|
|
1020
|
-
* @returns {{ changed: boolean, wrote: boolean, path: string }}
|
|
1021
|
-
*/
|
|
1022
|
-
function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
|
|
1023
|
-
return hooksSurface.reconcileCodexHooksJsonEvent(targetDir, eventName, opts);
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
/**
|
|
1027
|
-
* Reconcile the GSD-managed SessionStart hook entry in hooks.json.
|
|
1028
|
-
* Delegates to the generic reconcileCodexHooksJsonEvent helper.
|
|
1029
|
-
*
|
|
1030
|
-
* @param {string} targetDir
|
|
1031
|
-
* @param {{ managedCommand?: string|null, commandWindows?: string|null }} opts
|
|
1032
|
-
* @returns {{ changed: boolean, wrote: boolean, path: string }}
|
|
1033
|
-
*/
|
|
1034
|
-
function reconcileCodexHooksJsonSessionStart(targetDir, opts = {}) {
|
|
1035
|
-
return hooksSurface.reconcileCodexHooksJsonSessionStart(targetDir, opts);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
/**
|
|
1039
|
-
* Build a typed IR for the Codex hook .cmd shim used on Windows (#3426).
|
|
1040
|
-
*
|
|
1041
|
-
* On Windows, Codex runs hook commands from a PowerShell/cmd execution
|
|
1042
|
-
* environment. The previous command format was:
|
|
1043
|
-
*
|
|
1044
|
-
* "C:/Program Files/nodejs/node.exe" "C:/path/.codex/hooks/gsd-check-update.js"
|
|
1045
|
-
*
|
|
1046
|
-
* This caused `bash.exe: bash.exe: cannot execute binary file` because
|
|
1047
|
-
* Codex's hook dispatch shell (Git Bash / MSYS) tried to POSIX-exec node.exe
|
|
1048
|
-
* (a Windows PE binary) via execvp(), which fails with ENOEXEC on Windows PE
|
|
1049
|
-
* binaries that the MSYS layer doesn't know how to fork-exec natively.
|
|
1050
|
-
*
|
|
1051
|
-
* Fix: write a .cmd shim (using the same CRLF .cmd shim pattern) whose
|
|
1052
|
-
* content is `@ECHO OFF / @SETLOCAL / @"node.exe" "script.js" %*`.
|
|
1053
|
-
* cmd.exe executes
|
|
1054
|
-
* .cmd natively via CreateProcess — no POSIX exec layer, no MSYS shebang
|
|
1055
|
-
* walk, no PE binary fork-exec failure.
|
|
1056
|
-
*
|
|
1057
|
-
* Returns the typed IR `{ invocation, cmdPath, hookCommand, render }` so
|
|
1058
|
-
* callers can assert on the structured shape (CONTRIBUTING.md L558–L565
|
|
1059
|
-
* IR-first discipline). Returns null when absoluteRunnerToken is null so
|
|
1060
|
-
* callers can warn-and-skip instead of writing a broken hook.
|
|
1061
|
-
*
|
|
1062
|
-
* @param {string} scriptAbsPath - Absolute path to the .js hook script.
|
|
1063
|
-
* @param {string|null} absoluteRunnerToken - JSON-quoted absolute node path
|
|
1064
|
-
* (result of resolveNodeRunner()), e.g. `"C:/Program Files/nodejs/node.exe"`.
|
|
1065
|
-
* @returns {{ invocation: { interpreter: string, target: string }, cmdPath: string, hookCommand: string, render: { cmd: () => string } }|null}
|
|
1066
|
-
*/
|
|
1067
|
-
function buildCodexHookWindowsShimIR(scriptAbsPath, absoluteRunnerToken) {
|
|
1068
|
-
return hooksSurface.buildCodexHookWindowsShimIR(scriptAbsPath, absoluteRunnerToken);
|
|
1069
|
-
}
|
|
1180
|
+
// #2876: reconcileManagedShellHookCommands, buildCodexHookBlock,
|
|
1181
|
+
// rewriteLegacyCodexHookBlock, reconcileCodexHooksJsonEvent, and
|
|
1182
|
+
// reconcileCodexHooksJsonSessionStart used to be re-bound here as one-line
|
|
1183
|
+
// delegates to the equivalent hooksSurface.* implementations. None had an
|
|
1184
|
+
// install.js internal caller or an export consumer (tests import all five
|
|
1185
|
+
// directly from gsd-core/bin/lib/runtime-hooks-surface.cjs), so the retired
|
|
1186
|
+
// bindings were dead code with no reachable body — removed rather than kept
|
|
1187
|
+
// as unreachable wrappers.
|
|
1070
1188
|
|
|
1071
1189
|
/**
|
|
1072
1190
|
* Ensure Codex hooks.json contains exactly one managed SessionStart
|
|
@@ -1265,275 +1383,30 @@ function writeSettings(settingsPath, settings) {
|
|
|
1265
1383
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
1266
1384
|
}
|
|
1267
1385
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
*
|
|
1293
|
-
* Merges `~/.gsd/defaults.json` (global) with per-project
|
|
1294
|
-
* `<project>/.planning/config.json`. Per-project keys win on conflict so a
|
|
1295
|
-
* user can tune a single agent's model in one repo without re-setting the
|
|
1296
|
-
* global defaults for every other repo. Non-conflicting keys from both
|
|
1297
|
-
* sources are preserved.
|
|
1298
|
-
*
|
|
1299
|
-
* This is the fix for #2256: both adapters previously read only the global
|
|
1300
|
-
* file, so a per-project `model_overrides` (the common case the reporter
|
|
1301
|
-
* described — a per-project override for `gsd-codebase-mapper` in
|
|
1302
|
-
* `.planning/config.json`) was silently dropped and child agents inherited
|
|
1303
|
-
* the session default.
|
|
1304
|
-
*
|
|
1305
|
-
* `targetDir` is the consuming runtime's install root (e.g. `~/.codex` for
|
|
1306
|
-
* a global install, or `<project>/.codex` for a local install). We walk up
|
|
1307
|
-
* from there looking for `.planning/` so both cases resolve the correct
|
|
1308
|
-
* project root. When `targetDir` is null/undefined only the global file is
|
|
1309
|
-
* consulted (matches prior behavior for code paths that have no project
|
|
1310
|
-
* context).
|
|
1311
|
-
*
|
|
1312
|
-
* Returns a plain `{ agentName: modelId }` object, or `null` when neither
|
|
1313
|
-
* source defines `model_overrides`.
|
|
1314
|
-
*/
|
|
1315
|
-
function readGsdEffectiveModelOverrides(targetDir = null, options = {}) {
|
|
1316
|
-
const global = readGsdGlobalModelOverrides(options);
|
|
1317
|
-
|
|
1318
|
-
let projectOverrides = null;
|
|
1319
|
-
if (targetDir) {
|
|
1320
|
-
let probeDir = path.resolve(targetDir);
|
|
1321
|
-
for (let depth = 0; depth < 8; depth += 1) {
|
|
1322
|
-
const candidate = path.join(probeDir, '.planning', 'config.json');
|
|
1323
|
-
if (fs.existsSync(candidate)) {
|
|
1324
|
-
try {
|
|
1325
|
-
const parsed = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
1326
|
-
if (parsed && typeof parsed === 'object' && parsed.model_overrides
|
|
1327
|
-
&& typeof parsed.model_overrides === 'object') {
|
|
1328
|
-
projectOverrides = parsed.model_overrides;
|
|
1329
|
-
}
|
|
1330
|
-
} catch {
|
|
1331
|
-
// Malformed config.json — fall back to global; readGsdRuntimeProfileResolver
|
|
1332
|
-
// surfaces a parse warning via _readGsdConfigFile already.
|
|
1333
|
-
}
|
|
1334
|
-
break;
|
|
1335
|
-
}
|
|
1336
|
-
const parent = path.dirname(probeDir);
|
|
1337
|
-
if (parent === probeDir) break;
|
|
1338
|
-
probeDir = parent;
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
if (!global && !projectOverrides) return null;
|
|
1343
|
-
// Per-project wins on conflict; preserve non-conflicting global keys.
|
|
1344
|
-
return { ...(global || {}), ...(projectOverrides || {}) };
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
/**
|
|
1348
|
-
* #443 — Inject `effort: <value>` into YAML frontmatter of a Claude .md agent
|
|
1349
|
-
* file in a newline-agnostic way (LF and CRLF source files are both handled).
|
|
1350
|
-
*
|
|
1351
|
-
* The function:
|
|
1352
|
-
* - Detects the file's EOL (CRLF if the first `---` line ends with \r\n,
|
|
1353
|
-
* otherwise LF).
|
|
1354
|
-
* - Skips injection if an `effort:` key already exists in the frontmatter
|
|
1355
|
-
* (idempotent).
|
|
1356
|
-
* - Inserts `effort: <value>` immediately before the closing `---` delimiter,
|
|
1357
|
-
* using the same EOL as the surrounding frontmatter so the output file
|
|
1358
|
-
* stays EOL-consistent.
|
|
1359
|
-
* - Returns the original content unchanged when no YAML frontmatter is found.
|
|
1360
|
-
*
|
|
1361
|
-
* @param {string} content Raw file content (may have LF or CRLF endings).
|
|
1362
|
-
* @param {string} effortValue Rendered effort string, e.g. "xhigh".
|
|
1363
|
-
* @returns {string} Updated content with `effort:` injected, or the
|
|
1364
|
-
* original content when no frontmatter is found.
|
|
1365
|
-
*/
|
|
1366
|
-
function injectEffortFrontmatter(content, effortValue) {
|
|
1367
|
-
// Detect the dominant EOL from the first line (the opening `---`).
|
|
1368
|
-
// If the very first `---` is followed by \r\n, treat the whole file as CRLF.
|
|
1369
|
-
const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
|
|
1370
|
-
|
|
1371
|
-
// Build a frontmatter-matching regex that tolerates an optional \r before
|
|
1372
|
-
// each \n, so we handle both LF and CRLF files without needing to normalise
|
|
1373
|
-
// the whole content.
|
|
1374
|
-
//
|
|
1375
|
-
// Breakdown:
|
|
1376
|
-
// ^---\r?\n — opening delimiter (with optional \r)
|
|
1377
|
-
// ([\s\S]*?) — frontmatter body (non-greedy)
|
|
1378
|
-
// ^---\r?$ — closing delimiter line (optional \r, $ before \n in
|
|
1379
|
-
// multiline mode)
|
|
1380
|
-
// (\r?\n|$) — newline after closing --- (or end of string)
|
|
1381
|
-
//
|
|
1382
|
-
// The `m` flag makes ^ / $ match at every line boundary.
|
|
1383
|
-
const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
|
|
1384
|
-
const match = fmRe.exec(content);
|
|
1385
|
-
if (!match) return content; // no YAML frontmatter — leave unchanged
|
|
1386
|
-
|
|
1387
|
-
// Idempotency guard: don't insert a second effort: line.
|
|
1388
|
-
const fmBody = match[1]; // content between the two `---` lines
|
|
1389
|
-
if (/^effort:/m.test(fmBody)) return content;
|
|
1390
|
-
|
|
1391
|
-
// Locate the exact position of the closing `---` line so we can insert
|
|
1392
|
-
// before it using a simple string splice (avoids re-running the regex and
|
|
1393
|
-
// avoids any edge-cases with $ matching \r differently per engine).
|
|
1394
|
-
const closeIdx = match.index + 4 + fmBody.length; // 4 = len("---\n") (opening)
|
|
1395
|
-
// Actually compute based on the full match start + captured group length:
|
|
1396
|
-
// match[0] = full frontmatter block; match.index = start of that block.
|
|
1397
|
-
// The closing `---` starts at: match.index + ("---" + eol).length + fmBody.length
|
|
1398
|
-
const openLen = 3 + eol.length; // "---" + eol
|
|
1399
|
-
const closingStart = match.index + openLen + fmBody.length;
|
|
1400
|
-
|
|
1401
|
-
const before = content.slice(0, closingStart);
|
|
1402
|
-
const after = content.slice(closingStart);
|
|
1403
|
-
return `${before}effort: ${effortValue}${eol}${after}`;
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
/**
|
|
1407
|
-
* #767 — Inject `disallowedTools: <value>` into the YAML frontmatter of a Claude .md agent.
|
|
1408
|
-
* Mirrors injectEffortFrontmatter: idempotent (skips if disallowedTools: already present),
|
|
1409
|
-
* inserts immediately before the closing `---`. Claude-only — never call for other runtimes,
|
|
1410
|
-
* which break on unknown frontmatter keys.
|
|
1411
|
-
*/
|
|
1412
|
-
function injectDisallowedToolsFrontmatter(content, disallowedValue) {
|
|
1413
|
-
// Detect the dominant EOL from the first line (the opening `---`).
|
|
1414
|
-
// If the very first `---` is followed by \r\n, treat the whole file as CRLF.
|
|
1415
|
-
const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
|
|
1416
|
-
|
|
1417
|
-
// Build a frontmatter-matching regex that tolerates an optional \r before
|
|
1418
|
-
// each \n, so we handle both LF and CRLF files without needing to normalise
|
|
1419
|
-
// the whole content.
|
|
1420
|
-
const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
|
|
1421
|
-
const match = fmRe.exec(content);
|
|
1422
|
-
if (!match) return content; // no YAML frontmatter — leave unchanged
|
|
1423
|
-
|
|
1424
|
-
// Idempotency guard: don't insert a second disallowedTools: line.
|
|
1425
|
-
const fmBody = match[1]; // content between the two `---` lines
|
|
1426
|
-
if (/^disallowedTools:/m.test(fmBody)) return content;
|
|
1427
|
-
|
|
1428
|
-
// Locate the exact position of the closing `---` line so we can insert
|
|
1429
|
-
// before it using a simple string splice.
|
|
1430
|
-
const openLen = 3 + eol.length; // "---" + eol
|
|
1431
|
-
const closingStart = match.index + openLen + fmBody.length;
|
|
1432
|
-
|
|
1433
|
-
const before = content.slice(0, closingStart);
|
|
1434
|
-
const after = content.slice(closingStart);
|
|
1435
|
-
return `${before}disallowedTools: ${disallowedValue}${eol}${after}`;
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
// #767 — Read-only verifier/auditor agents get a Claude-Code disallowedTools deny-list.
|
|
1439
|
-
// Group A (pure read-only) deny Write,Edit,MultiEdit. Group B report-writers Write one
|
|
1440
|
-
// output file so they deny only Edit,MultiEdit. gsd-nyquist-auditor is intentionally
|
|
1441
|
-
// excluded (it legitimately uses Write AND Edit to create/patch test files).
|
|
1442
|
-
const READONLY_AGENT_DISALLOWED_TOOLS = {
|
|
1443
|
-
'gsd-plan-checker': 'Write, Edit, MultiEdit',
|
|
1444
|
-
'gsd-integration-checker': 'Write, Edit, MultiEdit',
|
|
1445
|
-
'gsd-ui-checker': 'Write, Edit, MultiEdit',
|
|
1446
|
-
'gsd-verifier': 'Edit, MultiEdit',
|
|
1447
|
-
'gsd-doc-verifier': 'Edit, MultiEdit',
|
|
1448
|
-
'gsd-eval-auditor': 'Edit, MultiEdit',
|
|
1449
|
-
'gsd-ui-auditor': 'Edit, MultiEdit',
|
|
1450
|
-
};
|
|
1451
|
-
|
|
1452
|
-
/**
|
|
1453
|
-
* #2517 — Build a runtime-aware tier resolver for the install path.
|
|
1454
|
-
*
|
|
1455
|
-
* Probes BOTH per-project `<targetDir>/.planning/config.json` AND
|
|
1456
|
-
* `~/.gsd/defaults.json`, with per-project keys winning over global. This
|
|
1457
|
-
* matches `loadConfig`'s precedence and is the only way the PR's headline claim
|
|
1458
|
-
* — "set runtime in .planning/config.json and the Codex TOML emit picks it up"
|
|
1459
|
-
* — actually holds end-to-end (review finding #1).
|
|
1460
|
-
*
|
|
1461
|
-
* `targetDir` should be the consuming runtime's install root — install code
|
|
1462
|
-
* passes `path.dirname(<runtime root>)` so `.planning/config.json` resolves
|
|
1463
|
-
* relative to the user's project. When `targetDir` is null/undefined, only the
|
|
1464
|
-
* global defaults are consulted.
|
|
1465
|
-
*
|
|
1466
|
-
* Returns null if no `runtime` is configured (preserves prior behavior — only
|
|
1467
|
-
* model_overrides is embedded, no tier/reasoning-effort inference). Returns
|
|
1468
|
-
* null when `model_profile` is `inherit` so the literal alias passes through
|
|
1469
|
-
* unchanged.
|
|
1470
|
-
*
|
|
1471
|
-
* Returns { runtime, resolve(agentName) -> { model, reasoning_effort? } | null }
|
|
1472
|
-
*/
|
|
1473
|
-
function readGsdRuntimeProfileResolver(targetDir = null) {
|
|
1474
|
-
const homeDefaults = _readGsdConfigFile(
|
|
1475
|
-
path.join(os.homedir(), '.gsd', 'defaults.json'),
|
|
1476
|
-
'~/.gsd/defaults.json'
|
|
1477
|
-
);
|
|
1478
|
-
|
|
1479
|
-
// Per-project config probe. Resolve the project root by walking up from
|
|
1480
|
-
// targetDir until we hit a `.planning/` directory; this covers both the
|
|
1481
|
-
// common case (caller passes the project root) and the case where caller
|
|
1482
|
-
// passes a nested install dir like `<root>/.codex/`.
|
|
1483
|
-
let projectConfig = null;
|
|
1484
|
-
if (targetDir) {
|
|
1485
|
-
let probeDir = path.resolve(targetDir);
|
|
1486
|
-
for (let depth = 0; depth < 8; depth += 1) {
|
|
1487
|
-
const candidate = path.join(probeDir, '.planning', 'config.json');
|
|
1488
|
-
if (fs.existsSync(candidate)) {
|
|
1489
|
-
projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
|
|
1490
|
-
break;
|
|
1491
|
-
}
|
|
1492
|
-
const parent = path.dirname(probeDir);
|
|
1493
|
-
if (parent === probeDir) break;
|
|
1494
|
-
probeDir = parent;
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
// Per-project wins. Only fall back to ~/.gsd/defaults.json when the project
|
|
1499
|
-
// didn't set the field. Field-level merge (not whole-object replace) so a
|
|
1500
|
-
// user can keep `runtime` global while overriding only `model_profile` per
|
|
1501
|
-
// project, and vice versa.
|
|
1502
|
-
const merged = {
|
|
1503
|
-
runtime:
|
|
1504
|
-
(projectConfig && projectConfig.runtime) ||
|
|
1505
|
-
(homeDefaults && homeDefaults.runtime) ||
|
|
1506
|
-
null,
|
|
1507
|
-
model_profile:
|
|
1508
|
-
(projectConfig && projectConfig.model_profile) ||
|
|
1509
|
-
(homeDefaults && homeDefaults.model_profile) ||
|
|
1510
|
-
'balanced',
|
|
1511
|
-
model_profile_overrides:
|
|
1512
|
-
(projectConfig && projectConfig.model_profile_overrides) ||
|
|
1513
|
-
(homeDefaults && homeDefaults.model_profile_overrides) ||
|
|
1514
|
-
null,
|
|
1515
|
-
};
|
|
1516
|
-
|
|
1517
|
-
if (!merged.runtime) return null;
|
|
1518
|
-
|
|
1519
|
-
const profile = String(merged.model_profile).toLowerCase();
|
|
1520
|
-
if (profile === 'inherit') return null;
|
|
1521
|
-
|
|
1522
|
-
return {
|
|
1523
|
-
runtime: merged.runtime,
|
|
1524
|
-
resolve(agentName) {
|
|
1525
|
-
const agentModels = GSD_MODEL_PROFILES[agentName];
|
|
1526
|
-
if (!agentModels) return null;
|
|
1527
|
-
const tier = agentModels[profile] || agentModels.balanced;
|
|
1528
|
-
if (!tier) return null;
|
|
1529
|
-
return gsdResolveTierEntry({
|
|
1530
|
-
runtime: merged.runtime,
|
|
1531
|
-
tier,
|
|
1532
|
-
overrides: merged.model_profile_overrides,
|
|
1533
|
-
});
|
|
1534
|
-
},
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1386
|
+
// #2875 Part 2 (J8): model-override resolution (readGsdGlobalModelOverrides /
|
|
1387
|
+
// readGsdEffectiveModelOverrides / readGsdRuntimeProfileResolver, plus the
|
|
1388
|
+
// shared resolveAgentModelOverride precedence chain) was extracted into the
|
|
1389
|
+
// shipped gsd-core/bin/lib/install-model-override-resolver.cjs, mirroring
|
|
1390
|
+
// install-effort-resolver.cjs's existing #2071 precedent, so the descriptor-
|
|
1391
|
+
// driven agents pipeline (runtime-artifact-layout.cts's convertedAgentsKind)
|
|
1392
|
+
// and this installer resolve model_overrides / model_profile_overrides
|
|
1393
|
+
// through the SAME code — a single source of truth for the precedence chain
|
|
1394
|
+
// the inline agent loop below used to duplicate across ~24 lines per runtime
|
|
1395
|
+
// (kilo, opencode). See install-model-override-resolver.cts's module doc.
|
|
1396
|
+
const {
|
|
1397
|
+
readGsdEffectiveModelOverrides,
|
|
1398
|
+
readGsdRuntimeProfileResolver,
|
|
1399
|
+
resolveAgentModelOverride,
|
|
1400
|
+
} = require(path.join(_gsdLibDir, 'install-model-override-resolver.cjs'));
|
|
1401
|
+
|
|
1402
|
+
// #2875 Part 2: effort frontmatter injection moved to runtimeArtifactConversion
|
|
1403
|
+
// (single source of truth with the descriptor pipeline's
|
|
1404
|
+
// applyAgentFrontmatterExtensions step, which now also owns disallowedTools
|
|
1405
|
+
// injection + the read-only agent deny-list internally — see its module doc
|
|
1406
|
+
// in src/runtime-artifact-conversion.cts). injectEffortFrontmatter used to be
|
|
1407
|
+
// re-bound here purely to stay on this module's export surface; #2876 found
|
|
1408
|
+
// no install.js internal caller either (tests import it directly from
|
|
1409
|
+
// gsd-core/bin/lib/runtime-artifact-conversion.cjs) and retired the binding.
|
|
1537
1410
|
|
|
1538
1411
|
// Cache for attribution settings (populated once per runtime during install)
|
|
1539
1412
|
const attributionCache = new Map();
|
|
@@ -1822,10 +1695,6 @@ function buildKiloAgentPermissionBlock(claudeTools) {
|
|
|
1822
1695
|
return lines;
|
|
1823
1696
|
}
|
|
1824
1697
|
|
|
1825
|
-
function escapeRegExp(value) {
|
|
1826
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
1698
|
function replaceRelativePathReference(content, fromPath, toPath) {
|
|
1830
1699
|
const escapedPath = escapeRegExp(fromPath);
|
|
1831
1700
|
return content.replace(
|
|
@@ -1946,10 +1815,6 @@ function skillFrontmatterName(skillDirName) {
|
|
|
1946
1815
|
return skillDirName;
|
|
1947
1816
|
}
|
|
1948
1817
|
|
|
1949
|
-
function normalizeClaudeSkillEffort(effort) {
|
|
1950
|
-
return effort === 'xhigh' ? 'max' : effort;
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
1818
|
/**
|
|
1954
1819
|
* Qwen Code skills accept an optional numeric `priority` frontmatter field.
|
|
1955
1820
|
* Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified
|
|
@@ -2006,10 +1871,11 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
|
|
|
2006
1871
|
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
2007
1872
|
const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
|
|
2008
1873
|
const agent = extractFrontmatterField(frontmatter, 'agent');
|
|
2009
|
-
// #769: preserve context:
|
|
2010
|
-
//
|
|
1874
|
+
// #769: preserve context: from source command files so it is emitted into
|
|
1875
|
+
// the installed SKILL.md frontmatter unchanged. (#3151: effort: is no longer
|
|
1876
|
+
// emitted into skill frontmatter — a static effort value invalidates the
|
|
1877
|
+
// caller's prompt cache at both scope boundaries.)
|
|
2011
1878
|
const context = extractFrontmatterField(frontmatter, 'context');
|
|
2012
|
-
const effort = extractFrontmatterField(frontmatter, 'effort');
|
|
2013
1879
|
|
|
2014
1880
|
// Preserve allowed-tools as YAML multiline list (Claude native format)
|
|
2015
1881
|
const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
@@ -2043,12 +1909,13 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
|
|
|
2043
1909
|
}
|
|
2044
1910
|
if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`;
|
|
2045
1911
|
if (agent) fm += `agent: ${agent}\n`;
|
|
2046
|
-
// #769: emit context:
|
|
2047
|
-
//
|
|
2048
|
-
//
|
|
2049
|
-
//
|
|
1912
|
+
// #769: emit context: when present so the runtime can honour it natively
|
|
1913
|
+
// (context: fork = isolated subagent window). Claude-specific; unknown
|
|
1914
|
+
// frontmatter fields are silently ignored by other runtimes (backward-compatible).
|
|
1915
|
+
// (#3151: effort: is intentionally NOT emitted into skill frontmatter — a
|
|
1916
|
+
// static effort value changes output_config.effort on invocation and
|
|
1917
|
+
// invalidates the caller's prompt cache at both scope boundaries.)
|
|
2050
1918
|
if (context) fm += `context: ${context}\n`;
|
|
2051
|
-
if (effort) fm += `effort: ${normalizeClaudeSkillEffort(effort)}\n`;
|
|
2052
1919
|
if (toolsBlock) fm += toolsBlock;
|
|
2053
1920
|
fm += '---';
|
|
2054
1921
|
|
|
@@ -2501,56 +2368,6 @@ function extractFrontmatterField(frontmatter, fieldName) {
|
|
|
2501
2368
|
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
|
2502
2369
|
}
|
|
2503
2370
|
|
|
2504
|
-
// #2284 finding (b): the `<runtime_compatibility>` block appearing in
|
|
2505
|
-
// gsd-core/workflows/{plan-phase,execute-phase}.md is a runtime-COMPARISON
|
|
2506
|
-
// table ("**Claude Code:** Uses `Agent(...)`" / "a backgrounded Claude Code
|
|
2507
|
-
// agent" / "top-level Claude Code") — every "Claude Code" mention inside it
|
|
2508
|
-
// is a COMPARED-RUNTIME LABEL, not a host self-reference. The brand swap
|
|
2509
|
-
// below (`Claude Code` → the installing runtime's own display name) is
|
|
2510
|
-
// meant only for host self-references; applying it inside this block
|
|
2511
|
-
// mislabels the comparison (e.g. Windsurf installs would read "**Windsurf:**
|
|
2512
|
-
// Uses `Agent(...)`" describing what is actually Claude Code's behavior).
|
|
2513
|
-
// This is cross-cutting across every runtime that brand-swaps workflow
|
|
2514
|
-
// content (cursor/windsurf/trae/cline/codebuddy hardcoded; qwen/hermes
|
|
2515
|
-
// descriptor-driven via hostBehaviors.brandingRewrites) — confirmed to
|
|
2516
|
-
// reproduce on unmodified Windsurf, not Hermes-specific.
|
|
2517
|
-
const RUNTIME_COMPATIBILITY_BLOCK_RE = /<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/g;
|
|
2518
|
-
|
|
2519
|
-
/**
|
|
2520
|
-
* Rewrite bare "Claude Code" self-references in workflow content to
|
|
2521
|
-
* `brandName`, EXCEPT inside `<runtime_compatibility>...</runtime_compatibility>`
|
|
2522
|
-
* blocks, which are left byte-for-byte verbatim. Every other content
|
|
2523
|
-
* transform in a runtime's `.md` converter (tool-name renames, path
|
|
2524
|
-
* rewrites, etc.) is unaffected — only this literal brand-name swap is
|
|
2525
|
-
* protected-region-aware, since only it risks mislabeling a
|
|
2526
|
-
* runtime-comparison table.
|
|
2527
|
-
*
|
|
2528
|
-
* Implementation: SPLIT `content` on the protected-block regex, brand-swap
|
|
2529
|
-
* only the GAP text between (and around) matches, then rejoin gap+block
|
|
2530
|
-
* alternately. No placeholder/sentinel token of any kind is substituted in
|
|
2531
|
-
* — a prior version used a sentinel-token mask/restore, which is exactly the
|
|
2532
|
-
* kind of invisible landmine this rewrite eliminates (a sentinel string, no
|
|
2533
|
-
* matter how obscure, is a theoretical collision risk with real content and
|
|
2534
|
-
* is easy to silently reintroduce in a future edit without it showing in a
|
|
2535
|
-
* diff). Behavior-identical to the removed sentinel-token version — verified
|
|
2536
|
-
* via `npm run gen:golden` producing zero further diff.
|
|
2537
|
-
*/
|
|
2538
|
-
function applyClaudeCodeBrandSwap(content, brandName) {
|
|
2539
|
-
if (!brandName) return content;
|
|
2540
|
-
let result = '';
|
|
2541
|
-
let lastIndex = 0;
|
|
2542
|
-
RUNTIME_COMPATIBILITY_BLOCK_RE.lastIndex = 0; // reset shared global-regex state before each use
|
|
2543
|
-
let m;
|
|
2544
|
-
while ((m = RUNTIME_COMPATIBILITY_BLOCK_RE.exec(content))) {
|
|
2545
|
-
const gap = content.slice(lastIndex, m.index);
|
|
2546
|
-
result += gap.replace(/\bClaude Code\b/g, brandName);
|
|
2547
|
-
result += m[0]; // protected block, verbatim — never brand-swapped
|
|
2548
|
-
lastIndex = m.index + m[0].length;
|
|
2549
|
-
}
|
|
2550
|
-
result += content.slice(lastIndex).replace(/\bClaude Code\b/g, brandName);
|
|
2551
|
-
return result;
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
2371
|
// Tool name mapping from Claude Code to Cursor CLI
|
|
2555
2372
|
const claudeToCursorTools = {
|
|
2556
2373
|
Bash: 'Shell',
|
|
@@ -2629,36 +2446,11 @@ function convertClaudeCommandToCursorSkill(content, skillName) {
|
|
|
2629
2446
|
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
2630
2447
|
const adapter = getCursorSkillAdapterHeader(skillName);
|
|
2631
2448
|
|
|
2632
|
-
//
|
|
2633
|
-
//
|
|
2634
|
-
//
|
|
2635
|
-
//
|
|
2636
|
-
|
|
2637
|
-
// '/' while keeping model invocation — distinct from disable-model-invocation.)
|
|
2638
|
-
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
/**
|
|
2642
|
-
* Convert a Claude Code command to a Cursor 1.6 slash command (#785).
|
|
2643
|
-
*
|
|
2644
|
-
* Cursor slash commands live in `.cursor/commands/<name>.md` and are
|
|
2645
|
-
* plain markdown — no YAML frontmatter, no adapter header. The filename
|
|
2646
|
-
* becomes the command name (e.g. `gsd-help.md` → `/gsd-help`).
|
|
2647
|
-
*
|
|
2648
|
-
* Applies the same `convertClaudeToCursorMarkdown` transforms as the skill
|
|
2649
|
-
* converter (tool renames, brand substitution, slash-command normalisation),
|
|
2650
|
-
* then strips the YAML frontmatter block so only the prose body remains.
|
|
2651
|
-
*
|
|
2652
|
-
* @param {string} content raw Claude Code command markdown (may have frontmatter)
|
|
2653
|
-
* @param {string} _commandName the target command name (unused; present for
|
|
2654
|
-
* API symmetry with other converters so the runtime-artifact-layout stage
|
|
2655
|
-
* function can call it uniformly)
|
|
2656
|
-
* @returns {string} plain markdown body, no frontmatter
|
|
2657
|
-
*/
|
|
2658
|
-
function convertClaudeCommandToCursorCommand(content, _commandName) {
|
|
2659
|
-
const converted = convertClaudeToCursorMarkdown(content);
|
|
2660
|
-
const { body } = extractFrontmatterAndBody(converted);
|
|
2661
|
-
return body.trimStart();
|
|
2449
|
+
// Cursor skills are both slash-invocable and model-invocable. Do not emit the
|
|
2450
|
+
// unsupported `user-invocable` field: it is ignored by Cursor and previously
|
|
2451
|
+
// hid the real cause of duplicate entries, the parallel commands/ surface
|
|
2452
|
+
// retired in #2644.
|
|
2453
|
+
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
2662
2454
|
}
|
|
2663
2455
|
|
|
2664
2456
|
/**
|
|
@@ -2681,142 +2473,16 @@ function convertClaudeAgentToCursorAgent(content) {
|
|
|
2681
2473
|
}
|
|
2682
2474
|
|
|
2683
2475
|
// --- Windsurf converters ---
|
|
2684
|
-
//
|
|
2685
|
-
//
|
|
2686
|
-
|
|
2687
|
-
//
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
function convertSlashCommandsToWindsurfSkillMentions(content) {
|
|
2696
|
-
// Keep leading "/" for slash commands; only normalize gsd: -> gsd-.
|
|
2697
|
-
return content.replace(/gsd:/gi, 'gsd-');
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
function convertClaudeToWindsurfMarkdown(content) {
|
|
2701
|
-
let converted = convertSlashCommandsToWindsurfSkillMentions(content);
|
|
2702
|
-
// Replace tool name references in body text
|
|
2703
|
-
converted = converted.replace(/\bBash\(/g, 'Shell(');
|
|
2704
|
-
converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
|
|
2705
|
-
converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting');
|
|
2706
|
-
// Replace subagent_type from Claude to Windsurf format
|
|
2707
|
-
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
|
2708
|
-
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
|
2709
|
-
// Replace project-level Claude conventions with Windsurf equivalents.
|
|
2710
|
-
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
|
|
2711
|
-
converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
|
|
2712
|
-
converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
|
|
2713
|
-
converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
|
|
2714
|
-
converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
|
|
2715
|
-
converted = converted.replace(/\.\/\.claude\//g, './.windsurf/');
|
|
2716
|
-
converted = converted.replace(/\.claude\//g, '.windsurf/');
|
|
2717
|
-
// Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
|
|
2718
|
-
// Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
|
|
2719
|
-
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf');
|
|
2720
|
-
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf');
|
|
2721
|
-
// Environment variable name rewrite
|
|
2722
|
-
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
|
|
2723
|
-
// Remove Claude Code-specific bug workarounds before brand replacement
|
|
2724
|
-
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2725
|
-
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2726
|
-
// Replace "Claude Code" brand references with "Windsurf" — #2284(b): skips
|
|
2727
|
-
// <runtime_compatibility> comparison-table content (protected region).
|
|
2728
|
-
converted = applyClaudeCodeBrandSwap(converted, 'Windsurf');
|
|
2729
|
-
return converted;
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
function getWindsurfSkillAdapterHeader(skillName) {
|
|
2733
|
-
return `<windsurf_skill_adapter>
|
|
2734
|
-
## A. Skill Invocation
|
|
2735
|
-
- This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill.
|
|
2736
|
-
- Treat all user text after the skill mention as \`{{GSD_ARGS}}\`.
|
|
2737
|
-
- If no arguments are present, treat \`{{GSD_ARGS}}\` as empty.
|
|
2738
|
-
|
|
2739
|
-
## B. User Prompting
|
|
2740
|
-
When the workflow needs user input, prompt the user conversationally:
|
|
2741
|
-
- Present options as a numbered list in your response text
|
|
2742
|
-
- Ask the user to reply with their choice
|
|
2743
|
-
- For multi-select, ask for comma-separated numbers
|
|
2744
|
-
|
|
2745
|
-
## C. Tool Usage
|
|
2746
|
-
Use these Windsurf tools when executing GSD workflows:
|
|
2747
|
-
- \`Shell\` for running commands (terminal operations)
|
|
2748
|
-
- \`StrReplace\` for editing existing files
|
|
2749
|
-
- \`Read\`, \`Write\`, \`Glob\`, \`Grep\`, \`Task\`, \`WebSearch\`, \`WebFetch\`, \`TodoWrite\` as needed
|
|
2750
|
-
|
|
2751
|
-
## D. Subagent Spawning
|
|
2752
|
-
When the workflow needs to spawn a subagent:
|
|
2753
|
-
- Use \`Task(subagent_type="generalPurpose", ...)\`
|
|
2754
|
-
- The \`model\` parameter maps to Windsurf's model options (e.g., "fast")
|
|
2755
|
-
</windsurf_skill_adapter>`;
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
|
-
function convertClaudeCommandToWindsurfSkill(content, skillName) {
|
|
2759
|
-
const converted = convertClaudeToWindsurfMarkdown(content);
|
|
2760
|
-
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
2761
|
-
let description = `Run GSD workflow ${skillName}.`;
|
|
2762
|
-
if (frontmatter) {
|
|
2763
|
-
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
|
|
2764
|
-
if (maybeDescription) {
|
|
2765
|
-
description = maybeDescription;
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2768
|
-
description = toSingleLine(description);
|
|
2769
|
-
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
2770
|
-
const adapter = getWindsurfSkillAdapterHeader(skillName);
|
|
2771
|
-
|
|
2772
|
-
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
2773
|
-
}
|
|
2774
|
-
|
|
2775
|
-
function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
|
|
2776
|
-
// #1615 security: commandName flows unsanitized into a markdown body that
|
|
2777
|
-
// Windsurf loads as an LLM-readable workflow. Validate at entry to prevent
|
|
2778
|
-
// (a) prompt injection via newlines / markdown structure in the filename,
|
|
2779
|
-
// (b) path-component injection via .., /, \ in stem → @-reference target.
|
|
2780
|
-
// Pattern: optional gsd- prefix + lowercase alphanumeric + dashes; rejects
|
|
2781
|
-
// everything else. See DEFECT.PROMPT-INJECTION-SCAN-COLLISION and the
|
|
2782
|
-
// PR #1622 security review.
|
|
2783
|
-
if (typeof commandName !== 'string' || !/^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(commandName)) {
|
|
2784
|
-
const preview = typeof commandName === 'string' ? JSON.stringify(commandName.slice(0, 60)) : String(commandName);
|
|
2785
|
-
throw new Error(
|
|
2786
|
-
`convertClaudeCommandToWindsurfWorkflow: rejected commandName ${preview}; ` +
|
|
2787
|
-
'must match /^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ (no slashes, backslashes, spaces, dots, trailing dash, or control chars — prevents prompt injection and path-component injection into the workflow body)'
|
|
2788
|
-
);
|
|
2789
|
-
}
|
|
2790
|
-
const converted = convertClaudeToWindsurfMarkdown(content);
|
|
2791
|
-
const { frontmatter } = extractFrontmatterAndBody(converted);
|
|
2792
|
-
const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
|
|
2793
|
-
const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName;
|
|
2794
|
-
const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
|
|
2795
|
-
const byteLength = Buffer.byteLength(workflow, 'utf8');
|
|
2796
|
-
if (byteLength > 12000) {
|
|
2797
|
-
throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`);
|
|
2798
|
-
}
|
|
2799
|
-
return workflow;
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
|
-
/**
|
|
2803
|
-
* Convert Claude Code agent markdown to Windsurf agent format.
|
|
2804
|
-
* Strips frontmatter fields Windsurf doesn't support (color, skills),
|
|
2805
|
-
* converts tool references, and adds a role context header.
|
|
2806
|
-
*/
|
|
2807
|
-
function convertClaudeAgentToWindsurfAgent(content) {
|
|
2808
|
-
let converted = convertClaudeToWindsurfMarkdown(content);
|
|
2809
|
-
|
|
2810
|
-
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
2811
|
-
if (!frontmatter) return converted;
|
|
2812
|
-
|
|
2813
|
-
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
2814
|
-
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
2815
|
-
|
|
2816
|
-
const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
|
|
2817
|
-
|
|
2818
|
-
return `${cleanFrontmatter}\n${body}`;
|
|
2819
|
-
}
|
|
2476
|
+
// #2931 (ADR-1508): single-sourced in runtimeArtifactConversion, bound near
|
|
2477
|
+
// the top of this file alongside the #1675 Augment family. This block
|
|
2478
|
+
// previously carried byte-identical local duplicates of
|
|
2479
|
+
// convertSlashCommandsToWindsurfSkillMentions, convertClaudeToWindsurfMarkdown,
|
|
2480
|
+
// getWindsurfSkillAdapterHeader, convertClaudeCommandToWindsurfSkill,
|
|
2481
|
+
// convertClaudeCommandToWindsurfWorkflow, and convertClaudeAgentToWindsurfAgent,
|
|
2482
|
+
// plus an unused claudeToWindsurfTools table. Deleted here; the two
|
|
2483
|
+
// unexported helpers (getWindsurfSkillAdapterHeader,
|
|
2484
|
+
// convertSlashCommandsToWindsurfSkillMentions) now live only in the
|
|
2485
|
+
// conversion module, with no other caller in this file.
|
|
2820
2486
|
|
|
2821
2487
|
// --- Augment converters ---
|
|
2822
2488
|
// Augment uses a tool set similar to Cursor/Windsurf.
|
|
@@ -2844,10 +2510,55 @@ function convertClaudeToTraeMarkdown(content) {
|
|
|
2844
2510
|
// Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
|
|
2845
2511
|
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general_purpose_task"');
|
|
2846
2512
|
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2513
|
+
// #2658: full-path forms (with a leading dot-claude-slash prefix) MUST be
|
|
2514
|
+
// replaced before the bare Claude-instruction-file pattern and before the
|
|
2515
|
+
// generic dot-claude-slash rewrite below — otherwise the bare pattern
|
|
2516
|
+
// consumes only the instruction-filename tail, leaving that prefix stale
|
|
2517
|
+
// in place, and the generic rewrite then mutates the stale leftover too,
|
|
2518
|
+
// producing a doubled trae-prefix segment ahead of the rules path instead
|
|
2519
|
+
// of a single clean one. (Deliberately never spelling the instruction
|
|
2520
|
+
// filename as one contiguous "CLAUDE" + dot + "md" token, and never
|
|
2521
|
+
// spelling either malformed shape out as a literal contiguous string, in
|
|
2522
|
+
// ANY comment in this function: this file ships verbatim into local
|
|
2523
|
+
// `--trae` installs, where it is itself run through this same class of
|
|
2524
|
+
// find/replace — a literal instruction-filename token sitting in a
|
|
2525
|
+
// comment gets "fixed" right along with real code, and the emitted-content
|
|
2526
|
+
// regression test added alongside this fix asserts neither malformed
|
|
2527
|
+
// shape appears anywhere in the installed tree, comments included; this
|
|
2528
|
+
// bit the fix itself twice during development.) All forms converge on the
|
|
2529
|
+
// same concrete file (never a bare directory) so this stays in parity
|
|
2530
|
+
// with the `trae.js` RUNTIME_CONTENT_DISPATCH entry.
|
|
2531
|
+
converted = converted.replace(/`\.\/\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
|
|
2532
|
+
converted = converted.replace(/\.\/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
|
|
2533
|
+
converted = converted.replace(/`\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
|
|
2534
|
+
converted = converted.replace(/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
|
|
2535
|
+
// #2658 (found via the end-to-end install regression test, not the static
|
|
2536
|
+
// trace above): `copyWithPathReplacement` runs a GENERIC dot-claude-slash
|
|
2537
|
+
// -> runtime-config-dir rewrite on every .md file before calling this
|
|
2538
|
+
// converter — for `~/.claude/`, `$HOME/.claude/`, AND `./.claude/` alike —
|
|
2539
|
+
// substituting a runtime-appropriate `pathPrefix` this function is never
|
|
2540
|
+
// given and cannot itself compute (it differs per install invocation: a
|
|
2541
|
+
// relative `./.trae/` for a project-local install, an arbitrary absolute
|
|
2542
|
+
// path for a local install rooted elsewhere, `~/.trae/` for a global one).
|
|
2543
|
+
// So for source using any of those prefixed forms, the patterns above
|
|
2544
|
+
// never fire here — this converter only ever sees the ALREADY-rewritten
|
|
2545
|
+
// "<runtime-config-dir>/" + instruction-filename shape, with whatever
|
|
2546
|
+
// prefix the install actually used. The generic pattern below preserves
|
|
2547
|
+
// that prefix verbatim (via the capture group) and only fixes the
|
|
2548
|
+
// filename suffix, rather than assuming a fixed `./.trae/` shape — a
|
|
2549
|
+
// narrower fixed-prefix version of this pattern shipped first and still
|
|
2550
|
+
// left the doubled-prefix defect live for the `$HOME/.claude/` and
|
|
2551
|
+
// `~/.claude/` forms specifically (found the same way, one regression-test
|
|
2552
|
+
// run later). Scoped to a `.trae/` tail so it cannot also swallow the
|
|
2553
|
+
// unprefixed `./CLAUDE.md` form the very next pattern handles differently
|
|
2554
|
+
// (discarding the prefix entirely, not preserving it). Must run before
|
|
2555
|
+
// the bare pattern for the same consume-the-full-match-first reason.
|
|
2556
|
+
converted = converted.replace(/`([^\s`]*\.trae\/)CLAUDE\.md`/g, '`$1rules/rules.md`');
|
|
2557
|
+
converted = converted.replace(/([^\s`]*\.trae\/)CLAUDE\.md/g, '$1rules/rules.md');
|
|
2558
|
+
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
|
|
2559
|
+
converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/rules.md');
|
|
2560
|
+
converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/rules.md`');
|
|
2561
|
+
converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/rules.md');
|
|
2851
2562
|
converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/');
|
|
2852
2563
|
converted = converted.replace(/\.\/\.claude\//g, './.trae/');
|
|
2853
2564
|
converted = converted.replace(/\.claude\//g, '.trae/');
|
|
@@ -4018,7 +3729,9 @@ Typed mapping (agent_type-capable schema only):
|
|
|
4018
3729
|
to \`spawn_agent\` when the runtime/tool supports it. Omit missing, empty,
|
|
4019
3730
|
inherited, or unsupported values; do not invent one-off effort literals in
|
|
4020
3731
|
workflow prose.
|
|
4021
|
-
- \`fork_context: false\` by default — GSD agents load their own context via \`<
|
|
3732
|
+
- \`fork_context: false\` by default — GSD agents load their own context via \`<required_reading>\` blocks
|
|
3733
|
+
- \`task_name\` — required by the collaboration schema; provide a descriptive name for each spawned task
|
|
3734
|
+
- \`fork_turns\` — optional parameter controlling turn-forking depth; coexists with \`fork_context\` (not a replacement)
|
|
4022
3735
|
- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
|
|
4023
3736
|
but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex
|
|
4024
3737
|
\`spawn_agent\` still does not create or bind a git worktree; instead GSD itself
|
|
@@ -4055,11 +3768,13 @@ Spawn restriction:
|
|
|
4055
3768
|
defaulting to inline execution.
|
|
4056
3769
|
|
|
4057
3770
|
Parallel fan-out:
|
|
4058
|
-
- Spawn multiple agents → collect agent IDs → \`
|
|
3771
|
+
- Spawn multiple agents → collect agent IDs → \`collaboration.wait_agent(timeout_ms=...)\` for each to complete
|
|
3772
|
+
- Do NOT use \`functions.wait(cell_id=...)\` — that is an unrelated exec-cell tool, not the collaboration wait
|
|
4059
3773
|
|
|
4060
3774
|
Result parsing:
|
|
4061
3775
|
- Look for structured markers in agent output: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc.
|
|
4062
|
-
- \`close_agent(id)\` after collecting results
|
|
3776
|
+
- \`close_agent(id)\` after collecting results — but only if \`close_agent\` is visible in the current
|
|
3777
|
+
tool schema (check via \`tool_search\` first, same schema-detection gate as \`spawn_agent\` above)
|
|
4063
3778
|
</codex_skill_adapter>`;
|
|
4064
3779
|
}
|
|
4065
3780
|
|
|
@@ -4109,15 +3824,19 @@ purpose: ${toSingleLine(description)}
|
|
|
4109
3824
|
/**
|
|
4110
3825
|
* #2310 — True if `model` is an Anthropic-flavored value that must never appear as a
|
|
4111
3826
|
* Codex agent `.toml` `model`. Two forms: (a) a bare Claude Agent-tool tier alias
|
|
4112
|
-
* (opus/sonnet/haiku/fable — the canonical CLAUDE_AGENT_ALIASES
|
|
4113
|
-
*
|
|
4114
|
-
*
|
|
4115
|
-
*
|
|
4116
|
-
*
|
|
4117
|
-
*
|
|
3827
|
+
* (opus/sonnet/haiku/fable — the canonical CLAUDE_AGENT_ALIASES); (b) any Claude model
|
|
3828
|
+
* id in any provider namespacing — `claude-*`, `anthropic/claude-*`, `us.anthropic.claude-*`
|
|
3829
|
+
* (the forms the catalog assigns to opencode/hermes/kilo, reachable on a Codex .toml via
|
|
3830
|
+
* the runtime-resolver path). No OpenAI/Codex model id contains "claude", so a
|
|
3831
|
+
* case-insensitive substring test is a safe, exhaustive guard for (b). Codex/ChatGPT
|
|
3832
|
+
* rejects all of these.
|
|
3833
|
+
*
|
|
3834
|
+
* #3241 — thin delegation to the shared predicate on src/model-catalog.cts (moved there
|
|
3835
|
+
* so it can't diverge across Codex-posture surfaces); kept as a local name because it
|
|
3836
|
+
* reads better at the call sites below.
|
|
4118
3837
|
*/
|
|
4119
3838
|
function _isAnthropicFlavoredModel(model) {
|
|
4120
|
-
return
|
|
3839
|
+
return gsdIsAnthropicFlavoredModel(model);
|
|
4121
3840
|
}
|
|
4122
3841
|
|
|
4123
3842
|
// #2310 — dedupe stderr warnings so repeated agent emits don't spam (mirrors the
|
|
@@ -4136,6 +3855,42 @@ function _warnCodexModelOverrideDropped(agentName, value) {
|
|
|
4136
3855
|
);
|
|
4137
3856
|
}
|
|
4138
3857
|
|
|
3858
|
+
// #3241 — one-time per-install deprecation notice: the automatic runtime-resolver
|
|
3859
|
+
// per-tier Codex model embed was removed (D1/D5, ADR-2313 passive-posture epic). When
|
|
3860
|
+
// the resolver *would have* supplied a model and nothing else ends up pinned, this
|
|
3861
|
+
// notice points the user at model_overrides as the explicit-pin replacement. Dedupes
|
|
3862
|
+
// with a module-level boolean (mirrors _codexModelOverrideDroppedWarned's Set above)
|
|
3863
|
+
// so a multi-agent install — every Codex agent hits this condition simultaneously —
|
|
3864
|
+
// emits exactly one line, not one per agent. Reset once per install() call (see
|
|
3865
|
+
// install()) so the "at most once" window is per-install, not per-process. That
|
|
3866
|
+
// reset lives ONLY inside install() (~:10116) — generateCodexAgentToml is also
|
|
3867
|
+
// exported standalone (~:13460), and a caller invoking it directly/repeatedly
|
|
3868
|
+
// outside install() gets process-lifetime dedupe instead of per-install. No
|
|
3869
|
+
// current test depends on the standalone caller's dedupe window.
|
|
3870
|
+
let _codexResolverModelOmittedWarned = false;
|
|
3871
|
+
function _warnCodexResolverModelOmitted() {
|
|
3872
|
+
if (_codexResolverModelOmittedWarned) return;
|
|
3873
|
+
_codexResolverModelOmittedWarned = true;
|
|
3874
|
+
process.stderr.write(
|
|
3875
|
+
'gsd: notice — Codex agents no longer auto-pin a per-tier model from the runtime ' +
|
|
3876
|
+
'resolver; set model_overrides for an agent if you want a specific Codex model ' +
|
|
3877
|
+
'instead of the session model.\n',
|
|
3878
|
+
);
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
// Test seam only — bin/install.js deliberately keeps per-install warning/notice
|
|
3882
|
+
// dedupe in module scope (both the _codexModelOverrideDroppedWarned Set above and
|
|
3883
|
+
// the _codexResolverModelOmittedWarned boolean; install() resets the latter at
|
|
3884
|
+
// ~:10116). A unit test that drives generateCodexAgentToml() directly, without
|
|
3885
|
+
// going through install(), has no other way to reset either store between
|
|
3886
|
+
// assertions without busting the require.cache (which breaks module-instance
|
|
3887
|
+
// sharing with the rest of the suite). This is the single sanctioned way for a
|
|
3888
|
+
// unit test to clear both dedupe stores — exported so tests can call it instead.
|
|
3889
|
+
function _resetCodexWarningDedupeForTests() {
|
|
3890
|
+
_codexModelOverrideDroppedWarned.clear();
|
|
3891
|
+
_codexResolverModelOmittedWarned = false;
|
|
3892
|
+
}
|
|
3893
|
+
|
|
4139
3894
|
/**
|
|
4140
3895
|
* Generate a per-agent .toml config file for Codex.
|
|
4141
3896
|
* Sets required agent metadata, sandbox_mode, and developer_instructions
|
|
@@ -4168,38 +3923,58 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
|
|
|
4168
3923
|
// Embed model override when configured in ~/.gsd/defaults.json so that
|
|
4169
3924
|
// model_overrides is respected on Codex (which uses static TOML, not inline
|
|
4170
3925
|
// Task() model parameters). See #2256.
|
|
4171
|
-
// Precedence: per-agent model_overrides > runtime-aware tier resolution (#2517).
|
|
4172
3926
|
// #2310 — a Codex .toml `model` MUST be a real Codex/OpenAI model id. Codex is a
|
|
4173
3927
|
// passive/session-only model host (ADR-1239): GSD cannot reliably route per-agent
|
|
4174
3928
|
// tiers, and a bare GSD/Claude tier alias (opus/sonnet/haiku/fable) or a claude-*
|
|
4175
3929
|
// id 400s on a ChatGPT-account Codex ("The 'sonnet' model is not supported when
|
|
4176
3930
|
// using Codex with a ChatGPT account"). So: embed ONLY an explicit real-Codex
|
|
4177
3931
|
// model pin from model_overrides; omit anything Anthropic-flavored so the agent
|
|
4178
|
-
// inherits the always-available session model.
|
|
4179
|
-
//
|
|
3932
|
+
// inherits the always-available session model.
|
|
3933
|
+
// #3241 (D1/D5) — the runtime-aware tier-resolver auto-embed that used to fall
|
|
3934
|
+
// through here when model_overrides had nothing was removed: Codex is passive by
|
|
3935
|
+
// default now, and only an explicit model_overrides pin survives. See the
|
|
3936
|
+
// deprecation-notice block below for the population that used to get a pin from
|
|
3937
|
+
// the resolver and no longer does.
|
|
4180
3938
|
const rawModelOverride = modelOverrides?.[resolvedName] || modelOverrides?.[agentName];
|
|
4181
3939
|
let pinnedModel = null;
|
|
4182
3940
|
if (rawModelOverride) {
|
|
4183
|
-
|
|
4184
|
-
|
|
3941
|
+
// Trim before the truthiness test (#3241 defect fix): a whitespace-only value
|
|
3942
|
+
// (e.g. ' ') is a truthy JS string but not a model id — it must not be
|
|
3943
|
+
// embedded verbatim (`model = " "`, a live pre-fix defect) or routed to
|
|
3944
|
+
// _warnCodexModelOverrideDropped, whose "is not a valid Codex model
|
|
3945
|
+
// (Anthropic alias/id)" text would misdescribe a blank config field. It is
|
|
3946
|
+
// silently dropped, matching how '' already behaves (no pin, no warning).
|
|
3947
|
+
const trimmedOverride = typeof rawModelOverride === 'string' ? rawModelOverride.trim() : rawModelOverride;
|
|
3948
|
+
if (typeof trimmedOverride === 'string' && trimmedOverride && !_isAnthropicFlavoredModel(trimmedOverride)) {
|
|
3949
|
+
pinnedModel = trimmedOverride; // explicit real-Codex model pin → embed verbatim (#2256)
|
|
3950
|
+
} else if (typeof rawModelOverride === 'string' && trimmedOverride === '') {
|
|
3951
|
+
// whitespace-only override — no pin, no warning (#3241).
|
|
4185
3952
|
} else {
|
|
4186
3953
|
_warnCodexModelOverrideDropped(resolvedName, rawModelOverride); // alias/claude-* → omit
|
|
4187
3954
|
}
|
|
4188
3955
|
}
|
|
4189
|
-
if (!pinnedModel && runtimeResolver) {
|
|
4190
|
-
// #2517 — runtime-aware tier resolution. Embeds Codex-native model + reasoning_effort
|
|
4191
|
-
// from RUNTIME_PROFILE_MAP / model_profile_overrides for the configured tier.
|
|
4192
|
-
// (Superseded on the default path by the ADR-2310 passive-posture epic.)
|
|
4193
|
-
const entry = runtimeResolver.resolve(resolvedName) || runtimeResolver.resolve(agentName);
|
|
4194
|
-
if (entry?.model) pinnedModel = entry.model;
|
|
4195
|
-
}
|
|
4196
3956
|
// #2310 — final safety gate: never emit an Anthropic-flavored model into a Codex
|
|
4197
|
-
// .toml, even
|
|
4198
|
-
//
|
|
3957
|
+
// .toml, even one that reached here through some other path than the override
|
|
3958
|
+
// check above.
|
|
4199
3959
|
if (pinnedModel && _isAnthropicFlavoredModel(pinnedModel)) {
|
|
4200
3960
|
_warnCodexModelOverrideDropped(resolvedName, pinnedModel);
|
|
4201
3961
|
pinnedModel = null;
|
|
4202
3962
|
}
|
|
3963
|
+
// #3241 — one-time deprecation notice: if nothing ends up pinned but the
|
|
3964
|
+
// runtime resolver would have supplied a per-tier model that would actually
|
|
3965
|
+
// have been EMBEDDED (the population that loses a pin now that the
|
|
3966
|
+
// auto-embed above is gone), point the user at model_overrides. The would-be
|
|
3967
|
+
// model must also clear the #2310 Anthropic-flavored gate above — if it
|
|
3968
|
+
// wouldn't have survived that gate, the user never had that pin pre-Phase-1
|
|
3969
|
+
// either, and the notice would be false. Never fires when the resolver is
|
|
3970
|
+
// null, resolves to nothing, resolves to an Anthropic-flavored model, or an
|
|
3971
|
+
// explicit real-Codex pin survived — in all of those cases nothing was lost.
|
|
3972
|
+
if (!pinnedModel && runtimeResolver) {
|
|
3973
|
+
const wouldHavePinned = runtimeResolver.resolve(resolvedName) || runtimeResolver.resolve(agentName);
|
|
3974
|
+
if (wouldHavePinned?.model && !_isAnthropicFlavoredModel(wouldHavePinned.model)) {
|
|
3975
|
+
_warnCodexResolverModelOmitted();
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
4203
3978
|
let hasPinnedModel = false;
|
|
4204
3979
|
if (pinnedModel) {
|
|
4205
3980
|
lines.push(`model = ${JSON.stringify(pinnedModel)}`);
|
|
@@ -4219,8 +3994,12 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
|
|
|
4219
3994
|
// follows GSD. Keep those knobs coupled unless GSD also pins the model.
|
|
4220
3995
|
if (hasPinnedModel) {
|
|
4221
3996
|
const _universalEffortCodex = resolveInstallTimeEffort(effortCfg, resolvedName !== agentName ? resolvedName : agentName);
|
|
4222
|
-
|
|
4223
|
-
|
|
3997
|
+
// #3533 (10d): 'inherit' means OMIT the pin — the agent follows the host's
|
|
3998
|
+
// own effort default. Never write the literal.
|
|
3999
|
+
if (_universalEffortCodex !== 'inherit') {
|
|
4000
|
+
const _renderedEffortCodex = _getGsdEffortCatalog().renderEffortForRuntime('codex', _universalEffortCodex).value;
|
|
4001
|
+
lines.push(`model_reasoning_effort = ${JSON.stringify(_renderedEffortCodex)}`);
|
|
4002
|
+
}
|
|
4224
4003
|
}
|
|
4225
4004
|
|
|
4226
4005
|
// #774 — Emit service_tier and model_verbosity for light-tier agents.
|
|
@@ -6445,17 +6224,40 @@ function mergeCodexConfig(configPath, gsdBlock) {
|
|
|
6445
6224
|
const normalizedGsdBlock = mergedGsdBlock.replace(/\r?\n/g, eol);
|
|
6446
6225
|
const markerIndex = existing.indexOf(GSD_CODEX_MARKER);
|
|
6447
6226
|
|
|
6448
|
-
// Case 2: Has GSD marker —
|
|
6227
|
+
// Case 2: Has GSD marker — preserve user content on BOTH sides, regenerate the GSD block.
|
|
6228
|
+
//
|
|
6229
|
+
// #2940: the marker delimits where GSD's OWN block begins, NOT where every post-marker byte
|
|
6230
|
+
// is GSD-owned. A fresh install writes the GSD block as the file's entire content, so any
|
|
6231
|
+
// settings the user or Codex CLI later adds ([model], [mcp_servers.*], [profiles.*]) land
|
|
6232
|
+
// AFTER the block. The previous truncate-to-marker logic discarded that trailing region on
|
|
6233
|
+
// every update, destroying user config. The fix routes the trailing region through the
|
|
6234
|
+
// existing AST-based `stripLeakedGsdCodexSections`, which removes GSD's own managed/leaked
|
|
6235
|
+
// sections (the bare [agents] table GSD regenerates, legacy [agents.gsd-*], [[agents]])
|
|
6236
|
+
// while preserving genuine user TOML — so #2406's de-dup still holds AND user content survives.
|
|
6449
6237
|
if (markerIndex !== -1) {
|
|
6450
6238
|
let before = existing.substring(0, markerIndex).trimEnd();
|
|
6451
6239
|
if (before) {
|
|
6452
6240
|
// Strip any GSD-managed sections that leaked above the marker from previous installs
|
|
6453
6241
|
before = stripLeakedGsdCodexSections(before).trimEnd();
|
|
6454
|
-
|
|
6455
|
-
atomicWriteFileSync(configPath, before + eol + eol + normalizedGsdBlock + eol);
|
|
6456
|
-
} else {
|
|
6457
|
-
atomicWriteFileSync(configPath, normalizedGsdBlock + eol);
|
|
6458
6242
|
}
|
|
6243
|
+
// Capture and preserve genuine user content AFTER the GSD-managed region. The whole
|
|
6244
|
+
// post-marker region is passed through stripLeakedGsdCodexSections: GSD's own previously-
|
|
6245
|
+
// emitted [agents] table (regenerated above as normalizedGsdBlock) and any leaked sections
|
|
6246
|
+
// are removed, while user tables ([model], [mcp_servers.*], [profiles.*]) are kept. The
|
|
6247
|
+
// marker comment line itself (and the optional codex_hooks ownership line right under it)
|
|
6248
|
+
// is GSD-owned and is stripped from the trailing region so it is not duplicated alongside
|
|
6249
|
+
// the freshly regenerated block.
|
|
6250
|
+
const rawAfter = existing.substring(markerIndex);
|
|
6251
|
+
const markerStripped = rawAfter
|
|
6252
|
+
.replace(GSD_CODEX_MARKER, '')
|
|
6253
|
+
.replace(/^\r?\n# GSD codex_hooks ownership: (?:section|root_dotted)\r?\n/, '');
|
|
6254
|
+
const afterUser = stripLeakedGsdCodexSections(markerStripped).trim();
|
|
6255
|
+
|
|
6256
|
+
const parts = [];
|
|
6257
|
+
if (before) parts.push(before);
|
|
6258
|
+
parts.push(normalizedGsdBlock);
|
|
6259
|
+
if (afterUser) parts.push(afterUser);
|
|
6260
|
+
atomicWriteFileSync(configPath, parts.join(eol + eol) + eol);
|
|
6459
6261
|
return;
|
|
6460
6262
|
}
|
|
6461
6263
|
|
|
@@ -6756,43 +6558,13 @@ function stripGsdFromCopilotInstructions(content) {
|
|
|
6756
6558
|
const GSD_AGENTS_MD_MARKER = '<!-- GSD Configuration — managed by gsd-core installer -->';
|
|
6757
6559
|
const GSD_AGENTS_MD_CLOSE_MARKER = '<!-- End GSD Configuration -->';
|
|
6758
6560
|
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
}
|
|
6767
|
-
|
|
6768
|
-
/** AGENTS.md body for the cross-tool global instruction target (`~/.agents/AGENTS.md`). */
|
|
6769
|
-
function buildClineAgentsMdBody() {
|
|
6770
|
-
return hooksSurface.buildClineAgentsMdBody();
|
|
6771
|
-
}
|
|
6772
|
-
|
|
6773
|
-
/**
|
|
6774
|
-
* The Cline PreToolUse hook script (issue #787).
|
|
6775
|
-
*
|
|
6776
|
-
* Cline invokes hooks as executable scripts named exactly after the event with
|
|
6777
|
-
* no extension, passing the operation context as JSON on stdin and reading a
|
|
6778
|
-
* JSON decision from stdout ({ cancel, errorMessage, contextModification }).
|
|
6779
|
-
*
|
|
6780
|
-
* This hook is a self-standing planning-artifact guard: it cancels write-class
|
|
6781
|
-
* tool calls that target `.planning/` (GSD-owned artifacts), and otherwise
|
|
6782
|
-
* allows the operation. It FAILS OPEN — any parse/IO error allows the call so a
|
|
6783
|
-
* hook bug can never wedge the user. No dependency on the #782 skills work.
|
|
6784
|
-
*/
|
|
6785
|
-
function buildClinePreToolUseHook() {
|
|
6786
|
-
return hooksSurface.buildClinePreToolUseHook();
|
|
6787
|
-
}
|
|
6788
|
-
|
|
6789
|
-
/**
|
|
6790
|
-
* Merge the GSD AGENTS.md block into an existing file (or create it), preserving
|
|
6791
|
-
* any user content. Mirrors mergeCopilotInstructions: marker-delimited, idempotent.
|
|
6792
|
-
*/
|
|
6793
|
-
function mergeGsdAgentsMd(filePath, gsdContent) {
|
|
6794
|
-
return hooksSurface.mergeGsdAgentsMd(filePath, gsdContent);
|
|
6795
|
-
}
|
|
6561
|
+
// #2876: buildClineRulesBody, buildClineAgentsMdBody, buildClinePreToolUseHook,
|
|
6562
|
+
// and mergeGsdAgentsMd used to be re-bound here as one-line delegates to the
|
|
6563
|
+
// equivalent hooksSurface.* implementations. None had an install.js internal
|
|
6564
|
+
// caller or an export consumer (tests import all four directly from
|
|
6565
|
+
// gsd-core/bin/lib/runtime-hooks-surface.cjs), so the retired bindings were
|
|
6566
|
+
// dead code with no reachable body — removed rather than kept as unreachable
|
|
6567
|
+
// wrappers.
|
|
6796
6568
|
|
|
6797
6569
|
/**
|
|
6798
6570
|
* Strip the GSD block from AGENTS.md content. Returns null if the file became
|
|
@@ -6844,47 +6616,13 @@ function writeClineArtifacts(targetDir, isGlobalInstall) {
|
|
|
6844
6616
|
//
|
|
6845
6617
|
// References: https://cursor.com/docs/hooks
|
|
6846
6618
|
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
return hooksSurface.buildCursorHookEntry(scriptPath);
|
|
6855
|
-
}
|
|
6856
|
-
|
|
6857
|
-
/**
|
|
6858
|
-
* Return true if a Cursor hook entry is GSD-managed.
|
|
6859
|
-
* Detection: presence of the GSD_CURSOR_HOOK_MARKER sentinel field.
|
|
6860
|
-
*
|
|
6861
|
-
* @param {object} entry - A hooks array element from hooks.json
|
|
6862
|
-
* @returns {boolean}
|
|
6863
|
-
*/
|
|
6864
|
-
function isManagedCursorHookEntry(entry) {
|
|
6865
|
-
return hooksSurface.isManagedCursorHookEntry(entry);
|
|
6866
|
-
}
|
|
6867
|
-
|
|
6868
|
-
/**
|
|
6869
|
-
* Reconcile the GSD-managed entries in a Cursor hooks.json file.
|
|
6870
|
-
*
|
|
6871
|
-
* Supports both known hooks.json shapes:
|
|
6872
|
-
* 1) { "version": 1, "hooks": { "sessionStart": [...], "postToolUse": [...] } }
|
|
6873
|
-
* 2) { "sessionStart": [...], "postToolUse": [...] } (no wrapper object)
|
|
6874
|
-
*
|
|
6875
|
-
* Managed entries (those with GSD_CURSOR_HOOK_MARKER) are removed then
|
|
6876
|
-
* re-added if managedEntries is non-null/non-empty. User-owned entries are
|
|
6877
|
-
* preserved. File is written atomically only when content changes.
|
|
6878
|
-
*
|
|
6879
|
-
* @param {string} hooksJsonPath - Absolute path to the hooks.json file
|
|
6880
|
-
* @param {{ sessionStart?: object|null, postToolUse?: object|null }|null} managedEntries
|
|
6881
|
-
* Map from event name to the new hook entry to register (or null to remove).
|
|
6882
|
-
* Pass null for the whole param to remove all managed entries.
|
|
6883
|
-
* @returns {{ changed: boolean, wrote: boolean, path: string }}
|
|
6884
|
-
*/
|
|
6885
|
-
function reconcileCursorHooksJson(hooksJsonPath, managedEntries) {
|
|
6886
|
-
return hooksSurface.reconcileCursorHooksJson(hooksJsonPath, managedEntries);
|
|
6887
|
-
}
|
|
6619
|
+
// #2876: buildCursorHookEntry, isManagedCursorHookEntry, and
|
|
6620
|
+
// reconcileCursorHooksJson used to be re-bound here as one-line delegates to
|
|
6621
|
+
// the equivalent hooksSurface.* implementations. None had an install.js
|
|
6622
|
+
// internal caller or an export consumer (tests import all three directly from
|
|
6623
|
+
// gsd-core/bin/lib/runtime-hooks-surface.cjs), so the retired bindings were
|
|
6624
|
+
// dead code with no reachable body — removed rather than kept as unreachable
|
|
6625
|
+
// wrappers.
|
|
6888
6626
|
|
|
6889
6627
|
/**
|
|
6890
6628
|
* #777 — Write GSD-managed Cursor lifecycle hooks into <targetDir>/hooks.json.
|
|
@@ -6944,23 +6682,13 @@ function removeWindsurfHooksJson(targetDir) {
|
|
|
6944
6682
|
return hooksSurface.removeWindsurfHooksJson(targetDir);
|
|
6945
6683
|
}
|
|
6946
6684
|
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
* run inline (no external script file), so the config can never reference a
|
|
6955
|
-
* hook script that the installer did not also install — it is self-contained
|
|
6956
|
-
* by construction. The command is advisory-only (always exits 0) and orients
|
|
6957
|
-
* the agent toward the project's GSD planning state at session start.
|
|
6958
|
-
*
|
|
6959
|
-
* @returns {object} Copilot hooks-configuration object
|
|
6960
|
-
*/
|
|
6961
|
-
function buildCopilotHookConfig() {
|
|
6962
|
-
return hooksSurface.buildCopilotHookConfig();
|
|
6963
|
-
}
|
|
6685
|
+
// #2876: buildCopilotHookConfig used to be re-bound here as a one-line
|
|
6686
|
+
// delegate to hooksSurface.buildCopilotHookConfig. It had no install.js
|
|
6687
|
+
// internal caller or export consumer (tests import it directly from
|
|
6688
|
+
// gsd-core/bin/lib/runtime-hooks-surface.cjs), so the retired binding was
|
|
6689
|
+
// dead code with no reachable body — removed rather than kept as an
|
|
6690
|
+
// unreachable wrapper. writeCopilotHookConfig below is unaffected — it still
|
|
6691
|
+
// has an internal caller (finishInstall).
|
|
6964
6692
|
|
|
6965
6693
|
/**
|
|
6966
6694
|
* #786 — Write the GSD-managed Copilot lifecycle hook config under the runtime
|
|
@@ -7051,7 +6779,16 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
7051
6779
|
const codexGsdPath = `${path.resolve(targetDir, 'gsd-core').replace(/\\/g, '/')}/`;
|
|
7052
6780
|
|
|
7053
6781
|
for (const file of agentEntries) {
|
|
7054
|
-
|
|
6782
|
+
const agentTomlSourcePath = path.join(agentsSrc, file);
|
|
6783
|
+
let content = fs.readFileSync(agentTomlSourcePath, 'utf8');
|
|
6784
|
+
// #2995 (epic #1671 Phase 6.4): Codex embeds each agent's prompt into a
|
|
6785
|
+
// per-agent `.toml`, reading the source .md independently of the inline
|
|
6786
|
+
// agent loop — a separate emission path that must strip gsd:section
|
|
6787
|
+
// markers too, or a marked agent ships its markers inside the TOML.
|
|
6788
|
+
// Found by the exhaustive per-runtime emission sweep in
|
|
6789
|
+
// tests/agent-fragments-emission.install.test.cjs, not by call-graph
|
|
6790
|
+
// analysis, which is why that guard is behavioral rather than structural.
|
|
6791
|
+
content = composeWorkflow(content, { sourcePath: agentTomlSourcePath });
|
|
7055
6792
|
// Replace full .claude/gsd-core prefix so path resolves to the Codex
|
|
7056
6793
|
// GSD install before generic .claude → .codex conversion rewrites it.
|
|
7057
6794
|
content = content.replace(/~\/\.claude\/gsd-core\//g, codexGsdPath);
|
|
@@ -7480,10 +7217,14 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverrid
|
|
|
7480
7217
|
|
|
7481
7218
|
// convertClaudeCommandToOpencodeFamilySkill, convertClaudeCommandToOpencodeSkill,
|
|
7482
7219
|
// convertClaudeCommandToKiloSkill: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
7483
|
-
//
|
|
7220
|
+
// #2876 found no install.js internal caller for the latter two (tests import
|
|
7221
|
+
// them directly from gsd-core/bin/lib/install-engine.cjs) and retired the
|
|
7222
|
+
// destructured bindings above.
|
|
7484
7223
|
|
|
7485
7224
|
// applyOpencodeFamilyPathPrefix: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
7486
|
-
//
|
|
7225
|
+
// #2876 found no install.js internal caller (never had one either — it was
|
|
7226
|
+
// never part of this module's export surface) and retired the destructured
|
|
7227
|
+
// binding above.
|
|
7487
7228
|
//
|
|
7488
7229
|
// copyFlattenedCommands (OpenCode/Kilo flattened command/ writer): moved to
|
|
7489
7230
|
// src/install-engine.cts as installOpencodeFamilyCommands (ADR-1239 / #2087).
|
|
@@ -7585,13 +7326,13 @@ function writeHermesCategoryDescription(categoryDir) {
|
|
|
7585
7326
|
* @param {boolean} isGlobal - Whether this is a global install
|
|
7586
7327
|
*/
|
|
7587
7328
|
|
|
7588
|
-
// USER_OWNED_ARTIFACTS,
|
|
7589
|
-
//
|
|
7590
|
-
//
|
|
7591
|
-
//
|
|
7592
|
-
//
|
|
7593
|
-
//
|
|
7594
|
-
//
|
|
7329
|
+
// USER_OWNED_ARTIFACTS, migrateLegacyDevPreferencesToSkill, _snapshotDir,
|
|
7330
|
+
// installRuntimeArtifacts, installOpencodeFamilySkills, uninstallRuntimeArtifacts:
|
|
7331
|
+
// ALL moved to src/install-engine.cts (ADR-1239 Phase B). Imported from
|
|
7332
|
+
// installEngine above. _copyStaged, _removeGsdEntries,
|
|
7333
|
+
// _runLegacyInstallMigrations, _runLegacyUninstallCleanup, _restoreDir, and
|
|
7334
|
+
// _removeHermesBareStemDirs moved there too but #2876 found no install.js
|
|
7335
|
+
// internal caller for any of them and retired their destructured bindings.
|
|
7595
7336
|
|
|
7596
7337
|
// ---------------------------------------------------------------------------
|
|
7597
7338
|
// Phase 2 — Layout-driven install/uninstall orchestrators (moved to engine)
|
|
@@ -7668,7 +7409,18 @@ const RUNTIME_CONTENT_DISPATCH = {
|
|
|
7668
7409
|
return `/gsd-${commandName}`;
|
|
7669
7410
|
});
|
|
7670
7411
|
content = content.replace(/\.claude\/skills\//g, '.trae/skills/');
|
|
7671
|
-
|
|
7412
|
+
// #2658: the full dot-claude-slash-prefixed instruction-file path must
|
|
7413
|
+
// be replaced before the bare instruction-filename fallback, or the
|
|
7414
|
+
// bare regex only rewrites that filename and leaves the prefix stale
|
|
7415
|
+
// in place, producing a malformed doubled-prefix path (see the longer
|
|
7416
|
+
// note in convertClaudeToTraeMarkdown above — the instruction filename
|
|
7417
|
+
// and either malformed shape are deliberately never spelled out
|
|
7418
|
+
// contiguously here either, for the same reason: this file ships
|
|
7419
|
+
// verbatim). Both forms target the same concrete file (never a bare
|
|
7420
|
+
// directory), matching the `.md` converter (convertClaudeToTraeMarkdown)
|
|
7421
|
+
// so js/cjs and md content agree on one canonical path.
|
|
7422
|
+
content = content.replace(/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
|
|
7423
|
+
content = content.replace(/CLAUDE\.md/g, '.trae/rules/rules.md');
|
|
7672
7424
|
content = content.replace(/\bClaude Code\b/g, 'Trae');
|
|
7673
7425
|
return content;
|
|
7674
7426
|
},
|
|
@@ -7777,6 +7529,15 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7777
7529
|
const srcPath = path.join(srcDir, entry.name);
|
|
7778
7530
|
const destPath = path.join(destDir, entry.name);
|
|
7779
7531
|
|
|
7532
|
+
// #3333: srcPath was enumerated by readdirSync above, but a filesystem is not
|
|
7533
|
+
// transactional — the file it named can vanish between listing and this read
|
|
7534
|
+
// (a concurrent process, or another test in this suite writing/cleaning up a
|
|
7535
|
+
// fixture inside this same real directory). Treat "gone by the time we get
|
|
7536
|
+
// here" as benign and skip it, never a fatal crash of the whole install.
|
|
7537
|
+
if (!entry.isDirectory() && !fs.existsSync(srcPath)) {
|
|
7538
|
+
continue;
|
|
7539
|
+
}
|
|
7540
|
+
|
|
7780
7541
|
if (entry.isDirectory()) {
|
|
7781
7542
|
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand, isGlobal, confinementRoot);
|
|
7782
7543
|
} else if (entry.name.endsWith('.md')) {
|
|
@@ -7786,6 +7547,42 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7786
7547
|
// Replace ~/.claude/ and $HOME/.claude/ and ./.claude/ with runtime-appropriate paths
|
|
7787
7548
|
// Skip generic replacement for Copilot/Antigravity — their converters handle all paths
|
|
7788
7549
|
let content = fs.readFileSync(srcPath, 'utf8');
|
|
7550
|
+
|
|
7551
|
+
// #2930 (epic #1671 Phase 3): strip `<!-- gsd:section -->` markers
|
|
7552
|
+
// BEFORE any per-runtime rewrite so a `.claude/` -> `.windsurf/` regex
|
|
7553
|
+
// (or any other converter below) never reaches inside a marker
|
|
7554
|
+
// attribute and corrupts it. composeWorkflow is a no-op (byte-identical
|
|
7555
|
+
// return) for the 88+ workflows and every non-workflow .md that carries
|
|
7556
|
+
// no markers, and for a malformed marker it throws loudly naming
|
|
7557
|
+
// srcPath — never emit a half-composed workflow.
|
|
7558
|
+
//
|
|
7559
|
+
// Scoped to gsd-core/workflows/ ONLY (two independent reviewers,
|
|
7560
|
+
// chore/2930): copyWithPathReplacement is the emit path for every .md
|
|
7561
|
+
// under gsd-core/, skills/, and commands/ (see the three call sites),
|
|
7562
|
+
// not just workflows. A doc that merely DOCUMENTS the marker syntax
|
|
7563
|
+
// with an unfenced example (docs/reference/workflow-fragments.md is
|
|
7564
|
+
// the live instance of this class, though not under the install tree
|
|
7565
|
+
// today) would otherwise get silently mis-parsed as a real marker and
|
|
7566
|
+
// that line lossily dropped — a file class issue #2930 never scoped
|
|
7567
|
+
// to. Path is normalized UNCONDITIONALLY (backslash paths arrive on
|
|
7568
|
+
// Linux too — CONTEXT.md path-separator rule) and checked as a
|
|
7569
|
+
// path-segment match so the recursive descent (srcPath may be several
|
|
7570
|
+
// directory levels below gsd-core/workflows/) is still caught.
|
|
7571
|
+
//
|
|
7572
|
+
// #3072: the scoping predicate itself now lives in ONE place —
|
|
7573
|
+
// shouldCompose (src/mcp-catalog.cts, imported above) — rather than
|
|
7574
|
+
// being re-declared inline here. The MCP served catalog calls the
|
|
7575
|
+
// SAME function to decide what it composes vs serves verbatim, so this
|
|
7576
|
+
// install path and the catalog can never independently drift on what
|
|
7577
|
+
// gets composed (ADR-1671:309, DEFECT.GENERATIVE-FIX; the parity gate
|
|
7578
|
+
// is tests/mcp-catalog-parity.test.cjs). shouldCompose normalizes with
|
|
7579
|
+
// the identical unconditional `.replace(/\\/g, '/')` internally, so
|
|
7580
|
+
// this call is behavior-preserving byte-for-behavior with the regex it
|
|
7581
|
+
// replaces.
|
|
7582
|
+
if (shouldCompose(srcPath)) {
|
|
7583
|
+
content = composeWorkflow(content, { sourcePath: srcPath });
|
|
7584
|
+
}
|
|
7585
|
+
|
|
7789
7586
|
if (!dispatch.mdSkipGenericRewrite) {
|
|
7790
7587
|
const globalClaudeRegex = /~\/\.claude\//g;
|
|
7791
7588
|
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
@@ -7793,8 +7590,20 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7793
7590
|
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
7794
7591
|
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
7795
7592
|
content = content.replace(localClaudeRegex, `./${dirName}/`);
|
|
7796
|
-
|
|
7797
|
-
|
|
7593
|
+
// #3544 review (Finding 1 fallout): guarded with the SAME
|
|
7594
|
+
// negative-lookahead convention already used at ~:2859-2860 below
|
|
7595
|
+
// ("preserve .claude-plugin and .claudeignore"). A naive `\b` here
|
|
7596
|
+
// is satisfied by ANY non-word character, including '-' — so for a
|
|
7597
|
+
// --config-dir whose name EXTENDS '.claude' (e.g. '.claude-work',
|
|
7598
|
+
// pathPrefix '$HOME/.claude-work/'), this pass re-matched the
|
|
7599
|
+
// '$HOME/.claude' PREFIX of its own slash-form output (lines above)
|
|
7600
|
+
// and re-appended the full prefix, corrupting every emitted path to
|
|
7601
|
+
// '$HOME/.claude-work-work/...'. Harmless no-op for the literal
|
|
7602
|
+
// default '.claude' (self-replace with an identical string), which
|
|
7603
|
+
// is why this went undetected until a non-default config-dir name
|
|
7604
|
+
// was exercised.
|
|
7605
|
+
content = content.replace(/~\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
|
|
7606
|
+
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
|
|
7798
7607
|
content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
|
|
7799
7608
|
content = content.replace(/~\/\.qwen\//g, pathPrefix);
|
|
7800
7609
|
content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
|
|
@@ -7802,6 +7611,17 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7802
7611
|
content = content.replace(/~\/\.hermes\//g, pathPrefix);
|
|
7803
7612
|
content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
|
|
7804
7613
|
content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
|
|
7614
|
+
// #3544: restore @-file-reference lines to the tilde form Claude Code
|
|
7615
|
+
// actually expands — the SAME correction #3133 already applies to
|
|
7616
|
+
// skill/command bodies via _applyRuntimeRewrites's 'claude' case (see
|
|
7617
|
+
// restoreClaudeGlobalAtRefTilde's doc comment in
|
|
7618
|
+
// runtime-artifact-conversion.cts). This is the gsd-core/ spec-tree
|
|
7619
|
+
// emit path, which never had it: every @~/.claude/gsd-core/… include
|
|
7620
|
+
// in a global install's workflows/references tree silently resolved
|
|
7621
|
+
// to nothing (54 includes across 22 files on a live install).
|
|
7622
|
+
if (runtime === 'claude') {
|
|
7623
|
+
content = runtimeArtifactConversion._restoreClaudeGlobalAtRefTilde(content, pathPrefix);
|
|
7624
|
+
}
|
|
7805
7625
|
}
|
|
7806
7626
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
7807
7627
|
|
|
@@ -8080,6 +7900,23 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8080
7900
|
|
|
8081
7901
|
let removedCount = 0;
|
|
8082
7902
|
|
|
7903
|
+
// #2875 (#1874-F19 anti-inertness, test-matrix C7): recover any user
|
|
7904
|
+
// artifact orphaned by a PRIOR uninstall run that died between staging and
|
|
7905
|
+
// its own restore/discard, BEFORE this run's own preserve steps (sites 2,
|
|
7906
|
+
// 3, 5 below) stage anything new. Uninstall's own gsd-core/ removal and
|
|
7907
|
+
// legacy-commands cleanup are exactly as crash-exposed as install's —
|
|
7908
|
+
// without this, an orphan from a crashed uninstall is recoverable only if
|
|
7909
|
+
// the user later re-installs.
|
|
7910
|
+
// #2875 defect fix: DEGRADE, never abort uninstall, when the staging root
|
|
7911
|
+
// itself cannot be resolved — skip this recovery pass rather than throw
|
|
7912
|
+
// out of uninstall() before it does anything.
|
|
7913
|
+
{
|
|
7914
|
+
const _uninstallEntryStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
7915
|
+
if (_uninstallEntryStagingRoot !== null) {
|
|
7916
|
+
recoverOrphanedUserArtifacts(_uninstallEntryStagingRoot, targetDir);
|
|
7917
|
+
}
|
|
7918
|
+
}
|
|
7919
|
+
|
|
8083
7920
|
// Remove profile marker so a clean reinstall defaults to full surface.
|
|
8084
7921
|
try {
|
|
8085
7922
|
fs.unlinkSync(path.join(targetDir, '.gsd-profile'));
|
|
@@ -8087,7 +7924,16 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8087
7924
|
} catch {}
|
|
8088
7925
|
|
|
8089
7926
|
// 1. Remove GSD commands/skills (layout-driven)
|
|
8090
|
-
|
|
7927
|
+
// #2870: scope id resolved ONCE here and reused below (was two independent
|
|
7928
|
+
// isGlobal-derived re-derivations). Routed through the Install Scope
|
|
7929
|
+
// Module (src/install-scope.cts) when the capability registry is
|
|
7930
|
+
// available; degrades to the plain id on failure (unknown/non-installable
|
|
7931
|
+
// runtime, broken bundle) so this function's scope-id uses — which never
|
|
7932
|
+
// depended on registry availability before this migration — keep working
|
|
7933
|
+
// exactly as they did pre-migration.
|
|
7934
|
+
const _uninstallScopeId = isGlobal ? 'global' : 'local';
|
|
7935
|
+
const _resolvedUninstallScope = _resolveScopeSafe(_uninstallScopeId, runtime);
|
|
7936
|
+
const scope = _resolvedUninstallScope ? _resolvedUninstallScope.id : _uninstallScopeId;
|
|
8091
7937
|
// ADR-1239 / #2086: drive uninstall through the public Host-Integration Interface.
|
|
8092
7938
|
// Fail-open to the engine directly if the composed-registry adapter can't load.
|
|
8093
7939
|
const _uninstallAdapter = _runtimeAdapter(runtime);
|
|
@@ -8168,11 +8014,12 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8168
8014
|
|
|
8169
8015
|
// 1a-kimi. Non-layout Kimi side-effect (#2095 EoS/kimi Upgrade 1): kimi's
|
|
8170
8016
|
// native config.toml lives outside targetDir entirely (resolveKimiHooksTomlDir
|
|
8171
|
-
// resolves ~/.kimi
|
|
8017
|
+
// resolves ~/.kimi for kimi and ~/.kimi-code for kimi-code (#2755), a sibling
|
|
8018
|
+
// of targetDir's ~/.config/agents), so its
|
|
8172
8019
|
// cleanup can't be driven by anything under targetDir the way every other
|
|
8173
8020
|
// hook surface above is.
|
|
8174
8021
|
if (resolveInstallPlan(runtime).hooksSurface === 'kimi-hooks-toml') {
|
|
8175
|
-
const kimiHooksRoot = resolveKimiHooksTomlDir();
|
|
8022
|
+
const kimiHooksRoot = resolveKimiHooksTomlDir({ runtime });
|
|
8176
8023
|
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
8177
8024
|
const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath);
|
|
8178
8025
|
if (kimiHooksCleanup.changed) {
|
|
@@ -8219,23 +8066,24 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8219
8066
|
}
|
|
8220
8067
|
}
|
|
8221
8068
|
|
|
8069
|
+
// #2544: the marker now lives inside kimi's hooks/ dir — remove it
|
|
8070
|
+
// before the emptiness check below, or the dir would never prune.
|
|
8071
|
+
if (removeCommonJsMarker(kimiHooksDir)) {
|
|
8072
|
+
removedCount++;
|
|
8073
|
+
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksDir}`);
|
|
8074
|
+
}
|
|
8075
|
+
|
|
8222
8076
|
try {
|
|
8223
8077
|
if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir);
|
|
8224
8078
|
} catch (_) { /* not empty — leave it */ }
|
|
8225
8079
|
}
|
|
8226
8080
|
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
removedCount++;
|
|
8234
|
-
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot}`);
|
|
8235
|
-
}
|
|
8236
|
-
} catch (e) {
|
|
8237
|
-
// Ignore read errors
|
|
8238
|
-
}
|
|
8081
|
+
// Retire the pre-#2544 marker at kimi's root (~/.kimi), where the bundle
|
|
8082
|
+
// used to write it. Exact content match — a user's own package.json in
|
|
8083
|
+
// kimi's native config home is never touched.
|
|
8084
|
+
if (removeCommonJsMarker(kimiHooksRoot)) {
|
|
8085
|
+
removedCount++;
|
|
8086
|
+
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot} (pre-#2544 marker)`);
|
|
8239
8087
|
}
|
|
8240
8088
|
}
|
|
8241
8089
|
|
|
@@ -8412,18 +8260,42 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8412
8260
|
// Preserve user-owned dev-preferences.md if present (#1423 parity).
|
|
8413
8261
|
const legacyGsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
|
|
8414
8262
|
if (fs.existsSync(legacyGsdCommandsDir)) {
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8263
|
+
// Stage user-owned dev-preferences.md DURABLY before wiping (#2875 /
|
|
8264
|
+
// #1874-F19 "site 7" — found by sweeping bin/install.js for the
|
|
8265
|
+
// read-then-wipe-then-write PATTERN, not for preserveUserArtifacts'
|
|
8266
|
+
// callers; this uninstall-path block open-coded the same round-trip).
|
|
8267
|
+
// #2875 defect fix: DEGRADE, never abort uninstall, when the staging
|
|
8268
|
+
// root cannot be resolved — skip this legacy-cleanup block entirely
|
|
8269
|
+
// (leave the stale dir in place) rather than wipe without a durable
|
|
8270
|
+
// backup for dev-preferences.md.
|
|
8271
|
+
const _legacyGsdCommandsStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
8272
|
+
if (_legacyGsdCommandsStagingRoot !== null) {
|
|
8273
|
+
const stagedDevPrefs = stageUserArtifacts(legacyGsdCommandsDir, ['dev-preferences.md'], _legacyGsdCommandsStagingRoot);
|
|
8274
|
+
// Preserve the ORIGINAL truthy-content check exactly: an existing but
|
|
8275
|
+
// EMPTY dev-preferences.md was (and still is) silently not restored.
|
|
8276
|
+
const savedDevPrefs = stagedDevPrefs.names.includes('dev-preferences.md')
|
|
8277
|
+
? fs.readFileSync(path.join(stagedDevPrefs.filesDir, 'dev-preferences.md'), 'utf8')
|
|
8278
|
+
: null;
|
|
8279
|
+
fs.rmSync(legacyGsdCommandsDir, { recursive: true });
|
|
8280
|
+
removedCount++;
|
|
8281
|
+
console.log(` ${green}✓${reset} Removed legacy commands/gsd/`);
|
|
8282
|
+
if (savedDevPrefs) {
|
|
8283
|
+
try {
|
|
8284
|
+
restoreStagedUserArtifacts(legacyGsdCommandsDir, stagedDevPrefs);
|
|
8285
|
+
discardStagedUserArtifacts(stagedDevPrefs);
|
|
8286
|
+
console.log(` ${green}✓${reset} Preserved commands/gsd/dev-preferences.md`);
|
|
8287
|
+
} catch (err) {
|
|
8288
|
+
console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`);
|
|
8289
|
+
}
|
|
8290
|
+
} else {
|
|
8291
|
+
// #2875 defect fix: an existing-but-EMPTY dev-preferences.md was (and
|
|
8292
|
+
// still is) never restored — the original truthy-content check is
|
|
8293
|
+
// preserved byte-for-byte above — but the staged batch was never
|
|
8294
|
+
// discarded either, leaking a <configDir>/.gsd-staging/ record
|
|
8295
|
+
// forever and re-materializing the just-deleted file on a future
|
|
8296
|
+
// install's orphan-recovery pass. Discard unconditionally when there
|
|
8297
|
+
// is nothing to restore.
|
|
8298
|
+
discardStagedUserArtifacts(stagedDevPrefs);
|
|
8427
8299
|
}
|
|
8428
8300
|
}
|
|
8429
8301
|
}
|
|
@@ -8441,21 +8313,77 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8441
8313
|
// so this is a best-effort guard.
|
|
8442
8314
|
const legacyDir = path.join(targetDir, 'commands', 'gsd');
|
|
8443
8315
|
if (fs.existsSync(legacyDir)) {
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
const
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8316
|
+
// #2875 (#1874-F19): staged DURABLY to disk before the wipe below,
|
|
8317
|
+
// instead of an in-memory Map only — a crash between the wipe and the
|
|
8318
|
+
// restore-on-failure branch below now survives via
|
|
8319
|
+
// recoverOrphanedUserArtifacts on the next run.
|
|
8320
|
+
// #2875 defect fix: DEGRADE, never abort uninstall, when the staging
|
|
8321
|
+
// root cannot be resolved — skip this legacy-migration block entirely
|
|
8322
|
+
// (leave the stale dir in place) rather than wipe without a durable
|
|
8323
|
+
// backup.
|
|
8324
|
+
const stagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
8325
|
+
if (stagingRoot !== null) {
|
|
8326
|
+
const stagedLegacyArtifacts = stageUserArtifacts(legacyDir, ['dev-preferences.md'], stagingRoot);
|
|
8327
|
+
fs.rmSync(legacyDir, { recursive: true });
|
|
8328
|
+
removedCount++;
|
|
8329
|
+
console.log(` ${green}✓${reset} Removed legacy commands/gsd/`);
|
|
8330
|
+
const _uninstallScope = scope;
|
|
8331
|
+
// migrateLegacyDevPreferencesToSkill's Map<string,string> contract is
|
|
8332
|
+
// unchanged — read the staged content back from disk (not an in-memory
|
|
8333
|
+
// value held across the wipe above).
|
|
8334
|
+
//
|
|
8335
|
+
// #2875 defect fix (readFileSync following a staged symlink) — matches
|
|
8336
|
+
// install-engine.cts's _runLegacyInstallMigrations call site 1 exactly:
|
|
8337
|
+
// readFileSync ALWAYS follows a symlink, so a staged artifact that is
|
|
8338
|
+
// itself a symlink (user-artifact-staging.cts's "Symlink safety": a
|
|
8339
|
+
// symlinked user artifact is recreated AS a symlink in the staging
|
|
8340
|
+
// tree, never copied by content) would have its REFERENT's bytes read
|
|
8341
|
+
// here and land in SKILL.md. Excluded from migration below and
|
|
8342
|
+
// restored to its original location unchanged instead.
|
|
8343
|
+
// #2875 defect fix (regression closed — was previously unguarded and
|
|
8344
|
+
// BRICKED uninstall, the very command that should recover from this):
|
|
8345
|
+
// legacyDir was already removed above, so stagedLegacyArtifacts is
|
|
8346
|
+
// the only surviving copy. migrateLegacyDevPreferencesToSkill
|
|
8347
|
+
// correctly THROWS when it finds a planted/dangling symlink at the
|
|
8348
|
+
// skill-file leaf (security fix); a raw `fs.lstatSync` in the loop
|
|
8349
|
+
// below can also throw on a TOCTOU-vanished staged file. Either one,
|
|
8350
|
+
// left unguarded, propagated straight out of uninstall, aborting it
|
|
8351
|
+
// WITHOUT ever reaching the restore-or-discard branch below — the
|
|
8352
|
+
// staged batch was orphaned on disk and every retry hit the same
|
|
8353
|
+
// throw again. Degrade identically to every other #2875 staging step
|
|
8354
|
+
// in this function: catch, warn once, and treat the batch as
|
|
8355
|
+
// unmigrated so the restore branch below always fires.
|
|
8356
|
+
let _legacyMigrated = false;
|
|
8357
|
+
let migratableLegacyNames = [];
|
|
8358
|
+
try {
|
|
8359
|
+
const savedLegacyArtifacts = new Map();
|
|
8360
|
+
for (const name of stagedLegacyArtifacts.names) {
|
|
8361
|
+
const stagedPath = path.join(stagedLegacyArtifacts.filesDir, name);
|
|
8362
|
+
if (fs.lstatSync(stagedPath).isSymbolicLink()) continue;
|
|
8363
|
+
savedLegacyArtifacts.set(name, fs.readFileSync(stagedPath, 'utf8'));
|
|
8364
|
+
migratableLegacyNames.push(name);
|
|
8365
|
+
}
|
|
8366
|
+
_legacyMigrated = migrateLegacyDevPreferencesToSkill(targetDir, savedLegacyArtifacts, runtime, _uninstallScope);
|
|
8367
|
+
} catch (err) {
|
|
8368
|
+
console.warn(` ${yellow}!${reset} dev-preferences.md migration skipped (${err.message}) — restoring the legacy copy instead.`);
|
|
8369
|
+
_legacyMigrated = false;
|
|
8370
|
+
migratableLegacyNames = [];
|
|
8371
|
+
}
|
|
8372
|
+
if (_legacyMigrated && migratableLegacyNames.length === stagedLegacyArtifacts.names.length) {
|
|
8373
|
+
// Compute the actual path written so the log line is accurate per-runtime
|
|
8374
|
+
const _layout = resolveRuntimeArtifactLayout(runtime, targetDir, _uninstallScope);
|
|
8375
|
+
const _sk = _layout.kinds.find((k) => k.kind === 'skills');
|
|
8376
|
+
const _stem = _sk && _sk.prefix === '' ? 'dev-preferences' : 'gsd-dev-preferences';
|
|
8377
|
+
const _skillRelPath = _sk ? `${_sk.destSubpath}/${_stem}/SKILL.md` : 'skills/gsd-dev-preferences/SKILL.md';
|
|
8378
|
+
console.log(` ${green}✓${reset} Migrated dev-preferences.md → ${_skillRelPath} (#2973)`);
|
|
8379
|
+
discardStagedUserArtifacts(stagedLegacyArtifacts);
|
|
8380
|
+
} else {
|
|
8381
|
+
// Migration failed, already exists, or a symlinked name was excluded
|
|
8382
|
+
// above — restore the WHOLE batch to the legacy location so no user
|
|
8383
|
+
// content is silently lost.
|
|
8384
|
+
restoreStagedUserArtifacts(legacyDir, stagedLegacyArtifacts);
|
|
8385
|
+
discardStagedUserArtifacts(stagedLegacyArtifacts);
|
|
8386
|
+
}
|
|
8459
8387
|
}
|
|
8460
8388
|
}
|
|
8461
8389
|
}
|
|
@@ -8463,22 +8391,49 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8463
8391
|
// 2. Remove gsd-core directory
|
|
8464
8392
|
const gsdDir = path.join(targetDir, 'gsd-core');
|
|
8465
8393
|
if (fs.existsSync(gsdDir)) {
|
|
8466
|
-
//
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8394
|
+
// Stage user-generated files DURABLY to disk before wipe (#1423; #2875 /
|
|
8395
|
+
// #1874-F19 "site 5" — this block open-coded its own preserve/restore
|
|
8396
|
+
// instead of calling preserveUserArtifacts, which is why it was missed
|
|
8397
|
+
// by the original symbol-search measurement).
|
|
8398
|
+
// #2875 defect fix: this IS the core uninstall step (removing gsd-core/)
|
|
8399
|
+
// — unlike the optional legacy-cleanup blocks above, uninstall must
|
|
8400
|
+
// still be able to proceed and actually remove gsd-core/ even when the
|
|
8401
|
+
// staging root cannot be resolved. Degrade by skipping ONLY the
|
|
8402
|
+
// USER-PROFILE.md preserve/restore wrapper (warn), never the removal
|
|
8403
|
+
// itself.
|
|
8404
|
+
const _gsdDirStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
8405
|
+
if (_gsdDirStagingRoot === null) {
|
|
8406
|
+
console.warn(` ${yellow}!${reset} Skipping gsd-core/USER-PROFILE.md preservation (staging unavailable) — it will be lost if present.`);
|
|
8407
|
+
fs.rmSync(gsdDir, { recursive: true });
|
|
8408
|
+
removedCount++;
|
|
8409
|
+
console.log(` ${green}✓${reset} Removed gsd-core/`);
|
|
8410
|
+
} else {
|
|
8411
|
+
const stagedProfile = stageUserArtifacts(gsdDir, USER_OWNED_ARTIFACTS, _gsdDirStagingRoot);
|
|
8412
|
+
// Preserve the ORIGINAL truthy-content check exactly: an existing but
|
|
8413
|
+
// EMPTY USER-PROFILE.md was (and still is) silently not restored —
|
|
8414
|
+
// matching prior behavior byte-for-byte rather than widening scope.
|
|
8415
|
+
const preservedProfile = stagedProfile.names.includes('USER-PROFILE.md')
|
|
8416
|
+
? fs.readFileSync(path.join(stagedProfile.filesDir, 'USER-PROFILE.md'), 'utf8')
|
|
8417
|
+
: null;
|
|
8418
|
+
|
|
8419
|
+
fs.rmSync(gsdDir, { recursive: true });
|
|
8420
|
+
removedCount++;
|
|
8421
|
+
console.log(` ${green}✓${reset} Removed gsd-core/`);
|
|
8473
8422
|
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8423
|
+
// Restore user-generated files
|
|
8424
|
+
if (preservedProfile) {
|
|
8425
|
+
try {
|
|
8426
|
+
restoreStagedUserArtifacts(gsdDir, stagedProfile);
|
|
8427
|
+
discardStagedUserArtifacts(stagedProfile);
|
|
8428
|
+
console.log(` ${green}✓${reset} Preserved gsd-core/USER-PROFILE.md`);
|
|
8429
|
+
} catch (err) {
|
|
8430
|
+
console.error(` ${red}✗${reset} Failed to restore USER-PROFILE.md: ${err.message}`);
|
|
8431
|
+
}
|
|
8432
|
+
} else {
|
|
8433
|
+
// #2875 defect fix: same empty-file orphan leak as the legacy
|
|
8434
|
+
// commands/gsd/ site above — discard the staging batch regardless of
|
|
8435
|
+
// whether the staged content was truthy.
|
|
8436
|
+
discardStagedUserArtifacts(stagedProfile);
|
|
8482
8437
|
}
|
|
8483
8438
|
}
|
|
8484
8439
|
}
|
|
@@ -8501,7 +8456,8 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8501
8456
|
}
|
|
8502
8457
|
|
|
8503
8458
|
// 4. Remove GSD hooks
|
|
8504
|
-
|
|
8459
|
+
// #3023: mirror the install site's descriptor-driven bundle dir name.
|
|
8460
|
+
const hooksDir = path.join(targetDir, resolveSharedHooksDirName(runtime));
|
|
8505
8461
|
if (fs.existsSync(hooksDir)) {
|
|
8506
8462
|
let hookCount = 0;
|
|
8507
8463
|
for (const hook of GSD_UNINSTALL_HOOKS) {
|
|
@@ -8543,13 +8499,18 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8543
8499
|
}
|
|
8544
8500
|
}
|
|
8545
8501
|
|
|
8546
|
-
//
|
|
8547
|
-
//
|
|
8548
|
-
// it
|
|
8549
|
-
//
|
|
8550
|
-
//
|
|
8502
|
+
// Retire the CommonJS marker staged into hooks/. hooks/ is shared space and
|
|
8503
|
+
// is deliberately never rmdir'd here, so the marker must be removed
|
|
8504
|
+
// explicitly or it would be left behind. Removed ONLY when it still carries
|
|
8505
|
+
// GSD's exact content — a user-authored package.json is never deleted.
|
|
8506
|
+
//
|
|
8507
|
+
// #2717 reaches the runtimes that stage .js hooks via dedicated paths
|
|
8508
|
+
// (cursor/windsurf/codex); #2544 reaches the shared-bundle runtimes, whose
|
|
8509
|
+
// marker this PR moves out of the config root and into hooks/. Both land in
|
|
8510
|
+
// the same directory, so one guarded call covers both.
|
|
8551
8511
|
try {
|
|
8552
8512
|
if (hooksSurface.removeCommonJsMarkerIfGsdOwned(hooksDir)) {
|
|
8513
|
+
removedCount++;
|
|
8553
8514
|
console.log(` ${green}✓${reset} Removed GSD hooks/package.json (CommonJS marker)`);
|
|
8554
8515
|
}
|
|
8555
8516
|
} catch { /* best-effort */ }
|
|
@@ -8564,12 +8525,38 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8564
8525
|
if (_np) {
|
|
8565
8526
|
const pluginsDir = path.join(targetDir, _np.dir);
|
|
8566
8527
|
const pluginPath = path.join(pluginsDir, _np.file);
|
|
8528
|
+
// Tracks whether GSD actually removed anything from pluginsDir. The rmdir
|
|
8529
|
+
// below is gated on it: pruning a directory GSD never wrote to is the same
|
|
8530
|
+
// "don't touch territory GSD didn't fill" violation this issue is about,
|
|
8531
|
+
// just inverted — a user-created but empty plugin/ or extensions/ dir is
|
|
8532
|
+
// theirs, and an uninstall that never removed anything has no business
|
|
8533
|
+
// deleting it.
|
|
8534
|
+
let removedFromPluginsDir = false;
|
|
8567
8535
|
if (fs.existsSync(pluginPath)) {
|
|
8568
8536
|
try {
|
|
8569
8537
|
fs.unlinkSync(pluginPath);
|
|
8570
8538
|
removedCount++;
|
|
8539
|
+
removedFromPluginsDir = true;
|
|
8571
8540
|
console.log(` ${green}✓${reset} Removed native plugin adapter (${runtime})`);
|
|
8572
8541
|
} catch (_) { /* best-effort */ }
|
|
8542
|
+
}
|
|
8543
|
+
// #2544: the adapter's CommonJS marker sits beside it. Cleaned up OUTSIDE
|
|
8544
|
+
// the adapter-exists guard above — a partial install (or a hand-deleted
|
|
8545
|
+
// adapter) would otherwise strand GSD's marker forever and keep the dir
|
|
8546
|
+
// from ever pruning. Conditioned on the adapter being GONE, though: if the
|
|
8547
|
+
// unlink above failed, pulling the marker out from under a still-present
|
|
8548
|
+
// CommonJS adapter would leave it unloadable. The exact content match
|
|
8549
|
+
// still leaves any user-authored package.json in place.
|
|
8550
|
+
if (!fs.existsSync(pluginPath) && removeCommonJsMarker(pluginsDir)) {
|
|
8551
|
+
removedCount++;
|
|
8552
|
+
removedFromPluginsDir = true;
|
|
8553
|
+
console.log(` ${green}✓${reset} Removed GSD package.json from ${_np.dir}/`);
|
|
8554
|
+
}
|
|
8555
|
+
// Only prune a dir GSD emptied. Pre-fix this rmdir sat inside the
|
|
8556
|
+
// adapter-exists guard, so it could never fire on a dir GSD had not
|
|
8557
|
+
// written to; hoisting it out to catch the marker-only case must not
|
|
8558
|
+
// silently widen it to "any empty plugin dir".
|
|
8559
|
+
if (removedFromPluginsDir) {
|
|
8573
8560
|
try { fs.rmdirSync(pluginsDir); } catch (_) { /* not empty — user plugins present */ }
|
|
8574
8561
|
}
|
|
8575
8562
|
}
|
|
@@ -8579,13 +8566,8 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8579
8566
|
// Any file NOT in this set is user-owned and must survive uninstall.
|
|
8580
8567
|
// After removing GSD files, attempt to rmdir — if the directory is still
|
|
8581
8568
|
// non-empty (user has custom helpers) it stays; otherwise it goes cleanly.
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
'github-release-notes.cjs', 'lint.cjs', 'new.cjs',
|
|
8585
|
-
'README.md', // documentation only — not user-authored
|
|
8586
|
-
];
|
|
8587
|
-
const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs'];
|
|
8588
|
-
|
|
8569
|
+
// GSD_CHANGESET_FILES / GSD_SCRIPTS_LIB_FILES are module-scoped (#3184) so
|
|
8570
|
+
// tests can assert their parity against the real directory contents.
|
|
8589
8571
|
const changesetUninstallDir = path.join(targetDir, 'scripts', 'changeset');
|
|
8590
8572
|
if (fs.existsSync(changesetUninstallDir)) {
|
|
8591
8573
|
let removedChangeset = 0;
|
|
@@ -8630,19 +8612,14 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8630
8612
|
}
|
|
8631
8613
|
|
|
8632
8614
|
// 5. Remove GSD package.json (CommonJS mode marker)
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
console.log(` ${green}✓${reset} Removed GSD package.json`);
|
|
8642
|
-
}
|
|
8643
|
-
} catch (e) {
|
|
8644
|
-
// Ignore read errors
|
|
8645
|
-
}
|
|
8615
|
+
// Since #2544 the marker is staged into hooks/ (and the nativePlugin dir,
|
|
8616
|
+
// handled at 4z above) rather than at targetDir. The targetDir removal is
|
|
8617
|
+
// retained to retire the marker written by pre-#2544 installs — same exact
|
|
8618
|
+
// content match as before, so a user-authored package.json is still never
|
|
8619
|
+
// touched.
|
|
8620
|
+
if (removeCommonJsMarker(targetDir)) {
|
|
8621
|
+
removedCount++;
|
|
8622
|
+
console.log(` ${green}✓${reset} Removed GSD package.json (pre-#2544 config-root marker)`);
|
|
8646
8623
|
}
|
|
8647
8624
|
|
|
8648
8625
|
// 6. Clean up settings.json (remove GSD hooks and statusline)
|
|
@@ -9467,23 +9444,41 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9467
9444
|
// so the manifest records what's actually on disk. _resolveSkillsRootDir already
|
|
9468
9445
|
// resolves destSubpath (which includes hermes's 'skills/gsd' nesting) — do not
|
|
9469
9446
|
// re-append 'gsd' or the hermes dir gets double-nested to skills/gsd/gsd.
|
|
9470
|
-
|
|
9447
|
+
// #2872 (ADR-2866 Phase 3): the scope used to pick the skills root and the
|
|
9448
|
+
// scope RECORDED in the manifest are one value, resolved once. Two reads of
|
|
9449
|
+
// `options.scope` could drift; one cannot.
|
|
9450
|
+
const resolvedScope = options.scope === 'local' ? 'local' : 'global';
|
|
9451
|
+
const codexSkillsDir = _resolveSkillsRootDir(runtime, configDir, resolvedScope);
|
|
9471
9452
|
const codexSkillsManifestPrefix = _hostBehaviors(runtime).skillsManifestPrefix || 'skills/';
|
|
9472
9453
|
const agentsDir = path.join(configDir, 'agents');
|
|
9473
9454
|
const manifest = {
|
|
9455
|
+
// Schema version of this DOCUMENT (#2872) — distinct from `version`
|
|
9456
|
+
// below, which is the GSD package version. Absent ⇒ a pre-#2872 (v1)
|
|
9457
|
+
// manifest, which readInstallManifest still reads without error and
|
|
9458
|
+
// without requiring a reinstall. Read from the Installer Migration
|
|
9459
|
+
// Module rather than repeated as a second literal: the writer here and
|
|
9460
|
+
// the reader's normalizeManifestVersion are two surfaces over one
|
|
9461
|
+
// constant, and this repo's "generative fix divergence" class is exactly
|
|
9462
|
+
// two such literals drifting apart.
|
|
9463
|
+
manifestVersion: MANIFEST_SCHEMA_VERSION,
|
|
9474
9464
|
version: pkg.version,
|
|
9475
9465
|
timestamp: new Date().toISOString(),
|
|
9476
9466
|
mode: options.mode === 'minimal' ? 'minimal' : 'full',
|
|
9467
|
+
// Recorded so an Installed Surface Resolver can answer "which surfaces
|
|
9468
|
+
// are installed, at which scopes, for which runtimes" without re-deriving
|
|
9469
|
+
// it from the directory it happened to be found in (#2872).
|
|
9470
|
+
runtime,
|
|
9471
|
+
scope: resolvedScope,
|
|
9477
9472
|
files: {},
|
|
9478
9473
|
};
|
|
9479
9474
|
|
|
9480
9475
|
const gsdHashes = generateManifest(gsdDir);
|
|
9481
9476
|
for (const [rel, hash] of Object.entries(gsdHashes)) {
|
|
9482
|
-
// Skip user-owned artifacts (e.g. USER-PROFILE.md). They are
|
|
9483
|
-
//
|
|
9484
|
-
//
|
|
9485
|
-
// as a "local patch"
|
|
9486
|
-
// USER_OWNED_ARTIFACTS at top of file.
|
|
9477
|
+
// Skip user-owned artifacts (e.g. USER-PROFILE.md). They are staged
|
|
9478
|
+
// durably and restored across reinstalls (user-artifact-staging.cts,
|
|
9479
|
+
// #2875) and must NOT be hashed into the manifest — otherwise
|
|
9480
|
+
// saveLocalPatches() would flag every refresh as a "local patch"
|
|
9481
|
+
// (bug #2771). Single source of truth: USER_OWNED_ARTIFACTS at top of file.
|
|
9487
9482
|
if (USER_OWNED_ARTIFACTS.includes(rel)) continue;
|
|
9488
9483
|
manifest.files['gsd-core/' + rel] = hash;
|
|
9489
9484
|
}
|
|
@@ -9574,7 +9569,10 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9574
9569
|
// #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares
|
|
9575
9570
|
// skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed.
|
|
9576
9571
|
if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) {
|
|
9577
|
-
|
|
9572
|
+
// #3023: manifest keys must track the bundle wherever the descriptor put it,
|
|
9573
|
+
// or uninstall/saveLocalPatches silently orphan the tree.
|
|
9574
|
+
const sharedHooksDirName = resolveSharedHooksDirName(runtime);
|
|
9575
|
+
const hooksDir = path.join(configDir, sharedHooksDirName);
|
|
9578
9576
|
if (fs.existsSync(hooksDir)) {
|
|
9579
9577
|
// Drive from INSTALLED_HOOK_FILES (the canonical HOOKS_TO_COPY set from
|
|
9580
9578
|
// scripts/build-hooks.js) rather than a prefix/extension regex, so the
|
|
@@ -9586,7 +9584,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9586
9584
|
for (const hook of INSTALLED_HOOK_FILES) {
|
|
9587
9585
|
const hookPath = path.join(hooksDir, hook);
|
|
9588
9586
|
if (fs.existsSync(hookPath)) {
|
|
9589
|
-
manifest.files['
|
|
9587
|
+
manifest.files[sharedHooksDirName + '/' + hook] = fileHash(hookPath);
|
|
9590
9588
|
}
|
|
9591
9589
|
}
|
|
9592
9590
|
// Track hooks/lib/ helpers so saveLocalPatches() can back up user edits
|
|
@@ -9595,7 +9593,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9595
9593
|
if (fs.existsSync(hooksLibDir)) {
|
|
9596
9594
|
for (const file of fs.readdirSync(hooksLibDir)) {
|
|
9597
9595
|
if (GSD_HOOK_LIB_FILES.includes(file)) {
|
|
9598
|
-
manifest.files['
|
|
9596
|
+
manifest.files[sharedHooksDirName + '/lib/' + file] = fileHash(path.join(hooksLibDir, file));
|
|
9599
9597
|
}
|
|
9600
9598
|
}
|
|
9601
9599
|
}
|
|
@@ -9968,21 +9966,24 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9968
9966
|
// below were removed, leaving isKimi unused in this function (the kimi
|
|
9969
9967
|
// local-install-deferred branch above already reads
|
|
9970
9968
|
// _hostBehaviors(runtime).localInstallDeferred instead of this flag).
|
|
9971
|
-
// #2096: isAntigravity dropped — antigravity
|
|
9972
|
-
//
|
|
9973
|
-
// (the path-rewrite skip and the converter dispatch) were
|
|
9974
|
-
// code; both were removed rather than re-gated on
|
|
9975
|
-
// #
|
|
9976
|
-
// _DESCRIPTOR_AGENTS_RUNTIMES
|
|
9977
|
-
//
|
|
9969
|
+
// #2096: isAntigravity dropped — antigravity's agents were already
|
|
9970
|
+
// descriptor-driven (installRuntimeArtifacts), so its two legacy-agent-loop
|
|
9971
|
+
// branches (the path-rewrite skip and the converter dispatch) were
|
|
9972
|
+
// unreachable dead code; both were removed rather than re-gated on
|
|
9973
|
+
// hostBehaviors. (#2875 Part 2 later deleted that legacy loop and its
|
|
9974
|
+
// `_DESCRIPTOR_AGENTS_RUNTIMES` gate entirely — EVERY runtime is now
|
|
9975
|
+
// descriptor-driven for agents, not just this subset.)
|
|
9976
|
+
// #2098: isCodebuddy dropped — codebuddy's agents were likewise already
|
|
9977
|
+
// descriptor-driven, so its legacy converter-dispatch branch (the
|
|
9978
|
+
// `isCodebuddy` arm calling convertClaudeAgentToCodebuddyAgent) was
|
|
9978
9979
|
// unreachable dead code and was removed rather than re-gated.
|
|
9979
|
-
// #2099: isCopilot dropped — copilot
|
|
9980
|
-
//
|
|
9981
|
-
// the converter dispatch, and the .agent.md destName
|
|
9982
|
-
// unreachable dead code and were removed rather than
|
|
9983
|
-
// .agent.md suffix now lives on
|
|
9984
|
-
// src/install-engine.cts, and the
|
|
9985
|
-
// longer needs `&& !isCopilot`.
|
|
9980
|
+
// #2099: isCopilot dropped — copilot's agents were likewise already
|
|
9981
|
+
// descriptor-driven, so its three legacy-agent-loop branches (the
|
|
9982
|
+
// path-rewrite skip, the converter dispatch, and the .agent.md destName
|
|
9983
|
+
// ternary) were unreachable dead code and were removed rather than
|
|
9984
|
+
// re-gated; the .agent.md suffix now lives on
|
|
9985
|
+
// hostBehaviors.agentFileExtension in src/install-engine.cts, and the
|
|
9986
|
+
// skipSharedHooksInstall check above no longer needs `&& !isCopilot`.
|
|
9986
9987
|
// #2100: isWindsurf dropped — its four former isWindsurf-gated branches
|
|
9987
9988
|
// (legacy .devin/skills/gsd-* cleanup, the #1629 command-bodies copy, the
|
|
9988
9989
|
// workflow-verification report, and the shared-hooks-install exclusion) are
|
|
@@ -9990,14 +9991,20 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9990
9991
|
// hostBehaviors.installsCommandBodiesForWorkflowDelegation,
|
|
9991
9992
|
// hostBehaviors.verificationStyle === 'windsurf-workflows', and
|
|
9992
9993
|
// hostBehaviors.skipSharedHooksInstall respectively; its legacy-agent-loop
|
|
9993
|
-
// converter arm was likewise unreachable dead code (windsurf
|
|
9994
|
-
//
|
|
9994
|
+
// converter arm was likewise unreachable dead code (windsurf's agents were
|
|
9995
|
+
// already descriptor-driven) and was removed above.
|
|
9995
9996
|
// #2101: isZcode dropped — folded onto hostBehaviors.skipSharedHooksInstall.
|
|
9996
9997
|
const { isOpencode, isCodex, isCursor, isAugment, isTrae, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
|
9997
9998
|
const plan = resolveInstallPlan(runtime);
|
|
9998
9999
|
const dirName = getDirName(runtime);
|
|
9999
10000
|
const src = path.join(__dirname, '..');
|
|
10000
10001
|
|
|
10002
|
+
// #3241 — the Codex resolver-model-omitted notice dedupes "at most once", but
|
|
10003
|
+
// scoped per install() call rather than per process — each install() run gets
|
|
10004
|
+
// its own fresh window so a second install (e.g. a second runtime, or a test
|
|
10005
|
+
// re-running install()) can warn again if the same condition recurs.
|
|
10006
|
+
_codexResolverModelOmittedWarned = false;
|
|
10007
|
+
|
|
10001
10008
|
if (_hostBehaviors(runtime).localInstallDeferred && !isGlobal) {
|
|
10002
10009
|
console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 2.`);
|
|
10003
10010
|
console.log(` No .kimi-code/skills or .agents/skills project artifacts were written.`);
|
|
@@ -10015,6 +10022,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10015
10022
|
};
|
|
10016
10023
|
}
|
|
10017
10024
|
|
|
10025
|
+
// #2870: scope id resolved ONCE here and reused at every use below (was 10
|
|
10026
|
+
// independent isGlobal-derived re-derivations). Placed AFTER the kimi
|
|
10027
|
+
// local-deferred early return above so that return path does no extra
|
|
10028
|
+
// work. Routed through the Install Scope Module (src/install-scope.cts)
|
|
10029
|
+
// when the capability registry is available; degrades to the plain id on
|
|
10030
|
+
// failure (unknown/non-installable runtime, broken bundle) so this
|
|
10031
|
+
// function's plain scope-id uses — which never depended on registry
|
|
10032
|
+
// availability before this migration — keep working exactly as they did
|
|
10033
|
+
// pre-migration. `_installScope` (the full resolved value, not just the
|
|
10034
|
+
// id) additionally backs the settingsFileByScope routing below.
|
|
10035
|
+
const _installScopeId = isGlobal ? 'global' : 'local';
|
|
10036
|
+
const _installScope = _resolveScopeSafe(_installScopeId, runtime);
|
|
10037
|
+
|
|
10018
10038
|
// Reusable helper to copy hooks/lib/ (git-cmd.js + gsd-graphify-rebuild.sh).
|
|
10019
10039
|
// Defined early so it is visible to both the main and Codex code paths.
|
|
10020
10040
|
// `allowlist` (when non-empty) restricts copying to the named top-level entries,
|
|
@@ -10060,6 +10080,26 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10060
10080
|
? process.cwd()
|
|
10061
10081
|
: path.join(process.cwd(), dirName);
|
|
10062
10082
|
|
|
10083
|
+
// #2875 (#1874-F19 anti-inertness, test-matrix C7): recover any user
|
|
10084
|
+
// artifact orphaned by a PRIOR install run that died between staging and
|
|
10085
|
+
// its own restore/discard, BEFORE this run's own preserve step stages
|
|
10086
|
+
// anything new. This is the production entry point every install() call
|
|
10087
|
+
// reaches — the only place this phase's durability fix is complete rather
|
|
10088
|
+
// than merely callable (40-design.md "The inertness trap this design must
|
|
10089
|
+
// avoid" / #1879-F15). Runs for every runtime, ahead of both the
|
|
10090
|
+
// layout-driven path's _runLegacyInstallMigrations (site 1, inside
|
|
10091
|
+
// installRuntimeArtifacts) and this function's own mainline gsd-core copy
|
|
10092
|
+
// (site 4, below).
|
|
10093
|
+
// #2875 defect fix: DEGRADE, never abort install, when the staging root
|
|
10094
|
+
// itself cannot be resolved — skip this recovery pass rather than throw
|
|
10095
|
+
// out of install() before it does anything.
|
|
10096
|
+
{
|
|
10097
|
+
const _installEntryStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
10098
|
+
if (_installEntryStagingRoot !== null) {
|
|
10099
|
+
recoverOrphanedUserArtifacts(_installEntryStagingRoot, targetDir);
|
|
10100
|
+
}
|
|
10101
|
+
}
|
|
10102
|
+
|
|
10063
10103
|
const locationLabel = isGlobal
|
|
10064
10104
|
? targetDir.replace(os.homedir(), '~')
|
|
10065
10105
|
: targetDir.replace(process.cwd(), '.');
|
|
@@ -10212,7 +10252,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10212
10252
|
const codexPreInstallAgentContents = new Map();
|
|
10213
10253
|
let codexPreInstallVersionBytes = null;
|
|
10214
10254
|
if (_hostBehaviors(runtime).tomlConfigInstall && !isMinimalMode(_effectiveInstallMode)) {
|
|
10215
|
-
const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir,
|
|
10255
|
+
const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
|
|
10216
10256
|
if (fs.existsSync(_preSkillsDir)) {
|
|
10217
10257
|
for (const entry of fs.readdirSync(_preSkillsDir, { withFileTypes: true })) {
|
|
10218
10258
|
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
|
@@ -10268,7 +10308,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10268
10308
|
const _codexPreConfigRollback = !_hostBehaviors(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
|
|
10269
10309
|
rollbackInstallerMigrations();
|
|
10270
10310
|
// skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
|
|
10271
|
-
const _earlySkillsDir = _resolveSkillsRootDir(runtime, targetDir,
|
|
10311
|
+
const _earlySkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
|
|
10272
10312
|
for (const skillName of codexPreInstallSkillNames) {
|
|
10273
10313
|
const skillDirPath = path.join(_earlySkillsDir, skillName);
|
|
10274
10314
|
const fileMap = codexPreInstallSkillContents.get(skillName);
|
|
@@ -10366,7 +10406,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10366
10406
|
installerMigrationResult = runInstallerMigrations({
|
|
10367
10407
|
configDir: targetDir,
|
|
10368
10408
|
runtime,
|
|
10369
|
-
scope:
|
|
10409
|
+
scope: _installScopeId,
|
|
10370
10410
|
migrations: options.installerMigrations,
|
|
10371
10411
|
baselineScan: true,
|
|
10372
10412
|
});
|
|
@@ -10451,6 +10491,17 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10451
10491
|
// (copyWithPathReplacement + stale-skills cleanup).
|
|
10452
10492
|
const _isSkillsRuntime = (() => {
|
|
10453
10493
|
if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086)
|
|
10494
|
+
// #2875 Part 2 defect fix: a runtime whose LOCAL commands are embedded in a
|
|
10495
|
+
// rules file rather than materialized as files (hostBehaviors.localCommandsViaRules
|
|
10496
|
+
// — cline is the only declarant, capabilities/cline/capability.json) must not
|
|
10497
|
+
// flip into this skills/commands-reporting branch merely because its local
|
|
10498
|
+
// artifactLayout now also declares an `agents` kind (#2875 Part 2 cline-local
|
|
10499
|
+
// agents regression fix). That branch's own verification reporting expects a
|
|
10500
|
+
// skills/ or commands/ directory this runtime never writes locally and would
|
|
10501
|
+
// spuriously fail; the `localCommandsViaRules` branch below (unchanged
|
|
10502
|
+
// messaging) and the unconditional agents-materialization block further down
|
|
10503
|
+
// (installAgentsKindStandalone) already cover this runtime/scope correctly.
|
|
10504
|
+
if (!isGlobal && _hostBehaviors(runtime).localCommandsViaRules) return false;
|
|
10454
10505
|
const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
|
|
10455
10506
|
const layout = cap && cap.runtime && cap.runtime.artifactLayout;
|
|
10456
10507
|
if (!layout) return false;
|
|
@@ -10499,7 +10550,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10499
10550
|
|
|
10500
10551
|
if (_isSkillsRuntime) {
|
|
10501
10552
|
// Layout-driven install for skills-based runtimes (full and minimal modes)
|
|
10502
|
-
const scope =
|
|
10553
|
+
const scope = _installScopeId;
|
|
10503
10554
|
// ADR-1239 upgrade 3 / #2088: a kind may declare an alternate install `home`
|
|
10504
10555
|
// (e.g. Codex skills -> $HOME/.agents/skills) instead of the runtime's normal
|
|
10505
10556
|
// configDir. Resolve the ACTUAL on-disk skills root here, descriptor-driven
|
|
@@ -10655,8 +10706,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10655
10706
|
}
|
|
10656
10707
|
}
|
|
10657
10708
|
|
|
10658
|
-
// Descriptor-driven commands/ output report (
|
|
10659
|
-
//
|
|
10709
|
+
// Descriptor-driven commands/ output report (currently CodeBuddy).
|
|
10710
|
+
// Cursor retired this parallel surface in #2644 because its skills are
|
|
10711
|
+
// already slash-menu entries as well as model-invocable context.
|
|
10660
10712
|
if (_hostBehaviors(runtime).reportCommandsDir) {
|
|
10661
10713
|
const commandsDir = path.join(targetDir, 'commands');
|
|
10662
10714
|
if (fs.existsSync(commandsDir)) {
|
|
@@ -10734,15 +10786,34 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10734
10786
|
// that used the namespaced layout (wrote bare-name files under commands/gsd/).
|
|
10735
10787
|
const legacyGsdDir = path.join(commandsDir, 'gsd');
|
|
10736
10788
|
if (fs.existsSync(legacyGsdDir)) {
|
|
10737
|
-
//
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10789
|
+
// Stage user-owned dev-preferences.md DURABLY before wiping (#2875 /
|
|
10790
|
+
// #1874-F19 "site 6" — this Claude commands-install path open-coded
|
|
10791
|
+
// its own preserve/restore instead of calling preserveUserArtifacts,
|
|
10792
|
+
// found by sweeping for the read-then-wipe-then-write PATTERN rather
|
|
10793
|
+
// than for that helper's callers).
|
|
10794
|
+
// #2875 defect fix: DEGRADE, never abort install, when the staging
|
|
10795
|
+
// root cannot be resolved — skip this legacy-migration block entirely
|
|
10796
|
+
// (leave the stale dir in place) rather than wipe without a durable
|
|
10797
|
+
// backup.
|
|
10798
|
+
const _legacyGsdStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
10799
|
+
if (_legacyGsdStagingRoot !== null) {
|
|
10800
|
+
const stagedDevPrefs = stageUserArtifacts(legacyGsdDir, ['dev-preferences.md'], _legacyGsdStagingRoot);
|
|
10801
|
+
// Preserve the ORIGINAL truthy-content check exactly: an existing but
|
|
10802
|
+
// EMPTY dev-preferences.md was (and still is) silently not migrated —
|
|
10803
|
+
// matching prior behavior byte-for-byte rather than widening scope.
|
|
10804
|
+
const preservedDevPrefs = stagedDevPrefs.names.includes('dev-preferences.md')
|
|
10805
|
+
? fs.readFileSync(path.join(stagedDevPrefs.filesDir, 'dev-preferences.md'), 'utf8')
|
|
10806
|
+
: null;
|
|
10807
|
+
fs.rmSync(legacyGsdDir, { recursive: true });
|
|
10808
|
+
console.log(` ${green}✓${reset} Removed legacy commands/gsd/ (migrated to flat gsd-<cmd>.md layout)`);
|
|
10809
|
+
if (preservedDevPrefs) {
|
|
10810
|
+
// Migrate dev-preferences to the new flat form — a RENAME on
|
|
10811
|
+
// restore (staged as 'dev-preferences.md', restored as
|
|
10812
|
+
// 'gsd-dev-preferences.md'), not a round-trip.
|
|
10813
|
+
restoreStagedUserArtifacts(commandsDir, stagedDevPrefs, { rename: { 'dev-preferences.md': 'gsd-dev-preferences.md' } });
|
|
10814
|
+
console.log(` ${green}✓${reset} Migrated dev-preferences.md to commands/gsd-dev-preferences.md`);
|
|
10815
|
+
}
|
|
10816
|
+
discardStagedUserArtifacts(stagedDevPrefs);
|
|
10746
10817
|
}
|
|
10747
10818
|
}
|
|
10748
10819
|
|
|
@@ -10773,12 +10844,29 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10773
10844
|
}
|
|
10774
10845
|
|
|
10775
10846
|
// Copy gsd-core skill with path replacement
|
|
10776
|
-
//
|
|
10847
|
+
// Stage user-generated files DURABLY to disk before the wipe-and-copy so
|
|
10848
|
+
// they survive re-install even if the process dies mid-copy (#2875 /
|
|
10849
|
+
// #1874-F19) — copyWithPathReplacement wipes and recursively re-copies the
|
|
10850
|
+
// entire gsd-core/ tree, the single longest operation in the install, on
|
|
10851
|
+
// the path every user takes (40-design.md "Site 4 is far worse...").
|
|
10777
10852
|
const skillSrc = path.join(src, 'gsd-core');
|
|
10778
10853
|
const skillDest = path.join(targetDir, 'gsd-core');
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10854
|
+
// #2875 defect fix: this IS the mainline install step (installing
|
|
10855
|
+
// gsd-core/ itself) — unlike the optional legacy-cleanup blocks above,
|
|
10856
|
+
// install must still be able to proceed and actually write gsd-core/ even
|
|
10857
|
+
// when the staging root cannot be resolved. Degrade by skipping ONLY the
|
|
10858
|
+
// USER_OWNED_ARTIFACTS preserve/restore wrapper around the copy (warn),
|
|
10859
|
+
// never the copy itself.
|
|
10860
|
+
const _gsdArtifactsStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
10861
|
+
if (_gsdArtifactsStagingRoot === null) {
|
|
10862
|
+
console.warn(` ${yellow}!${reset} Skipping gsd-core/${USER_OWNED_ARTIFACTS.join(', gsd-core/')} preservation (staging unavailable) — it will be lost if present.`);
|
|
10863
|
+
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
|
|
10864
|
+
} else {
|
|
10865
|
+
const stagedGsdArtifacts = stageUserArtifacts(skillDest, USER_OWNED_ARTIFACTS, _gsdArtifactsStagingRoot);
|
|
10866
|
+
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
|
|
10867
|
+
restoreStagedUserArtifacts(skillDest, stagedGsdArtifacts);
|
|
10868
|
+
discardStagedUserArtifacts(stagedGsdArtifacts);
|
|
10869
|
+
}
|
|
10782
10870
|
if (verifyInstalled(skillDest, 'gsd-core')) {
|
|
10783
10871
|
console.log(` ${green}✓${reset} Installed workflow assets`);
|
|
10784
10872
|
} else {
|
|
@@ -10840,214 +10928,89 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10840
10928
|
}
|
|
10841
10929
|
}
|
|
10842
10930
|
|
|
10843
|
-
//
|
|
10844
|
-
//
|
|
10845
|
-
//
|
|
10846
|
-
//
|
|
10847
|
-
//
|
|
10848
|
-
//
|
|
10849
|
-
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
//
|
|
10853
|
-
//
|
|
10854
|
-
//
|
|
10855
|
-
//
|
|
10856
|
-
//
|
|
10857
|
-
//
|
|
10858
|
-
//
|
|
10859
|
-
//
|
|
10860
|
-
//
|
|
10861
|
-
//
|
|
10862
|
-
//
|
|
10863
|
-
//
|
|
10864
|
-
//
|
|
10865
|
-
//
|
|
10866
|
-
//
|
|
10867
|
-
//
|
|
10868
|
-
//
|
|
10869
|
-
//
|
|
10870
|
-
const _DESCRIPTOR_AGENTS_RUNTIMES = new Set(['cursor', 'windsurf', 'augment', 'trae', 'codebuddy', 'copilot', 'antigravity', 'qwen', 'kimi']);
|
|
10871
|
-
|
|
10872
|
-
// Always remove stale gsd-* agents first so re-installing with
|
|
10873
|
-
// `--minimal` actually shrinks a previously-full install.
|
|
10874
|
-
// For Codex this also covers per-agent `.toml` files alongside the `.md`
|
|
10875
|
-
// sources so a full → minimal switch doesn't leave stale registrations.
|
|
10876
|
-
// Skipped for descriptor-agent runtimes (installRuntimeArtifacts prunes) and
|
|
10877
|
-
// for pluginOnlyInstall runtimes (pi, ADR-1239 / #2102 Stage 1 — no agents/
|
|
10878
|
-
// dir is ever written for them, see the leading branch below).
|
|
10879
|
-
if (!_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime) && !_hostBehaviors(runtime).pluginOnlyInstall && fs.existsSync(agentsDest)) {
|
|
10880
|
-
for (const file of fs.readdirSync(agentsDest)) {
|
|
10881
|
-
if (
|
|
10882
|
-
file.startsWith('gsd-') &&
|
|
10883
|
-
(file.endsWith('.md') || (_hostBehaviors(runtime).agentTomlFiles && file.endsWith('.toml')))
|
|
10884
|
-
) {
|
|
10885
|
-
fs.unlinkSync(path.join(agentsDest, file));
|
|
10886
|
-
}
|
|
10887
|
-
}
|
|
10888
|
-
}
|
|
10889
|
-
|
|
10931
|
+
// Agents directory materialization.
|
|
10932
|
+
// #2875 Part 2 (the agents-bypass closure): EVERY runtime is now
|
|
10933
|
+
// descriptor-driven for agents — installRuntimeArtifacts (called earlier in
|
|
10934
|
+
// this function, the `_isSkillsRuntime` branch above) already wrote
|
|
10935
|
+
// agents/ for any runtime whose capability.json declares an `agents` kind,
|
|
10936
|
+
// via convertedAgentsKind/agentsKind (generic layout loop) or
|
|
10937
|
+
// installAgentsKindStandalone (OpenCode/Kilo's combinedFamilyInstall
|
|
10938
|
+
// branch, called from within installOpencodeFamilyArtifacts) — both reuse
|
|
10939
|
+
// the SAME stageAgentsForRuntimeWithConverter pipeline (path-rewrite →
|
|
10940
|
+
// attribution → converter → frontmatter extensions → normalize) the
|
|
10941
|
+
// inline loop this replaces used to hand-roll, and both prune stale gsd-*
|
|
10942
|
+
// entries via their own _removeGsdEntries pass BEFORE copying (broader
|
|
10943
|
+
// than this loop's old extension-gated stale check — see
|
|
10944
|
+
// runtime-artifact-layout.cts's convertedAgentsKind doc comment).
|
|
10945
|
+
// Minimal-mode agent filtering is handled the SAME way it already was for
|
|
10946
|
+
// the ten runtimes cut over before this change: via resolvedProfile.agents
|
|
10947
|
+
// at staging time, not a separate branch here.
|
|
10948
|
+
//
|
|
10949
|
+
// `!_isSkillsRuntime` runtimes (claude-local's legacy-flat local path, and
|
|
10950
|
+
// pi) never reach that loop at all — installAgentsKindStandalone is called
|
|
10951
|
+
// here explicitly to cover them (install-engine.cts's own doc comment
|
|
10952
|
+
// explains why; a regression here was caught by the install-tree golden
|
|
10953
|
+
// fixture, tests/fixtures/install-tree/claude-local.json). It is a no-op
|
|
10954
|
+
// for pi: its capability.json declares an EMPTY artifactLayout for both
|
|
10955
|
+
// scopes (programmatic dispatch, no named-dispatch subagent toolkit, no
|
|
10956
|
+
// host-read markdown surface), so the resolved layout has no `agents` kind
|
|
10957
|
+
// to stage and the function returns `null` without writing anything.
|
|
10890
10958
|
if (_hostBehaviors(runtime).pluginOnlyInstall) {
|
|
10891
|
-
// pi (ADR-1239 / #2102 Stage 1): programmatic dispatch has no named-dispatch
|
|
10892
|
-
// subagent toolkit (dispatch.subagentToolkit: "undocumented", no Agent-tool
|
|
10893
|
-
// equivalent) and no host-read markdown surface — skip writing agents/ entirely.
|
|
10894
10959
|
console.log(` ${green}✓${reset} pi: no subagent files (programmatic dispatch, no named-dispatch toolkit)`);
|
|
10895
|
-
} else if (
|
|
10896
|
-
// installRuntimeArtifacts already wrote agents + handles stale-file cleanup
|
|
10897
|
-
// via its own prune pass. No further action needed.
|
|
10960
|
+
} else if (_isSkillsRuntime) {
|
|
10898
10961
|
console.log(` ${dim}↳${reset} Agents installed via descriptor-driven layout (${runtime})`);
|
|
10899
|
-
} else
|
|
10900
|
-
|
|
10901
|
-
|
|
10902
|
-
|
|
10903
|
-
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
}
|
|
10912
|
-
|
|
10913
|
-
|
|
10962
|
+
} else {
|
|
10963
|
+
const _standaloneAgentsResult = installAgentsKindStandalone(runtime, targetDir, _installScopeId, _resolvedProfile, pathPrefix, getCommitAttribution, _installedCapabilityRegistry);
|
|
10964
|
+
if (_standaloneAgentsResult) {
|
|
10965
|
+
// #2875 defect fix: installAgentsKindStandalone now returns `null`
|
|
10966
|
+
// (rather than a truthy result pointing at an empty destDir) whenever a
|
|
10967
|
+
// restricted (non-'*') resolvedProfile — --minimal being the common
|
|
10968
|
+
// case — legitimately stages ZERO agents, matching the pre-#2875-Part-2
|
|
10969
|
+
// inline loop's behavior of never creating agentsDest under --minimal
|
|
10970
|
+
// at all (see the deleted `isMinimalMode` branch). `destDir` is
|
|
10971
|
+
// therefore guaranteed non-empty whenever we reach this branch, so a
|
|
10972
|
+
// real staging failure still fails loudly via verifyInstalled below.
|
|
10973
|
+
if (verifyInstalled(_standaloneAgentsResult.destDir, 'agents')) {
|
|
10974
|
+
console.log(` ${green}✓${reset} Installed agents`);
|
|
10975
|
+
} else {
|
|
10976
|
+
failures.push('agents');
|
|
10914
10977
|
}
|
|
10915
|
-
}
|
|
10916
|
-
|
|
10917
|
-
} else if (fs.existsSync(agentsSrc)) {
|
|
10918
|
-
fs.mkdirSync(agentsDest, { recursive: true });
|
|
10919
|
-
|
|
10920
|
-
// Copy new agents
|
|
10921
|
-
const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
|
|
10922
|
-
for (const entry of agentEntries) {
|
|
10923
|
-
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
10924
|
-
let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
|
|
10925
|
-
// Replace ~/.claude/ and $HOME/.claude/ as they are the source of truth in the repo
|
|
10926
|
-
const dirRegex = /~\/\.claude\//g;
|
|
10927
|
-
const homeDirRegex = /\$HOME\/\.claude\//g;
|
|
10928
|
-
const bareDirRegex = /~\/\.claude\b/g;
|
|
10929
|
-
const bareHomeDirRegex = /\$HOME\/\.claude\b/g;
|
|
10930
|
-
const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
|
|
10931
|
-
// #2096: `&& !isAntigravity` dropped — antigravity is in
|
|
10932
|
-
// _DESCRIPTOR_AGENTS_RUNTIMES above, so this whole branch is already
|
|
10933
|
-
// unreachable for it; the path-rewrite skip for antigravity now lives
|
|
10934
|
-
// in the descriptor-driven `applyAgentPathRewrites` (hostBehaviors.noPathRewrite).
|
|
10935
|
-
// #2099: `if (!isCopilot)` guard dropped — copilot is ALSO in
|
|
10936
|
-
// _DESCRIPTOR_AGENTS_RUNTIMES (line ~9564 above), so this whole
|
|
10937
|
-
// `else if (fs.existsSync(agentsSrc))` branch is unreachable for it;
|
|
10938
|
-
// isCopilot was therefore always false here, making the guard a no-op.
|
|
10939
|
-
content = content.replace(dirRegex, pathPrefix);
|
|
10940
|
-
content = content.replace(homeDirRegex, pathPrefix);
|
|
10941
|
-
content = content.replace(bareDirRegex, normalizedPathPrefix);
|
|
10942
|
-
content = content.replace(bareHomeDirRegex, normalizedPathPrefix);
|
|
10943
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
10944
|
-
// Convert frontmatter for runtime compatibility (agents need different handling)
|
|
10945
|
-
if (_hostBehaviors(runtime).frontmatterDialect === 'opencode') {
|
|
10946
|
-
// Resolve per-agent model for OpenCode agents.
|
|
10947
|
-
// Precedence: model_overrides[agent] > model_profile_overrides.opencode.<tier> > omit.
|
|
10948
|
-
// model_overrides (#2256): explicit per-agent override, highest precedence.
|
|
10949
|
-
// model_profile_overrides (#2794): tier-based runtime resolver, same parity as Codex.
|
|
10950
|
-
const _ocAgentName = entry.name.replace(/\.md$/, '');
|
|
10951
|
-
const _ocModelOverrides = readGsdEffectiveModelOverrides(targetDir);
|
|
10952
|
-
let _ocModelOverride = _ocModelOverrides?.[_ocAgentName] || null;
|
|
10953
|
-
if (!_ocModelOverride) {
|
|
10954
|
-
// Fall back to tier-based resolution via model_profile_overrides.opencode.<tier>.
|
|
10955
|
-
const _ocRuntimeResolver = readGsdRuntimeProfileResolver(targetDir);
|
|
10956
|
-
if (_ocRuntimeResolver) {
|
|
10957
|
-
const _ocEntry = _ocRuntimeResolver.resolve(_ocAgentName);
|
|
10958
|
-
if (_ocEntry?.model) {
|
|
10959
|
-
_ocModelOverride = _ocEntry.model;
|
|
10960
|
-
}
|
|
10961
|
-
}
|
|
10962
|
-
}
|
|
10963
|
-
content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true, modelOverride: _ocModelOverride });
|
|
10964
|
-
} else if (_hostBehaviors(runtime).frontmatterDialect === 'kilo') {
|
|
10965
|
-
// Resolve per-agent model for Kilo agents (#2093 UPGRADE 2; Kilo is an
|
|
10966
|
-
// OpenCode fork with the same static-frontmatter model constraint).
|
|
10967
|
-
// Precedence: model_overrides[agent] > model_profile_overrides.kilo.<tier> > omit.
|
|
10968
|
-
// model_overrides (#2256): explicit per-agent override, highest precedence.
|
|
10969
|
-
// model_profile_overrides (#2794): tier-based runtime resolver, same parity as OpenCode.
|
|
10970
|
-
const _kiloAgentName = entry.name.replace(/\.md$/, '');
|
|
10971
|
-
const _kiloModelOverrides = readGsdEffectiveModelOverrides(targetDir);
|
|
10972
|
-
let _kiloModelOverride = _kiloModelOverrides?.[_kiloAgentName] || null;
|
|
10973
|
-
if (!_kiloModelOverride) {
|
|
10974
|
-
// Fall back to tier-based resolution via model_profile_overrides.kilo.<tier>.
|
|
10975
|
-
const _kiloRuntimeResolver = readGsdRuntimeProfileResolver(targetDir);
|
|
10976
|
-
if (_kiloRuntimeResolver) {
|
|
10977
|
-
const _kiloEntry = _kiloRuntimeResolver.resolve(_kiloAgentName);
|
|
10978
|
-
if (_kiloEntry?.model) {
|
|
10979
|
-
_kiloModelOverride = _kiloEntry.model;
|
|
10980
|
-
}
|
|
10981
|
-
}
|
|
10982
|
-
}
|
|
10983
|
-
content = convertClaudeToKiloFrontmatter(content, { isAgent: true, modelOverride: _kiloModelOverride });
|
|
10984
|
-
} else if (_hostBehaviors(runtime).frontmatterDialect === 'codex') {
|
|
10985
|
-
content = convertClaudeAgentToCodexAgent(content);
|
|
10986
|
-
// #2099: `else if (isCopilot)` arm dropped — copilot is unreachable
|
|
10987
|
-
// here (see the isCopilot-guard-drop comment above); its content
|
|
10988
|
-
// conversion is applied pre-staging via the descriptor's
|
|
10989
|
-
// artifactLayout.converter (runtime-artifact-layout.cts), independent
|
|
10990
|
-
// of this legacy loop.
|
|
10991
|
-
// #2100: `else if (isWindsurf)` arm dropped — windsurf is ALSO in
|
|
10992
|
-
// _DESCRIPTOR_AGENTS_RUNTIMES (line ~9575 above), so this whole
|
|
10993
|
-
// `else if (fs.existsSync(agentsSrc))` branch is unreachable for it;
|
|
10994
|
-
// isWindsurf was therefore always false here, making the arm dead.
|
|
10995
|
-
// Its content conversion is applied pre-staging via the descriptor's
|
|
10996
|
-
// artifactLayout.converter (convertClaudeAgentToWindsurfAgent),
|
|
10997
|
-
// independent of this legacy loop.
|
|
10998
|
-
} else if (_hostBehaviors(runtime).frontmatterDialect === 'cline') {
|
|
10999
|
-
// Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
|
|
11000
|
-
// hostBehaviors.frontmatterDialect === 'cline'.
|
|
11001
|
-
content = convertClaudeAgentToClineAgent(content);
|
|
11002
|
-
} else if (_hostBehaviors(runtime).brandingRewrites) {
|
|
11003
|
-
// Descriptor-driven (ADR-1239 / #2092): folded from separate
|
|
11004
|
-
// `isQwen` / hermes-hardcoded branches into a single read of
|
|
11005
|
-
// runtime.hostBehaviors.brandingRewrites (qwen -> QWEN.md/Qwen
|
|
11006
|
-
// Code/.qwen/, hermes -> HERMES.md/Hermes Agent/.hermes/).
|
|
11007
|
-
const _b = _hostBehaviors(runtime).brandingRewrites;
|
|
11008
|
-
content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
|
|
11009
|
-
content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
|
|
11010
|
-
content = content.replace(/\.claude\//g, _b['.claude/']);
|
|
11011
|
-
}
|
|
11012
|
-
// #443 — Inject `effort:` into the Claude .md frontmatter ONLY.
|
|
11013
|
-
// OpenCode/Qwen/Hermes also produce .md files but break on
|
|
11014
|
-
// unknown frontmatter keys (the repo bans skills:/permissionMode: for
|
|
11015
|
-
// the same reason — see tests/agent-frontmatter.test.cjs).
|
|
11016
|
-
// Claude Code reads per-subagent `effort:` frontmatter (anthropics/claude-code #31536).
|
|
11017
|
-
// Injection is per-runtime at install time because the canonical source
|
|
11018
|
-
// agents/*.md must stay runtime-safe (no effort: key in source).
|
|
11019
|
-
if ((_hostBehaviors(runtime).agentFrontmatterExtensions || []).includes('effort')) {
|
|
11020
|
-
const _effortCfg = readGsdEffectiveEffortConfig(targetDir);
|
|
11021
|
-
const _agentName = entry.name.replace(/\.md$/, '');
|
|
11022
|
-
const _universalEffort = resolveInstallTimeEffort(_effortCfg, _agentName);
|
|
11023
|
-
const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime(runtime, _universalEffort).value;
|
|
11024
|
-
content = injectEffortFrontmatter(content, _renderedEffort);
|
|
11025
|
-
const _disallowedTools = READONLY_AGENT_DISALLOWED_TOOLS[_agentName];
|
|
11026
|
-
if (_disallowedTools) content = injectDisallowedToolsFrontmatter(content, _disallowedTools);
|
|
11027
|
-
}
|
|
11028
|
-
// #3677 — normalize retired `/gsd:<cmd>` colon refs in the agent body
|
|
11029
|
-
// to the canonical hyphen form `/gsd-<cmd>` for hyphen-`name:`
|
|
11030
|
-
// runtimes (claude / qwen / hermes). Self-converting and
|
|
11031
|
-
// colon-canonical runtimes are skipped by the predicate — see
|
|
11032
|
-
// shouldNormalizeHyphenNamespaceInAgentBody above. Mirrors the
|
|
11033
|
-
// SKILL.md-body fix shipped via #3629.
|
|
11034
|
-
content = normalizeAgentBodyForRuntime(content, runtime, readGsdCommandNames());
|
|
11035
|
-
// #2099: `isCopilot ? ... : entry.name` ternary dropped — copilot is
|
|
11036
|
-
// unreachable here (see the isCopilot-guard-drop comment above), so
|
|
11037
|
-
// the ternary always evaluated to entry.name in practice; its
|
|
11038
|
-
// .agent.md suffix is applied by the descriptor-driven fold in
|
|
11039
|
-
// src/install-engine.cts (hostBehaviors.agentFileExtension).
|
|
11040
|
-
const destName = entry.name;
|
|
11041
|
-
fs.writeFileSync(path.join(agentsDest, destName), content);
|
|
11042
|
-
}
|
|
11043
|
-
}
|
|
11044
|
-
if (verifyInstalled(agentsDest, 'agents')) {
|
|
11045
|
-
console.log(` ${green}✓${reset} Installed agents`);
|
|
10978
|
+
} else if (_resolvedProfile.skills !== '*') {
|
|
10979
|
+
console.log(` ${dim}↳${reset} Skipping agents (${_resolvedProfile.name} profile excludes all agents — run \`gsd update\` with a broader profile to add them)`);
|
|
11046
10980
|
} else {
|
|
11047
|
-
|
|
10981
|
+
console.log(` ${dim}↳${reset} No agents kind declared for ${runtime} at this scope`);
|
|
10982
|
+
}
|
|
10983
|
+
}
|
|
10984
|
+
|
|
10985
|
+
// Codex registers agents in `config.toml` via `[agents.gsd-*]` sections —
|
|
10986
|
+
// NOT agents-directory materialization (design doc "Deliberately not in
|
|
10987
|
+
// scope"), so this stays independent of the agents/ write above. Without
|
|
10988
|
+
// stripping these on a full → minimal reinstall, the runtime would keep
|
|
10989
|
+
// advertising the old full agent surface even though the descriptor-driven
|
|
10990
|
+
// write above already skipped writing the .md files for a minimal-tier
|
|
10991
|
+
// resolvedProfile. Reuse the same helper that powers `--uninstall`.
|
|
10992
|
+
if (isMinimalMode(_effectiveInstallMode) && _hostBehaviors(runtime).tomlConfigInstall) {
|
|
10993
|
+
const codexConfigPath = path.join(targetDir, 'config.toml');
|
|
10994
|
+
if (fs.existsSync(codexConfigPath)) {
|
|
10995
|
+
const existing = fs.readFileSync(codexConfigPath, 'utf8');
|
|
10996
|
+
const cleaned = stripGsdFromCodexConfig(existing);
|
|
10997
|
+
if (cleaned === null) {
|
|
10998
|
+
fs.unlinkSync(codexConfigPath);
|
|
10999
|
+
} else if (cleaned !== existing) {
|
|
11000
|
+
fs.writeFileSync(codexConfigPath, cleaned);
|
|
11001
|
+
}
|
|
11048
11002
|
}
|
|
11049
11003
|
}
|
|
11050
11004
|
|
|
11005
|
+
// agentsSrc is declared as `let` before the enclosing try block (not const)
|
|
11006
|
+
// so it is accessible by installCodexConfig() in the Codex config section
|
|
11007
|
+
// below — that function reads RAW source agents/*.md (not the
|
|
11008
|
+
// descriptor-staged output above) to build Codex's per-agent config.toml
|
|
11009
|
+
// sidecar files, a separate writer this migration deliberately does not
|
|
11010
|
+
// touch (design doc: "Codex's config.toml [agents.gsd-*] strip... is not
|
|
11011
|
+
// agents-directory materialization").
|
|
11012
|
+
agentsSrc = _stageAgents(path.join(src, 'agents'));
|
|
11013
|
+
|
|
11051
11014
|
// Copy CHANGELOG.md
|
|
11052
11015
|
const changelogSrc = path.join(src, 'CHANGELOG.md');
|
|
11053
11016
|
const changelogDest = path.join(targetDir, 'gsd-core', 'CHANGELOG.md');
|
|
@@ -11106,22 +11069,30 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11106
11069
|
// a safe no-op when the dir is already present.
|
|
11107
11070
|
fs.mkdirSync(destRootDir, { recursive: true });
|
|
11108
11071
|
|
|
11109
|
-
//
|
|
11110
|
-
//
|
|
11111
|
-
//
|
|
11112
|
-
const
|
|
11113
|
-
|
|
11114
|
-
|
|
11072
|
+
// #3023: the bundle's directory NAME is descriptor-driven — a host that
|
|
11073
|
+
// reserves `hooks/` (pi) must be able to opt out. Resolved once here so the
|
|
11074
|
+
// stage / lib / marker sites can never disagree about where the bundle is.
|
|
11075
|
+
const sharedHooksDirName = resolveSharedHooksDirName(runtime);
|
|
11076
|
+
|
|
11077
|
+
// #2544: the CommonJS marker is NOT written here (destRootDir is the
|
|
11078
|
+
// runtime's shared config root — user-writable territory on OpenCode and
|
|
11079
|
+
// Kilo, where it is the documented place to declare local-plugin npm
|
|
11080
|
+
// dependencies). It is written into hooks/ below, the directory GSD
|
|
11081
|
+
// creates and fills with its own .js scripts, once that directory exists.
|
|
11115
11082
|
|
|
11116
11083
|
let hooksOk = true;
|
|
11084
|
+
// #2544: true once GSD has actually written into destRootDir/hooks/, which
|
|
11085
|
+
// is what licenses the CommonJS marker below.
|
|
11086
|
+
let stagedHooks = false;
|
|
11117
11087
|
|
|
11118
11088
|
// Copy hooks from dist/ (bundled with dependencies)
|
|
11119
11089
|
// Template paths for the target runtime (replaces '.claude' with correct config dir)
|
|
11120
11090
|
const hooksSrc = path.join(src, 'hooks', 'dist');
|
|
11121
11091
|
if (fs.existsSync(hooksSrc)) {
|
|
11122
|
-
const hooksDest = path.join(destRootDir,
|
|
11092
|
+
const hooksDest = path.join(destRootDir, sharedHooksDirName);
|
|
11123
11093
|
fs.mkdirSync(hooksDest, { recursive: true });
|
|
11124
11094
|
const hookEntries = fs.readdirSync(hooksSrc);
|
|
11095
|
+
if (hookEntries.some((e) => fs.statSync(path.join(hooksSrc, e)).isFile())) stagedHooks = true;
|
|
11125
11096
|
const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
|
|
11126
11097
|
for (const entry of hookEntries) {
|
|
11127
11098
|
const srcFile = path.join(hooksSrc, entry);
|
|
@@ -11184,7 +11155,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11184
11155
|
}
|
|
11185
11156
|
}
|
|
11186
11157
|
if (verifyInstalled(hooksDest, 'hooks')) {
|
|
11187
|
-
console.log(` ${green}✓${reset} Installed
|
|
11158
|
+
console.log(` ${green}✓${reset} Installed ${sharedHooksDirName} (bundled)`);
|
|
11188
11159
|
// Warn if expected community .sh hooks are missing (non-fatal)
|
|
11189
11160
|
const expectedShHooks = ['gsd-session-state.sh', 'gsd-validate-commit.sh', 'gsd-phase-boundary.sh', 'gsd-graphify-update.sh'];
|
|
11190
11161
|
for (const sh of expectedShHooks) {
|
|
@@ -11213,10 +11184,48 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11213
11184
|
// below; this helper itself only checks source presence.)
|
|
11214
11185
|
const hooksLibSrc = path.join(src, 'hooks', 'lib');
|
|
11215
11186
|
if (fs.existsSync(hooksLibSrc)) {
|
|
11216
|
-
const hooksLibDest = path.join(destRootDir,
|
|
11187
|
+
const hooksLibDest = path.join(destRootDir, sharedHooksDirName, 'lib');
|
|
11217
11188
|
fs.mkdirSync(hooksLibDest, { recursive: true });
|
|
11218
11189
|
copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);
|
|
11219
|
-
|
|
11190
|
+
if (GSD_HOOK_LIB_FILES.some((f) => fs.existsSync(path.join(hooksLibDest, f)))) stagedHooks = true;
|
|
11191
|
+
console.log(` ${green}✓${reset} Installed ${sharedHooksDirName}/lib/ helpers (git-cmd, graphify-rebuild, ...)`);
|
|
11192
|
+
}
|
|
11193
|
+
|
|
11194
|
+
// #2544: pin the staged hook scripts to CommonJS from inside hooks/ — the
|
|
11195
|
+
// directory GSD just created and filled — instead of from destRootDir.
|
|
11196
|
+
// Scoping the marker to GSD's own directory keeps `require` working in
|
|
11197
|
+
// hooks/*.js and hooks/lib/*.js under any ambient "type": "module", while
|
|
11198
|
+
// leaving the shared config root untouched.
|
|
11199
|
+
//
|
|
11200
|
+
// Gated on `stagedHooks`, NOT on the directory merely existing: hooks/ is
|
|
11201
|
+
// shared space, so an existence check would drop a GSD marker into a
|
|
11202
|
+
// hooks/ directory the user created and GSD never wrote to — the same
|
|
11203
|
+
// write-into-someone-else's-territory this issue is about. And never
|
|
11204
|
+
// written over a package.json GSD does not own.
|
|
11205
|
+
//
|
|
11206
|
+
// ALSO gated on `hooksOk`: `stagedHooks` is computed from the SOURCE
|
|
11207
|
+
// listing before the copy loop, so it stays true when the copies land but
|
|
11208
|
+
// `verifyInstalled` then fails. Marking a hooks/ GSD did not successfully
|
|
11209
|
+
// populate as CommonJS claims an ownership the install did not earn — the
|
|
11210
|
+
// two flags answer different questions ("did we intend to fill it" vs "is
|
|
11211
|
+
// it actually filled"), and the marker needs both.
|
|
11212
|
+
const hooksMarkerDir = path.join(destRootDir, sharedHooksDirName);
|
|
11213
|
+
if (stagedHooks && hooksOk) {
|
|
11214
|
+
switch (ensureCommonJsMarker(hooksMarkerDir)) {
|
|
11215
|
+
case 'written':
|
|
11216
|
+
console.log(` ${green}✓${reset} Wrote ${sharedHooksDirName}/package.json (CommonJS mode)`);
|
|
11217
|
+
break;
|
|
11218
|
+
case 'preserved-foreign':
|
|
11219
|
+
console.warn(` ${yellow}⚠${reset} Left existing ${sharedHooksDirName}/package.json untouched (not GSD's marker) — GSD hooks may not resolve as CommonJS`);
|
|
11220
|
+
break;
|
|
11221
|
+
case 'failed':
|
|
11222
|
+
// Best-effort: a read-only or full config dir must not abort the
|
|
11223
|
+
// install with a raw stack trace. The hooks themselves are staged.
|
|
11224
|
+
console.warn(` ${yellow}⚠${reset} Could not write ${sharedHooksDirName}/package.json (CommonJS mode) — install continued; GSD hooks may not resolve as CommonJS`);
|
|
11225
|
+
break;
|
|
11226
|
+
default:
|
|
11227
|
+
break;
|
|
11228
|
+
}
|
|
11220
11229
|
}
|
|
11221
11230
|
|
|
11222
11231
|
return hooksOk;
|
|
@@ -11377,12 +11386,35 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11377
11386
|
}
|
|
11378
11387
|
|
|
11379
11388
|
// Write file manifest for future modification detection
|
|
11380
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope:
|
|
11389
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
11381
11390
|
console.log(` ${green}✓${reset} Wrote file manifest (${MANIFEST_NAME})`);
|
|
11382
11391
|
|
|
11383
11392
|
// Report any backed-up local patches
|
|
11384
11393
|
reportLocalPatches(targetDir, runtime);
|
|
11385
11394
|
|
|
11395
|
+
// #2873: cross-scope shadow report. Fires ONCE per install (this is the
|
|
11396
|
+
// only writeManifest call site that gets it — the other four sites are
|
|
11397
|
+
// sub-writes within a single install, not separate installs). A shadowed
|
|
11398
|
+
// install is a warning, never a failure (ADR-2866 Consequences), so this
|
|
11399
|
+
// never touches `failures` or `process.exit`, and the whole block is
|
|
11400
|
+
// wrapped in a try/catch that swallows everything: a report failure must
|
|
11401
|
+
// never fail an otherwise-successful install (design row C5). No options
|
|
11402
|
+
// are injected into buildShadowReport — this is the production call shape,
|
|
11403
|
+
// resolving the real machine via os.homedir()/process.cwd() defaults
|
|
11404
|
+
// inside the resolver.
|
|
11405
|
+
try {
|
|
11406
|
+
const shadowReport = buildShadowReport(runtime);
|
|
11407
|
+
const shadowLines = renderShadowReport(shadowReport);
|
|
11408
|
+
if (shadowLines.length > 0) {
|
|
11409
|
+
console.warn(`\n ${yellow}⚠${reset} ${shadowLines[0]}`);
|
|
11410
|
+
for (const line of shadowLines.slice(1)) {
|
|
11411
|
+
console.warn(` ${dim}${line}${reset}`);
|
|
11412
|
+
}
|
|
11413
|
+
}
|
|
11414
|
+
} catch (_shadowReportErr) {
|
|
11415
|
+
// Never fail an install over a reporting concern — see comment above.
|
|
11416
|
+
}
|
|
11417
|
+
|
|
11386
11418
|
// Verify no leaked .claude paths in non-Claude runtimes (manifest-scoped)
|
|
11387
11419
|
if (!_hostBehaviors(runtime).ownsClaudePaths) {
|
|
11388
11420
|
const leakedPaths = [];
|
|
@@ -11528,7 +11560,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11528
11560
|
// (copyCommandsAsCodexSkills removes pre-existing gsd-* dirs before re-writing)
|
|
11529
11561
|
// are restored even when they are absent from disk at rollback time (#3245 CR).
|
|
11530
11562
|
// • Dirs that did not pre-exist: remove entirely.
|
|
11531
|
-
const _rollbackSkillsDir = _resolveSkillsRootDir(runtime, targetDir,
|
|
11563
|
+
const _rollbackSkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
|
|
11532
11564
|
// Pass 1 — restore snapshot entries (may be absent from disk if deleted mid-install).
|
|
11533
11565
|
for (const skillName of codexPreInstallSkillNames) {
|
|
11534
11566
|
const skillDirPath = path.join(_rollbackSkillsDir, skillName);
|
|
@@ -11643,7 +11675,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11643
11675
|
// Re-write the manifest now that .toml agent files exist on disk.
|
|
11644
11676
|
// The initial writeManifest call (before Codex config generation) could
|
|
11645
11677
|
// not include agents/gsd-*.toml because those files did not yet exist.
|
|
11646
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope:
|
|
11678
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
11647
11679
|
} else {
|
|
11648
11680
|
console.log(` ${dim}↳${reset} Skipping Codex agent config generation (minimal install)`);
|
|
11649
11681
|
}
|
|
@@ -11667,6 +11699,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11667
11699
|
const codexHooksDest = path.join(targetDir, 'hooks');
|
|
11668
11700
|
fs.mkdirSync(codexHooksDest, { recursive: true });
|
|
11669
11701
|
const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
|
|
11702
|
+
// #2544: track whether anything was actually staged. hooks/dist existing
|
|
11703
|
+
// is not the same as an allowlisted file landing in it — see the marker
|
|
11704
|
+
// gate below.
|
|
11705
|
+
let codexStagedHooks = false;
|
|
11670
11706
|
for (const entry of fs.readdirSync(codexHooksSrc)) {
|
|
11671
11707
|
if (!CODEX_HOOKS_TO_COPY.includes(entry)) continue;
|
|
11672
11708
|
const srcFile = path.join(codexHooksSrc, entry);
|
|
@@ -11700,6 +11736,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11700
11736
|
fs.copyFileSync(srcFile, destFile);
|
|
11701
11737
|
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows */ }
|
|
11702
11738
|
}
|
|
11739
|
+
codexStagedHooks = true;
|
|
11703
11740
|
}
|
|
11704
11741
|
console.log(` ${green}✓${reset} Installed hooks (Codex)`);
|
|
11705
11742
|
// #2717: write the CommonJS marker into hooks/ alongside the staged .js
|
|
@@ -11710,7 +11747,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11710
11747
|
// gsd-context-monitor.js as ESM and their require() calls fail silently.
|
|
11711
11748
|
// Reuses the same helper the Cursor/Windsurf writers call so the marker
|
|
11712
11749
|
// content + user-file-preservation contract is identical everywhere.
|
|
11713
|
-
|
|
11750
|
+
//
|
|
11751
|
+
// #2544: gated on codexStagedHooks, mirroring installSharedHooksBundle's
|
|
11752
|
+
// `stagedHooks`. The enclosing guard only proves hooks/dist EXISTS; if it
|
|
11753
|
+
// holds none of CODEX_HOOKS_TO_COPY, this block mkdirs hooks/ and stages
|
|
11754
|
+
// nothing, and an ungated marker would claim a directory GSD did not fill.
|
|
11755
|
+
if (codexStagedHooks && hooksSurface.ensureCommonJsMarker(codexHooksDest)) {
|
|
11714
11756
|
console.log(` ${green}✓${reset} Wrote hooks/package.json (CommonJS mode)`);
|
|
11715
11757
|
}
|
|
11716
11758
|
}
|
|
@@ -11921,7 +11963,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11921
11963
|
// manifest-tracked (verified) — uninstall removes them explicitly via
|
|
11922
11964
|
// removeCursorHooksJson + its script list, and reconcile is idempotent.
|
|
11923
11965
|
// The re-run is retained for parity with the settings.json install path.
|
|
11924
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope:
|
|
11966
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
11925
11967
|
persistActiveProfileMarker();
|
|
11926
11968
|
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
11927
11969
|
}
|
|
@@ -11931,7 +11973,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11931
11973
|
// hooks needed. Kimi is also artifact-only for its INSTALL surface (skills +
|
|
11932
11974
|
// kimi-agents, no settings.json) but #2095 Upgrade 1 gives it its own
|
|
11933
11975
|
// independent hooksSurface: kimi's native config.toml [[hooks]] array, which
|
|
11934
|
-
// lives outside targetDir entirely (resolveKimiHooksTomlDir resolves
|
|
11976
|
+
// lives outside targetDir entirely (resolveKimiHooksTomlDir resolves the
|
|
11977
|
+
// per-runtime root — ~/.kimi for kimi, ~/.kimi-code for kimi-code, #2755 —
|
|
11935
11978
|
// a sibling of targetDir's ~/.config/agents) — hence writing it here, inside
|
|
11936
11979
|
// this early-return, rather than requiring installSurface to change.
|
|
11937
11980
|
//
|
|
@@ -11952,7 +11995,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11952
11995
|
// ~/.kimi/hooks/<script> rather than a script that doesn't exist under
|
|
11953
11996
|
// targetDir/hooks (which kimi no longer receives).
|
|
11954
11997
|
if (plan.hooksSurface === 'kimi-hooks-toml' && isGlobal) {
|
|
11955
|
-
const kimiHooksRoot = resolveKimiHooksTomlDir();
|
|
11998
|
+
const kimiHooksRoot = resolveKimiHooksTomlDir({ runtime });
|
|
11956
11999
|
// Note: the `failures` array's hard-fail gate (`if (failures.length > 0)
|
|
11957
12000
|
// process.exit(1)`) runs earlier in this function, before this
|
|
11958
12001
|
// profile-marker-only branch is ever reached — pushing to it here would
|
|
@@ -11961,6 +12004,20 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11961
12004
|
if (!installSharedHooksBundle(kimiHooksRoot)) {
|
|
11962
12005
|
console.warn(` ${yellow}⚠${reset} Kimi hook bundle did not verify at ${path.join(kimiHooksRoot, 'hooks')} — GSD lifecycle hooks may be incomplete`);
|
|
11963
12006
|
}
|
|
12007
|
+
// #2544: retire the pre-fix marker at kimi's root. installSharedHooksBundle
|
|
12008
|
+
// used to write {"type":"commonjs"} at destRootDir itself; it now writes it
|
|
12009
|
+
// under destRootDir/hooks/, so on an upgrade the old root file is stale and
|
|
12010
|
+
// would keep ~/.kimi pinned to CommonJS.
|
|
12011
|
+
//
|
|
12012
|
+
// Done HERE rather than in installer-migration 007 (which retires the same
|
|
12013
|
+
// stale marker for every other runtime) because kimi's hook root is
|
|
12014
|
+
// the per-runtime Kimi root — resolved by resolveKimiHooksTomlDir, OUTSIDE the configDir.
|
|
12015
|
+
// Migration relPaths are structurally confined to configDir, so the
|
|
12016
|
+
// framework cannot address this path at all. Same exact-content predicate
|
|
12017
|
+
// either way, so a user-authored ~/.kimi/package.json is never touched.
|
|
12018
|
+
if (removeCommonJsMarker(kimiHooksRoot)) {
|
|
12019
|
+
console.log(` ${green}✓${reset} Removed stale package.json from ${kimiHooksRoot} (pre-#2544 marker)`);
|
|
12020
|
+
}
|
|
11964
12021
|
const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
|
|
11965
12022
|
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
11966
12023
|
const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
|
|
@@ -11993,7 +12050,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11993
12050
|
// explicitly via removeWindsurfHooksJson, and reconcileWindsurfHooksJson
|
|
11994
12051
|
// is idempotent on repeated installs, so manifest tracking isn't needed
|
|
11995
12052
|
// for correctness here.
|
|
11996
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope:
|
|
12053
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
11997
12054
|
}
|
|
11998
12055
|
|
|
11999
12056
|
persistActiveProfileMarker();
|
|
@@ -12007,7 +12064,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12007
12064
|
writeClineArtifacts(targetDir, isGlobal);
|
|
12008
12065
|
// Re-run the manifest pass: these artifacts are written *after* the earlier
|
|
12009
12066
|
// writeManifest() call, so a second pass is needed to hash-track them.
|
|
12010
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope:
|
|
12067
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
12011
12068
|
persistActiveProfileMarker();
|
|
12012
12069
|
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
12013
12070
|
}
|
|
@@ -12022,10 +12079,24 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12022
12079
|
// #338: local Claude installs write to settings.local.json (Claude Code's per-user/gitignored slot)
|
|
12023
12080
|
// so engineer-specific absolute paths (Node binary, home dir) never land in the repo-shared
|
|
12024
12081
|
// settings.json. Global installs and all other runtimes continue to use settings.json.
|
|
12082
|
+
// #2870: the CURRENT scope's settings filename is sourced from the Install
|
|
12083
|
+
// Scope Module (_installScope.settingsFile, resolveScope's per-scope field)
|
|
12084
|
+
// instead of indexing _scopedSettings by hand. _scopedSettings itself is
|
|
12085
|
+
// retained unchanged as the #338-privacy fail-safe path: _hostBehaviors
|
|
12086
|
+
// already degrades to FALLBACK_HOST_BEHAVIORS (see that constant's comment
|
|
12087
|
+
// above) when the registry fails to load, whereas resolveScope's registry
|
|
12088
|
+
// lookup throws in that same scenario (_installScope is null when it did).
|
|
12089
|
+
// Falling back to _scopedSettings[_installScopeId] there — and keeping the
|
|
12090
|
+
// non-local-claude branch's expression untouched — means this is
|
|
12091
|
+
// byte-identical to the pre-migration computation in every case, including
|
|
12092
|
+
// the broken-registry fail-safe floor.
|
|
12025
12093
|
const _scopedSettings = _hostBehaviors(runtime).settingsFileByScope || null;
|
|
12026
|
-
const
|
|
12094
|
+
const _currentScopeSettingsFile = _installScope
|
|
12095
|
+
? _installScope.settingsFile
|
|
12096
|
+
: (_scopedSettings ? (_scopedSettings[_installScopeId] ?? null) : null);
|
|
12097
|
+
const isLocalClaude = (!isGlobal && !!_currentScopeSettingsFile);
|
|
12027
12098
|
const settingsFileName = isLocalClaude
|
|
12028
|
-
?
|
|
12099
|
+
? _currentScopeSettingsFile
|
|
12029
12100
|
: ((_scopedSettings && _scopedSettings.global) || 'settings.json');
|
|
12030
12101
|
// ADR-1239 Phase B write-confinement: the descriptor-sourced settings filename
|
|
12031
12102
|
// must resolve under targetDir (this path also drives a recursive mkdirSync).
|
|
@@ -13073,14 +13144,26 @@ function maybeSuggestPathExport(globalBin, homeDir) {
|
|
|
13073
13144
|
|
|
13074
13145
|
console.log('');
|
|
13075
13146
|
console.log(` ${yellow}⚠${reset} ${bold}${globalBin}${reset} is not on your PATH.`);
|
|
13076
|
-
console.log(` Add it with one of:`);
|
|
13077
13147
|
const projected = projectPersistentPathExportActions({
|
|
13078
13148
|
targetDir: globalBin,
|
|
13079
13149
|
platform: process.platform,
|
|
13080
13150
|
});
|
|
13081
|
-
|
|
13082
|
-
|
|
13083
|
-
|
|
13151
|
+
if (projected.reason === PATH_ACTION_REASON.WIN32_RESERVED_QUOTE) {
|
|
13152
|
+
// #3118 review MINOR: a win32 targetDir containing `"` makes
|
|
13153
|
+
// projectPathActionProjection return [] (no command can quote it safely
|
|
13154
|
+
// on Windows) — printing the "Add it with one of:" header with nothing
|
|
13155
|
+
// under it is a silent dead-end. Name the cause instead.
|
|
13156
|
+
console.log(` No command can be suggested: the path contains a ${cyan}"${reset} character, which cannot appear in a Windows path.`);
|
|
13157
|
+
} else if (projected.shellActions.length === 0) {
|
|
13158
|
+
// #3118: no target directory to talk about (reason === NO_TARGET_DIR, or
|
|
13159
|
+
// no reason at all) — there is nothing to print beyond the "not on your
|
|
13160
|
+
// PATH" line above.
|
|
13161
|
+
} else {
|
|
13162
|
+
console.log(` Add it with one of:`);
|
|
13163
|
+
for (const action of projected.shellActions) {
|
|
13164
|
+
const labelPrefix = action.label ? `${action.label}: ` : '';
|
|
13165
|
+
console.log(` ${cyan}${labelPrefix}${action.command}${reset}`);
|
|
13166
|
+
}
|
|
13084
13167
|
}
|
|
13085
13168
|
console.log('');
|
|
13086
13169
|
}
|
|
@@ -13291,14 +13374,12 @@ module.exports = {
|
|
|
13291
13374
|
shouldNormalizeHyphenNamespaceInAgentBody,
|
|
13292
13375
|
normalizeAgentBodyForRuntime,
|
|
13293
13376
|
yamlIdentifier,
|
|
13294
|
-
computePathPrefix,
|
|
13295
|
-
applyRuntimeContentRewritesInPlace,
|
|
13296
13377
|
getCodexSkillAdapterHeader,
|
|
13297
13378
|
convertClaudeCommandToCursorSkill,
|
|
13298
|
-
convertClaudeCommandToCursorCommand,
|
|
13299
13379
|
convertClaudeAgentToCursorAgent,
|
|
13300
13380
|
convertClaudeAgentToCodexAgent,
|
|
13301
13381
|
generateCodexAgentToml,
|
|
13382
|
+
_resetCodexWarningDedupeForTests,
|
|
13302
13383
|
cleanupCodexSkillMetadataSidecars,
|
|
13303
13384
|
cleanupWindsurfLegacyDevinSkills,
|
|
13304
13385
|
cleanupMovedSkillsOldLocation,
|
|
@@ -13306,7 +13387,6 @@ module.exports = {
|
|
|
13306
13387
|
_resolveSkillsRootDir,
|
|
13307
13388
|
codexBareAgentsHasOnlyKnownScalars,
|
|
13308
13389
|
extractCodexUserAgentsScalars,
|
|
13309
|
-
spliceCodexAgentsScalars,
|
|
13310
13390
|
CODEX_EXTENDED_HOOK_EVENTS,
|
|
13311
13391
|
generateCodexConfigBlock,
|
|
13312
13392
|
stripGsdFromCodexConfig,
|
|
@@ -13317,19 +13397,19 @@ module.exports = {
|
|
|
13317
13397
|
validateCodexConfigSchema,
|
|
13318
13398
|
mergeCodexConfig,
|
|
13319
13399
|
installCodexConfig,
|
|
13320
|
-
readGsdRuntimeProfileResolver,
|
|
13321
|
-
readGsdEffectiveModelOverrides,
|
|
13322
|
-
readGsdEffectiveEffortConfig,
|
|
13323
|
-
resolveInstallTimeEffort,
|
|
13324
|
-
injectEffortFrontmatter,
|
|
13325
|
-
get _GSD_EFFORT_MANIFEST_TIER_DEFAULTS() { return _getGsdEffortCatalog().EFFORT_MANIFEST_TIER_DEFAULTS; },
|
|
13326
|
-
get _GSD_EFFORT_MANIFEST_DEFAULT() { return _getGsdEffortCatalog().EFFORT_MANIFEST_DEFAULT; },
|
|
13327
13400
|
install,
|
|
13328
13401
|
installAllRuntimes,
|
|
13329
13402
|
uninstall,
|
|
13330
13403
|
// #2086 — host-behavior resolution + the #338 privacy fail-safe floor (exported for tests)
|
|
13331
13404
|
_resolveHostBehaviors,
|
|
13332
13405
|
FALLBACK_HOST_BEHAVIORS,
|
|
13406
|
+
// #3023 — shared hook bundle directory name, descriptor-driven
|
|
13407
|
+
SHARED_HOOKS_DIR_DEFAULT,
|
|
13408
|
+
resolveSharedHooksDirName,
|
|
13409
|
+
// #3184 — uninstall-side GSD-managed file enumerations, exported for
|
|
13410
|
+
// parity assertions against the wholesale-copy source directories
|
|
13411
|
+
GSD_CHANGESET_FILES,
|
|
13412
|
+
GSD_SCRIPTS_LIB_FILES,
|
|
13333
13413
|
convertSlashCommandsToCodexSkillMentions,
|
|
13334
13414
|
convertClaudeCommandToCodexSkill,
|
|
13335
13415
|
convertClaudeCommandToKimiSkill,
|
|
@@ -13338,8 +13418,6 @@ module.exports = {
|
|
|
13338
13418
|
buildKimiAgentArtifacts,
|
|
13339
13419
|
convertClaudeToOpencodeFrontmatter,
|
|
13340
13420
|
convertClaudeToKiloFrontmatter,
|
|
13341
|
-
convertClaudeCommandToOpencodeSkill,
|
|
13342
|
-
convertClaudeCommandToKiloSkill,
|
|
13343
13421
|
configureOpencodePermissions,
|
|
13344
13422
|
neutralizeAgentReferences,
|
|
13345
13423
|
// #768 — Claude Code permissions pre-population
|
|
@@ -13349,7 +13427,6 @@ module.exports = {
|
|
|
13349
13427
|
GSD_CLAUDE_DENY_PERMISSIONS,
|
|
13350
13428
|
GSD_CODEX_MARKER,
|
|
13351
13429
|
CODEX_AGENT_SANDBOX,
|
|
13352
|
-
getDirName,
|
|
13353
13430
|
getGlobalDir,
|
|
13354
13431
|
getConfigDirFromHome,
|
|
13355
13432
|
resolveKiloConfigPath,
|
|
@@ -13371,20 +13448,11 @@ module.exports = {
|
|
|
13371
13448
|
mergeCopilotInstructions,
|
|
13372
13449
|
stripGsdFromCopilotInstructions,
|
|
13373
13450
|
GSD_COPILOT_HOOK_FILE,
|
|
13374
|
-
buildCopilotHookConfig,
|
|
13375
|
-
writeCopilotHookConfig,
|
|
13376
13451
|
convertClaudeToAntigravityContent,
|
|
13377
13452
|
convertClaudeCommandToAntigravitySkill,
|
|
13378
13453
|
convertClaudeAgentToAntigravityAgent,
|
|
13379
13454
|
convertClaudeCommandToClaudeSkill,
|
|
13380
13455
|
skillFrontmatterName,
|
|
13381
|
-
convertClaudeToWindsurfMarkdown,
|
|
13382
|
-
convertClaudeCommandToWindsurfSkill,
|
|
13383
|
-
convertClaudeCommandToWindsurfWorkflow,
|
|
13384
|
-
convertClaudeAgentToWindsurfAgent,
|
|
13385
|
-
convertClaudeToAugmentMarkdown,
|
|
13386
|
-
convertClaudeCommandToAugmentSkill,
|
|
13387
|
-
convertClaudeAgentToAugmentAgent,
|
|
13388
13456
|
convertClaudeToTraeMarkdown,
|
|
13389
13457
|
convertClaudeCommandToTraeSkill,
|
|
13390
13458
|
convertClaudeAgentToTraeAgent,
|
|
@@ -13395,8 +13463,6 @@ module.exports = {
|
|
|
13395
13463
|
convertClaudeToCliineMarkdown,
|
|
13396
13464
|
convertClaudeCommandToClineSkill,
|
|
13397
13465
|
convertClaudeAgentToClineAgent,
|
|
13398
|
-
// #2284(b) — cross-cutting branding protected-region helper
|
|
13399
|
-
applyClaudeCodeBrandSwap,
|
|
13400
13466
|
// #2284 — Hermes named-dispatch → delegate_task projection
|
|
13401
13467
|
convertClaudeToHermesMarkdown,
|
|
13402
13468
|
projectNamedDispatchToStructuralDelegate,
|
|
@@ -13406,30 +13472,11 @@ module.exports = {
|
|
|
13406
13472
|
maskStringLiterals,
|
|
13407
13473
|
findDispatchCallSpans,
|
|
13408
13474
|
_assertProjectionComplete,
|
|
13409
|
-
_normalizeDispatchCallSpan,
|
|
13410
|
-
buildClineRulesBody,
|
|
13411
|
-
buildClineAgentsMdBody,
|
|
13412
|
-
buildClinePreToolUseHook,
|
|
13413
|
-
writeClineArtifacts,
|
|
13414
|
-
mergeGsdAgentsMd,
|
|
13415
13475
|
GSD_CURSOR_SESSION_HOOK_SCRIPT,
|
|
13416
13476
|
GSD_CURSOR_POST_TOOL_HOOK_SCRIPT,
|
|
13417
|
-
GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT,
|
|
13418
|
-
GSD_CURSOR_STOP_HOOK_SCRIPT,
|
|
13419
|
-
GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT,
|
|
13420
|
-
GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT,
|
|
13421
|
-
GSD_CURSOR_HOOK_SCRIPTS,
|
|
13422
13477
|
GSD_CURSOR_HOOK_MARKER,
|
|
13423
|
-
buildCursorHookEntry,
|
|
13424
|
-
isManagedCursorHookEntry,
|
|
13425
|
-
reconcileCursorHooksJson,
|
|
13426
|
-
writeCursorHooksJson,
|
|
13427
|
-
removeCursorHooksJson,
|
|
13428
13478
|
GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT,
|
|
13429
13479
|
GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT,
|
|
13430
|
-
GSD_WINDSURF_HOOK_SCRIPTS,
|
|
13431
|
-
writeWindsurfHooksJson,
|
|
13432
|
-
removeWindsurfHooksJson,
|
|
13433
13480
|
stripGsdFromAgentsMd,
|
|
13434
13481
|
GSD_AGENTS_MD_MARKER,
|
|
13435
13482
|
GSD_AGENTS_MD_CLOSE_MARKER,
|
|
@@ -13437,11 +13484,9 @@ module.exports = {
|
|
|
13437
13484
|
saveLocalPatches,
|
|
13438
13485
|
reportLocalPatches,
|
|
13439
13486
|
validateHookFields,
|
|
13440
|
-
preserveUserArtifacts,
|
|
13441
|
-
restoreUserArtifacts,
|
|
13442
|
-
migrateLegacyDevPreferencesToSkill,
|
|
13443
13487
|
populatePristineDir,
|
|
13444
|
-
|
|
13488
|
+
_resolveUserArtifactStagingRoot,
|
|
13489
|
+
_tryResolveUserArtifactStagingRoot,
|
|
13445
13490
|
finishInstall,
|
|
13446
13491
|
homePathCoveredByRc,
|
|
13447
13492
|
homePathCoveredByFishConfig,
|
|
@@ -13456,34 +13501,11 @@ module.exports = {
|
|
|
13456
13501
|
buildUpdateBannerPromptText,
|
|
13457
13502
|
parseUpdateBannerInput,
|
|
13458
13503
|
buildUpdateBannerHookEntry,
|
|
13459
|
-
buildHookCommand,
|
|
13460
|
-
normalizeNodePath,
|
|
13461
|
-
resolveNodeRunner,
|
|
13462
|
-
referencesHook,
|
|
13463
|
-
applySettingsJsonHooks,
|
|
13464
|
-
rewriteLegacyManagedNodeHookCommands,
|
|
13465
|
-
buildCodexHookBlock,
|
|
13466
|
-
rewriteLegacyCodexHookBlock,
|
|
13467
|
-
buildCodexHookWindowsShimIR,
|
|
13468
|
-
ensureCodexHooksJsonSessionStart,
|
|
13469
|
-
ensureCodexHooksJsonEvent,
|
|
13470
|
-
removeCodexHooksJsonEvent,
|
|
13471
|
-
reconcileCodexHooksJsonEvent,
|
|
13472
|
-
readGsdCommandNames,
|
|
13473
|
-
installRuntimeArtifacts,
|
|
13474
|
-
installOpencodeFamilySkills,
|
|
13475
|
-
uninstallRuntimeArtifacts,
|
|
13476
13504
|
parseConfigDirFromArgs,
|
|
13477
13505
|
cleanupLegacyGsdCc,
|
|
13478
|
-
_applyRuntimeRewrites,
|
|
13479
13506
|
// #1191 — exported so tests exercise the REAL readSettings, not a replica
|
|
13480
13507
|
readSettings,
|
|
13481
13508
|
stripJsonComments,
|
|
13482
|
-
// Compatibility relays retained after auditing the former broad
|
|
13483
|
-
// runtimeArtifactConversion spread (#1559).
|
|
13484
|
-
processAttribution,
|
|
13485
|
-
applyRuntimeContentRewritesForCommandsInPlace,
|
|
13486
|
-
_copyStaged,
|
|
13487
13509
|
copyWithPathReplacement,
|
|
13488
13510
|
};
|
|
13489
13511
|
|
|
@@ -13511,7 +13533,19 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
|
|
|
13511
13533
|
console.error('Usage: node install.js --skills-root <runtime>');
|
|
13512
13534
|
process.exit(1);
|
|
13513
13535
|
}
|
|
13514
|
-
|
|
13536
|
+
// #3024: validate the runtime id against the shipped capability registry
|
|
13537
|
+
// BEFORE resolving anything. getGlobalSkillsBase's bare `runtimes[runtime]`
|
|
13538
|
+
// lookup falls through the prototype chain to claude's skills root for an
|
|
13539
|
+
// unregistered/hostile id (`__proto__`, `constructor`, `prototype`, …)
|
|
13540
|
+
// instead of failing loudly. isRegisteredRuntimeId is the SAME validator
|
|
13541
|
+
// gsd-tools' `routeSkillsRoot` calls, so this entry point and the shipped
|
|
13542
|
+
// `gsd-tools query skills-root` entry point can never diverge on which
|
|
13543
|
+
// runtime ids they accept.
|
|
13544
|
+
if (!isRegisteredRuntimeId(runtimeArg)) {
|
|
13545
|
+
console.error(`Unknown runtime "${runtimeArg}" — must be a registered runtime id`);
|
|
13546
|
+
process.exit(1);
|
|
13547
|
+
}
|
|
13548
|
+
const skillsRoot = getGlobalSkillsBase(runtimeArg.trim());
|
|
13515
13549
|
if (skillsRoot === null) {
|
|
13516
13550
|
console.error(`${runtimeArg} does not use a skills directory`);
|
|
13517
13551
|
process.exit(1);
|