@opengsd/gsd-core 1.10.0 → 1.12.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 +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +12 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +22 -14
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +5 -3
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +23 -2
- package/agents/gsd-plan-checker.md +50 -53
- package/agents/gsd-planner.md +50 -50
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +63 -4
- package/agents/gsd-ui-researcher.md +41 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +13 -4
- package/bin/install.js +1448 -1103
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +9 -5
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1035 -138
- package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
- package/gsd-core/bin/lib/api-coverage.cjs +33 -14
- package/gsd-core/bin/lib/artifacts.cjs +5 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +1026 -268
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- 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-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +845 -130
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +700 -40
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +216 -42
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +735 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +1172 -108
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
- package/gsd-core/bin/lib/config-loader.cjs +187 -23
- package/gsd-core/bin/lib/config.cjs +102 -3
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +208 -33
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/edge-probe.cjs +9 -1
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +899 -229
- package/gsd-core/bin/lib/gap-checker.cjs +95 -10
- package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- 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 +149 -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 +268 -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 +187 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +439 -168
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +811 -259
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
- package/gsd-core/bin/lib/install-profiles.cjs +212 -61
- 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-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +148 -38
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +170 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +183 -22
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +842 -73
- package/gsd-core/bin/lib/model-catalog.cjs +232 -16
- package/gsd-core/bin/lib/model-resolver.cjs +193 -68
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
- package/gsd-core/bin/lib/phase-id.cjs +514 -40
- package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
- package/gsd-core/bin/lib/phase-locator.cjs +262 -34
- package/gsd-core/bin/lib/phase.cjs +1038 -214
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +98 -3
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
- package/gsd-core/bin/lib/probe-core.cjs +5 -2
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +766 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
- package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +442 -96
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
- package/gsd-core/bin/lib/smart-entry.cjs +133 -23
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +52 -19
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +338 -8
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +846 -176
- package/gsd-core/bin/lib/state.cjs +2589 -369
- package/gsd-core/bin/lib/surface.cjs +33 -11
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- 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 +67 -23
- package/gsd-core/bin/lib/uat.cjs +1761 -167
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
- package/gsd-core/bin/lib/unusable-input.cjs +37 -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-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +75 -0
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -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 +272 -9
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +453 -918
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/references/agent-contracts.md +44 -26
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +39 -21
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +4 -6
- package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
- 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 +17 -11
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +5 -4
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +61 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planner-revision.md +1 -1
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +44 -13
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/tdd.md +1 -3
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +5 -5
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -17
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +5 -4
- package/gsd-core/templates/state.md +11 -4
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +3 -3
- package/gsd-core/workflows/add-tests.md +3 -8
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +13 -20
- package/gsd-core/workflows/audit-fix.md +12 -3
- package/gsd-core/workflows/audit-milestone.md +9 -9
- package/gsd-core/workflows/audit-uat.md +17 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +11 -27
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +64 -5
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
- package/gsd-core/workflows/code-review-fix.md +38 -11
- package/gsd-core/workflows/code-review.md +159 -52
- package/gsd-core/workflows/complete-milestone.md +151 -23
- package/gsd-core/workflows/debug.md +12 -8
- package/gsd-core/workflows/diagnose-issues.md +47 -15
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +3 -6
- package/gsd-core/workflows/docs-update.md +5 -4
- package/gsd-core/workflows/edit-phase.md +27 -2
- package/gsd-core/workflows/eval-review.md +7 -14
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- 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 +24 -4
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase.md +72 -100
- package/gsd-core/workflows/execute-plan.md +52 -15
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +76 -10
- package/gsd-core/workflows/import.md +18 -15
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +49 -16
- package/gsd-core/workflows/insert-phase.md +5 -5
- package/gsd-core/workflows/list-seeds.md +5 -3
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +12 -23
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +8 -5
- package/gsd-core/workflows/new-milestone.md +22 -29
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +26 -40
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +14 -2
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
- package/gsd-core/workflows/plan-phase.md +162 -59
- package/gsd-core/workflows/plan-review-convergence.md +96 -11
- package/gsd-core/workflows/plant-seed.md +2 -2
- package/gsd-core/workflows/pr-branch.md +187 -51
- package/gsd-core/workflows/profile-user.md +16 -14
- package/gsd-core/workflows/progress.md +61 -18
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
- package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
- package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +55 -44
- package/gsd-core/workflows/remove-phase.md +4 -4
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +8 -12
- package/gsd-core/workflows/review.md +219 -20
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +69 -7
- package/gsd-core/workflows/ship.md +116 -50
- package/gsd-core/workflows/sketch-wrap-up.md +11 -17
- package/gsd-core/workflows/sketch.md +12 -18
- package/gsd-core/workflows/smart-entry.md +3 -5
- package/gsd-core/workflows/spec-phase.md +53 -13
- package/gsd-core/workflows/spike-wrap-up.md +7 -11
- package/gsd-core/workflows/spike.md +20 -31
- package/gsd-core/workflows/stats.md +2 -2
- package/gsd-core/workflows/sync-skills.md +64 -9
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +49 -14
- package/gsd-core/workflows/ui-phase.md +15 -21
- package/gsd-core/workflows/ui-review.md +8 -12
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +7 -11
- package/gsd-core/workflows/validate-phase.md +3 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +66 -25
- package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +19 -10
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +2 -3
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +83 -3
- package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +76 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +37 -27
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +55 -32
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +231 -24
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-validate-commit.sh +80 -6
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +162 -46
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- 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 +9 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +158 -30
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +19 -10
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +2 -3
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +83 -3
- package/hooks/gsd-cursor-subagent-stop.js +6 -3
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +76 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +37 -27
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +55 -32
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +231 -24
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-validate-commit.sh +80 -6
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +162 -46
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +28 -11
- package/pi/gsd.cjs +19 -5
- package/scripts/base64-scan.sh +74 -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 +5 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +64 -14
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +11 -21
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +138 -25
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +308 -0
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- 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-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-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +38 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +492 -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 +471 -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 +488 -0
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +950 -0
- package/scripts/lint-test-file-count.allowlist.json +137 -8
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +830 -0
- package/scripts/lint-vendored-deps.cjs +297 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +100 -14
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +24 -7
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +9 -5
- 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/bin/lib/ui-safety-gate.cjs +0 -107
- 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 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
package/bin/install.js
CHANGED
|
@@ -36,10 +36,19 @@ const {
|
|
|
36
36
|
resolveKimiHooksTomlDir,
|
|
37
37
|
isRegisteredRuntimeId,
|
|
38
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');
|
|
44
|
+
const { isTestHomeGuardRefusal } = require('../gsd-core/bin/lib/real-home-guard.cjs');
|
|
39
45
|
// getDirName (runtime -> local config dir name) is relocated out of this
|
|
40
46
|
// installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the
|
|
41
47
|
// conversion module's rewrite engine can consume it without importing
|
|
42
|
-
// bin/install.js.
|
|
48
|
+
// bin/install.js. Imported here for install.js's own internal call sites
|
|
49
|
+
// (getConfigDirFromHome and the runtime-content-rewrite loops below) — #2876
|
|
50
|
+
// retired the re-export; tests now import getDirName directly from
|
|
51
|
+
// gsd-core/bin/lib/runtime-name-policy.cjs.
|
|
43
52
|
const { getDirName, getRuntimeLabel, getGlobalConfigHomeFragment, runtimeFlags, getRuntimeNewProjectCommand } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
|
44
53
|
const {
|
|
45
54
|
applyWorktreeBaseRef,
|
|
@@ -54,6 +63,11 @@ const { composeWorkflow } = require('../gsd-core/bin/lib/workflow-fragments.cjs'
|
|
|
54
63
|
// MCP catalog, src/mcp-catalog.cts) — see the comment at its call site below.
|
|
55
64
|
const { shouldCompose } = require('../gsd-core/bin/lib/mcp-catalog.cjs');
|
|
56
65
|
const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
|
|
66
|
+
const { escapeRegex: escapeRegExp } = require('../gsd-core/bin/lib/pattern.cjs');
|
|
67
|
+
// #2873: cross-scope shadow detection — reports (never fails) when a
|
|
68
|
+
// GSD-owned scope shadows another on this machine (design doc:
|
|
69
|
+
// .gsd/phase/feat-2873-cross-scope-shadowing/40-design.md).
|
|
70
|
+
const { buildShadowReport, renderShadowReport } = require('../gsd-core/bin/lib/install-shadow-report.cjs');
|
|
57
71
|
// #2544: the CommonJS marker's single source of truth. classifyMarker() backs
|
|
58
72
|
// BOTH ensureCommonJsMarker() (install) and removeCommonJsMarker() (uninstall),
|
|
59
73
|
// so the write side can no longer clobber a package.json the remove side would
|
|
@@ -66,9 +80,14 @@ const { ensureCommonJsMarker, removeCommonJsMarker } = require('../gsd-core/bin/
|
|
|
66
80
|
const { HOOKS_TO_COPY: _HOOKS_TO_COPY } = require('../scripts/build-hooks.js');
|
|
67
81
|
const INSTALLED_HOOK_FILES = new Set(_HOOKS_TO_COPY);
|
|
68
82
|
|
|
69
|
-
// ADR-857 phase 5f-1: hook-surface writer functions extracted to a dedicated
|
|
70
|
-
//
|
|
71
|
-
// (require('../bin/install.js').writeCursorHooksJson etc.)
|
|
83
|
+
// ADR-857 phase 5f-1: hook-surface writer functions extracted to a dedicated
|
|
84
|
+
// module. install.js used to re-export the whole hooksSurface surface so
|
|
85
|
+
// existing callers (require('../bin/install.js').writeCursorHooksJson etc.)
|
|
86
|
+
// kept working — #2876 found zero production/test consumers of any of those
|
|
87
|
+
// re-exports (tests import runtime-hooks-surface.cjs directly) and retired
|
|
88
|
+
// them from module.exports. install.js still requires hooksSurface below for
|
|
89
|
+
// its own internal call sites (writeCursorHooksJson, writeClineArtifacts,
|
|
90
|
+
// resolveNodeRunner, applySettingsJsonHooks, etc.).
|
|
72
91
|
const hooksSurface = require('../gsd-core/bin/lib/runtime-hooks-surface.cjs');
|
|
73
92
|
|
|
74
93
|
/**
|
|
@@ -296,13 +315,26 @@ const GSD_COPILOT_SESSION_HOOK_PWSH =
|
|
|
296
315
|
// subagentStart → gsd-cursor-subagent-start.js (subagent context injection)
|
|
297
316
|
// subagentStop → gsd-cursor-subagent-stop.js (subagent completion reminder)
|
|
298
317
|
// Cursor docs: https://cursor.com/docs/hooks
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
//
|
|
318
|
+
//
|
|
319
|
+
// These script-name/marker constants used to be independently re-declared
|
|
320
|
+
// here with their own string literals — a second, unlinked copy of exactly
|
|
321
|
+
// the values runtime-hooks-surface.cts also defines for its own internal use
|
|
322
|
+
// (buildCursorHookEntry, writeCursorHooksJson, etc.). #2876's code review
|
|
323
|
+
// found tests reading the constant from install.js's copy while calling
|
|
324
|
+
// functions built from hooksSurface's copy, with nothing guarding the two
|
|
325
|
+
// staying equal — the same unlinked-duplicate-value hazard the ADR-1508
|
|
326
|
+
// dedup elsewhere in this file exists to prevent. Fixed at the root: these
|
|
327
|
+
// are now bare references to hooksSurface's own exports, so there is exactly
|
|
328
|
+
// one literal definition of each value, full stop.
|
|
329
|
+
const GSD_CURSOR_SESSION_HOOK_SCRIPT = hooksSurface.GSD_CURSOR_SESSION_HOOK_SCRIPT;
|
|
330
|
+
const GSD_CURSOR_POST_TOOL_HOOK_SCRIPT = hooksSurface.GSD_CURSOR_POST_TOOL_HOOK_SCRIPT;
|
|
331
|
+
const GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT = hooksSurface.GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT;
|
|
332
|
+
const GSD_CURSOR_STOP_HOOK_SCRIPT = hooksSurface.GSD_CURSOR_STOP_HOOK_SCRIPT;
|
|
333
|
+
const GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT = hooksSurface.GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT;
|
|
334
|
+
const GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT = hooksSurface.GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT;
|
|
335
|
+
// All GSD-managed Cursor hook scripts (used by uninstall cleanup). Not
|
|
336
|
+
// independently defined in hooksSurface — built here from the bare
|
|
337
|
+
// references above, so it can never drift from them either.
|
|
306
338
|
const GSD_CURSOR_HOOK_SCRIPTS = [
|
|
307
339
|
GSD_CURSOR_SESSION_HOOK_SCRIPT,
|
|
308
340
|
GSD_CURSOR_POST_TOOL_HOOK_SCRIPT,
|
|
@@ -312,7 +344,7 @@ const GSD_CURSOR_HOOK_SCRIPTS = [
|
|
|
312
344
|
GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT,
|
|
313
345
|
];
|
|
314
346
|
// Marker comment embedded in managed hook entries so GSD can find+remove them.
|
|
315
|
-
const GSD_CURSOR_HOOK_MARKER =
|
|
347
|
+
const GSD_CURSOR_HOOK_MARKER = hooksSurface.GSD_CURSOR_HOOK_MARKER;
|
|
316
348
|
|
|
317
349
|
// #2100 Stage 2 — Windsurf/Cascade lifecycle hook constants.
|
|
318
350
|
// Windsurf/Cascade reads hook configs from <project-root>/.windsurf/hooks.json
|
|
@@ -328,15 +360,17 @@ const GSD_CURSOR_HOOK_MARKER = 'gsd-managed';
|
|
|
328
360
|
// have no Windsurf counterpart and are deliberately NOT ported.
|
|
329
361
|
// Cascade hooks docs (reference): https://docs.windsurf.com/llms-full.txt ,
|
|
330
362
|
// https://docs.devin.ai/desktop/cascade/hooks
|
|
331
|
-
|
|
332
|
-
|
|
363
|
+
//
|
|
364
|
+
// Same #2876 fix as the Cursor block above: bare references to hooksSurface's
|
|
365
|
+
// own exports instead of a second, unlinked literal copy.
|
|
366
|
+
const GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT = hooksSurface.GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT;
|
|
367
|
+
const GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT = hooksSurface.GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT;
|
|
333
368
|
// All GSD-managed Windsurf hook scripts (used by uninstall cleanup).
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
];
|
|
369
|
+
// hooksSurface independently defines the same array — bare reference here so
|
|
370
|
+
// the two can never drift.
|
|
371
|
+
const GSD_WINDSURF_HOOK_SCRIPTS = hooksSurface.GSD_WINDSURF_HOOK_SCRIPTS;
|
|
338
372
|
|
|
339
|
-
// GSD-managed files under hooks/lib/ (helpers required by gsd-*.
|
|
373
|
+
// GSD-managed files under hooks/lib/ (helpers required by gsd-*.js hooks).
|
|
340
374
|
// git-cmd.js does not start with "gsd-" (shared classifier for #3129), gsd-graphify-rebuild.sh does.
|
|
341
375
|
// cursor-workspace.js (#2587) is required by the Cursor lifecycle hooks. Those
|
|
342
376
|
// are staged individually by writeCursorHooksJson (Cursor sets
|
|
@@ -344,7 +378,10 @@ const GSD_WINDSURF_HOOK_SCRIPTS = [
|
|
|
344
378
|
// copy below) — that function stages this helper alongside them. Listing it
|
|
345
379
|
// here keeps uninstall and the manifest managing it for every OTHER runtime
|
|
346
380
|
// that does receive hooks/lib.
|
|
347
|
-
|
|
381
|
+
// injection-patterns.js (#3504) is required by gsd-prompt-guard.js and
|
|
382
|
+
// gsd-read-injection-scanner.js — the shared prompt-injection pattern list the
|
|
383
|
+
// two guards require so their copies cannot drift.
|
|
384
|
+
const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh', 'cursor-workspace.js', 'injection-patterns.js'];
|
|
348
385
|
|
|
349
386
|
/**
|
|
350
387
|
* Directory name GSD stages its shared hook bundle under, inside a runtime's
|
|
@@ -361,6 +398,21 @@ const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh', 'cursor-wor
|
|
|
361
398
|
*/
|
|
362
399
|
const SHARED_HOOKS_DIR_DEFAULT = 'hooks';
|
|
363
400
|
|
|
401
|
+
// #3184 — GSD-managed file enumerations for scripts/changeset/ and scripts/lib/
|
|
402
|
+
// uninstall. The install-side copy of both directories is wholesale ("copy every
|
|
403
|
+
// file present"), so these enumerations MUST be kept in parity with the real
|
|
404
|
+
// directory contents or an added file ships on install and then orphans on
|
|
405
|
+
// uninstall (survives removal, keeps the dir non-empty, blocks its rmdir).
|
|
406
|
+
// Hoisted to module scope (and exported below) so tests/install.test.cjs can
|
|
407
|
+
// assert parity against fs.readdirSync(scripts/lib) / fs.readdirSync(scripts/changeset)
|
|
408
|
+
// without source-grepping this file.
|
|
409
|
+
const GSD_CHANGESET_FILES = [
|
|
410
|
+
'cli.cjs', 'parse.cjs', 'render.cjs', 'serialize.cjs',
|
|
411
|
+
'github-release-notes.cjs', 'lint.cjs', 'new.cjs',
|
|
412
|
+
'README.md', // documentation only — not user-authored
|
|
413
|
+
];
|
|
414
|
+
const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-scan.cjs', 'alias-drift-families.cjs', 'exit-code-registry.cjs', 'ndjson-reporter.cjs', 'ci-job-timing.cjs'];
|
|
415
|
+
|
|
364
416
|
/**
|
|
365
417
|
* Resolve a runtime's shared-hooks directory name from its descriptor.
|
|
366
418
|
*
|
|
@@ -408,19 +460,32 @@ function resolveSharedHooksDirName(runtime) {
|
|
|
408
460
|
return name;
|
|
409
461
|
}
|
|
410
462
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
463
|
+
// #3897 rung 3 — sandbox_mode derivation, the hold list, and the hold-roster
|
|
464
|
+
// validator now live in `src/codex-agent-toml.cts` (compiled to
|
|
465
|
+
// `gsd-core/bin/lib/codex-agent-toml.cjs`), NOT here. This module used to be
|
|
466
|
+
// the sole owner, and `agent-install-check.cts`'s `checkCodexSandboxPosture`
|
|
467
|
+
// lazily `require()`d THIS FILE to reach `deriveCodexSandboxMode` — but
|
|
468
|
+
// requiring `bin/install.js` runs its whole top-level script, including the
|
|
469
|
+
// CLI's ASCII banner print to stdout, which corrupted every stdout-JSON
|
|
470
|
+
// caller downstream of that posture check (`gsd-tools validate agents`).
|
|
471
|
+
// `codex-agent-toml.cjs` is a genuine leaf (no top-level side effects), so
|
|
472
|
+
// both this file and `agent-install-check.cts` import the derivation from
|
|
473
|
+
// there — ONE owner, no second predicate. See that module's header for the
|
|
474
|
+
// full rationale, and CAUSE B (below, `installCodexConfig`) for the removal
|
|
475
|
+
// of `validateCodexSandboxHolds`'s call from the install runtime path.
|
|
476
|
+
const {
|
|
477
|
+
CODEX_SANDBOX_HOLDS,
|
|
478
|
+
deriveCodexSandboxMode,
|
|
479
|
+
validateCodexSandboxHolds,
|
|
480
|
+
// #3897 list-form parse fix, Fix 3 (generative-fix-divergence): this
|
|
481
|
+
// file's own `generateCodexAgentToml` used to pull `tools:` via its
|
|
482
|
+
// private `extractFrontmatterField` (single-line only) instead of this
|
|
483
|
+
// shared reader — the two sandbox-feeding paths (this emitter and
|
|
484
|
+
// `agent-install-check.cts`'s `checkCodexSandboxPosture`) silently
|
|
485
|
+
// disagreed on YAML block-list `tools:` form. Both now route through this
|
|
486
|
+
// ONE extractor.
|
|
487
|
+
extractToolsValue,
|
|
488
|
+
} = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'codex-agent-toml.cjs'));
|
|
424
489
|
|
|
425
490
|
// Copilot tool name mapping — Claude Code tools to GitHub Copilot tools
|
|
426
491
|
// Tool mapping applies ONLY to agents, NOT to skills (per CONTEXT.md decision)
|
|
@@ -450,14 +515,13 @@ const pkg = require('../package.json');
|
|
|
450
515
|
// of cwd, but keeping the require at the top makes the dependency explicit and
|
|
451
516
|
// surfaces resolution failures at process start instead of at first install call.
|
|
452
517
|
const _gsdLibDir = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib');
|
|
453
|
-
const { MODEL_PROFILES: GSD_MODEL_PROFILES } = require(path.join(_gsdLibDir, 'model-profiles.cjs'));
|
|
454
518
|
const {
|
|
455
519
|
RUNTIME_PROFILE_MAP: GSD_RUNTIME_PROFILE_MAP,
|
|
520
|
+
isAnthropicFlavoredModel: gsdIsAnthropicFlavoredModel,
|
|
456
521
|
} = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
} = require(path.join(_gsdLibDir, 'model-resolver.cjs'));
|
|
522
|
+
// #2875 Part 2: MODEL_PROFILES + resolveTierEntry are now consumed only by
|
|
523
|
+
// install-model-override-resolver.cjs's readGsdRuntimeProfileResolver
|
|
524
|
+
// (required below) — this installer no longer needs its own bindings.
|
|
461
525
|
|
|
462
526
|
// #2071 — install-time effort resolution (readGsdEffectiveEffortConfig /
|
|
463
527
|
// resolveInstallTimeEffort, plus their _getGsdEffortCatalog + _readGsdConfigFile
|
|
@@ -530,6 +594,17 @@ try {
|
|
|
530
594
|
// hardcoded string-equality branch) so behavior degrades CLOSED (safe), never open.
|
|
531
595
|
// The live descriptor (capabilities/claude/capability.json) remains the source of
|
|
532
596
|
// truth; this mirrors only the privacy-load-bearing subset. (ADR-1239 / #2086)
|
|
597
|
+
//
|
|
598
|
+
// #2870: NOT routed through the Install Scope Module (resolveScope,
|
|
599
|
+
// src/install-scope.cts) despite that module owning per-scope settings-file
|
|
600
|
+
// resolution elsewhere in this file. resolveScope's own descriptor lookup
|
|
601
|
+
// goes through the SAME capability registry require this floor exists to
|
|
602
|
+
// survive the failure of (see getRegistry() in install-scope.cts) — so on
|
|
603
|
+
// exactly the "registry failed to load" path this constant is for,
|
|
604
|
+
// resolveScope would throw too. Routing through it here would trade a
|
|
605
|
+
// graceful, documented degrade for a crash in the one case this floor was
|
|
606
|
+
// added to prevent. This hardcoded literal is the correct, honest answer,
|
|
607
|
+
// not an un-migrated leftover.
|
|
533
608
|
const FALLBACK_HOST_BEHAVIORS = Object.freeze({
|
|
534
609
|
claude: Object.freeze({
|
|
535
610
|
settingsFileByScope: Object.freeze({ local: 'settings.local.json', global: 'settings.json' }),
|
|
@@ -591,6 +666,84 @@ function _hostIntegrationDispatch(runtime) {
|
|
|
591
666
|
return dispatch || {};
|
|
592
667
|
}
|
|
593
668
|
|
|
669
|
+
/**
|
|
670
|
+
* #2870: shared install()/uninstall() scope resolution. Routes `id` through
|
|
671
|
+
* the Install Scope Module (src/install-scope.cts) and degrades to `null` on
|
|
672
|
+
* failure (unknown/non-installable runtime, broken registry bundle) instead
|
|
673
|
+
* of throwing, so each call site's own plain-id fallback keeps working
|
|
674
|
+
* exactly as it did before this migration. Both call sites previously carried
|
|
675
|
+
* their own copy of this try/catch; this is the one shared copy.
|
|
676
|
+
*/
|
|
677
|
+
function _resolveScopeSafe(id, runtime) {
|
|
678
|
+
try {
|
|
679
|
+
return resolveScope({ id, runtime });
|
|
680
|
+
} catch (_) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Resolve a layout kind's on-disk destination directory. Shared by the
|
|
687
|
+
* skills-root resolution above and the #3664 warning below so the
|
|
688
|
+
* home-override + destSubpath join exists once (#3659-class re-encoding guard).
|
|
689
|
+
*/
|
|
690
|
+
function _kindDestDir(layout, kindName, targetDir) {
|
|
691
|
+
const kind = layout.kinds.find((k) => k.kind === kindName);
|
|
692
|
+
if (!kind) return null;
|
|
693
|
+
return path.join(kind.home || targetDir, kind.destSubpath);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* #3738: scope-aware, layout-resolving wrapper over _kindDestDir for callers
|
|
698
|
+
* that have (runtime, configDir, scope) rather than a resolved Layout — the
|
|
699
|
+
* writeManifest agents surface being the first. Never throws: a runtime whose
|
|
700
|
+
* layout cannot be resolved (unknown id, descriptor error) keeps the caller's
|
|
701
|
+
* own fallback rather than losing the manifest.
|
|
702
|
+
*/
|
|
703
|
+
function _kindDestDirSafe(runtime, configDir, scope, kindName) {
|
|
704
|
+
try {
|
|
705
|
+
return _kindDestDir(resolveRuntimeArtifactLayout(runtime, configDir, scope), kindName, configDir);
|
|
706
|
+
} catch {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* #3664 — warn (never refuse) when `--config-dir` points the install at a
|
|
713
|
+
* directory whose agent destination already holds FOREIGN (non-GSD) agent
|
|
714
|
+
* files — the fingerprint of another harness's config home (e.g. ~/.junie,
|
|
715
|
+
* ~/.factory) or a hand-curated agents dir. GSD emits the selected runtime's
|
|
716
|
+
* artifacts verbatim: tool IDs (`Skill`) and MCP grants (`mcp__server__tool`)
|
|
717
|
+
* that are inert or invalid in a foreign harness surface only at dispatch
|
|
718
|
+
* time, months later. Warn-and-proceed is the issue-sanctioned option (b):
|
|
719
|
+
* a fresh custom dir (the documented brand-specific-dir use), a gsd-only dir
|
|
720
|
+
* (updates, the --all shared dir — including kimi's root `gsd.md`, which is
|
|
721
|
+
* GSD-owned despite the bare `gsd` stem), and the no-flag default-home path
|
|
722
|
+
* (users keep personal agents in ~/.claude/agents) all stay silent. Degrades
|
|
723
|
+
* silently on any resolution failure — the warn path never blocks install.
|
|
724
|
+
*/
|
|
725
|
+
function warnIfForeignAgentDest(runtime, targetDir, scope, explicitConfigDir) {
|
|
726
|
+
if (explicitConfigDir !== true) return;
|
|
727
|
+
try {
|
|
728
|
+
const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope);
|
|
729
|
+
// kimi's global agents kind is `kimi-agents` (#2095 EoS), every other
|
|
730
|
+
// runtime's is `agents`.
|
|
731
|
+
const agentsDir = _kindDestDir(layout, 'agents', targetDir)
|
|
732
|
+
|| _kindDestDir(layout, 'kimi-agents', targetDir);
|
|
733
|
+
if (!agentsDir) return;
|
|
734
|
+
if (!fs.existsSync(agentsDir) || !fs.statSync(agentsDir).isDirectory()) return;
|
|
735
|
+
const foreign = fs
|
|
736
|
+
.readdirSync(agentsDir)
|
|
737
|
+
.filter((f) => f.endsWith('.md') && !f.startsWith('gsd-') && f !== 'gsd.md');
|
|
738
|
+
if (foreign.length === 0) return;
|
|
739
|
+
console.log(
|
|
740
|
+
` ${yellow}⚠${reset} ${bold}${targetDir}${reset} already contains ${foreign.length} non-GSD agent file(s) — this may be another harness's config home. GSD emits artifacts shaped for ${runtime}: tool IDs and MCP grants may be inert or invalid for whatever harness reads this directory (#3664).`
|
|
741
|
+
);
|
|
742
|
+
} catch (_) {
|
|
743
|
+
/* never block install on the warning path */
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
594
747
|
/**
|
|
595
748
|
* Resolve the ACTUAL on-disk skills-install directory for a runtime, honoring a
|
|
596
749
|
* skills-kind `home` override (ADR-1239 upgrade 3 / #2088: e.g. Codex skills ->
|
|
@@ -601,8 +754,8 @@ function _hostIntegrationDispatch(runtime) {
|
|
|
601
754
|
function _resolveSkillsRootDir(runtime, targetDir, scope) {
|
|
602
755
|
try {
|
|
603
756
|
const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope);
|
|
604
|
-
const
|
|
605
|
-
if (
|
|
757
|
+
const skillsDir = _kindDestDir(layout, 'skills', targetDir);
|
|
758
|
+
if (skillsDir) return skillsDir;
|
|
606
759
|
} catch (_e) { /* fall through to the configDir default */ }
|
|
607
760
|
return path.join(targetDir, 'skills');
|
|
608
761
|
}
|
|
@@ -623,8 +776,10 @@ function _runtimeAdapter(runtime) {
|
|
|
623
776
|
}
|
|
624
777
|
}
|
|
625
778
|
const {
|
|
779
|
+
acquireInstallMigrationLock,
|
|
626
780
|
applyInstallerMigrationPlan,
|
|
627
781
|
discoverInstallerMigrations,
|
|
782
|
+
MANIFEST_SCHEMA_VERSION,
|
|
628
783
|
runInstallerMigrations,
|
|
629
784
|
} = require(path.join(_gsdLibDir, 'installer-migrations.cjs'));
|
|
630
785
|
const {
|
|
@@ -653,29 +808,78 @@ const {
|
|
|
653
808
|
// getCommitAttribution STAYS here (impure install-time config I/O); it is injected
|
|
654
809
|
// into the engine functions via the resolveAttribution parameter at each call site.
|
|
655
810
|
const installEngine = require(path.join(_gsdLibDir, 'install-engine.cjs'));
|
|
811
|
+
// #2876: _copyStaged, convertClaudeCommandToOpencodeSkill, and
|
|
812
|
+
// convertClaudeCommandToKiloSkill used to be destructured here too — all
|
|
813
|
+
// three had no install.js internal caller and no export consumer (tests
|
|
814
|
+
// import all three directly from gsd-core/bin/lib/install-engine.cjs), so
|
|
815
|
+
// the retired bindings were dead code. applyOpencodeFamilyPathPrefix,
|
|
816
|
+
// _runLegacyInstallMigrations, _runLegacyUninstallCleanup,
|
|
817
|
+
// _removeGsdEntries, _restoreDir, and _removeHermesBareStemDirs were the
|
|
818
|
+
// same — unused local bindings that were never part of this module's export
|
|
819
|
+
// surface either — found and retired in the same sweep.
|
|
656
820
|
const {
|
|
657
821
|
installRuntimeArtifacts,
|
|
658
822
|
uninstallRuntimeArtifacts,
|
|
659
823
|
installOpencodeFamilySkills,
|
|
824
|
+
installAgentsKindStandalone,
|
|
660
825
|
_installNativePluginIfDeclared,
|
|
661
|
-
_copyStaged,
|
|
662
826
|
hasExistingSymlinkBetween,
|
|
663
827
|
isSymlinkedDestOptIn,
|
|
664
|
-
preserveUserArtifacts,
|
|
665
|
-
restoreUserArtifacts,
|
|
666
828
|
migrateLegacyDevPreferencesToSkill,
|
|
667
|
-
applyOpencodeFamilyPathPrefix,
|
|
668
|
-
convertClaudeCommandToOpencodeSkill,
|
|
669
|
-
convertClaudeCommandToKiloSkill,
|
|
670
829
|
USER_OWNED_ARTIFACTS,
|
|
671
|
-
_runLegacyInstallMigrations,
|
|
672
|
-
_runLegacyUninstallCleanup,
|
|
673
|
-
_removeGsdEntries,
|
|
674
830
|
_snapshotDir,
|
|
675
|
-
_restoreDir,
|
|
676
|
-
_removeHermesBareStemDirs,
|
|
677
831
|
} = installEngine;
|
|
678
832
|
|
|
833
|
+
// #2875 (epic #2866 Phase 6): durable on-disk staging for USER_OWNED_ARTIFACTS
|
|
834
|
+
// across the preserve -> wipe -> restore window (#1874-F19). See
|
|
835
|
+
// src/user-artifact-staging.cts's module doc.
|
|
836
|
+
const {
|
|
837
|
+
stageUserArtifacts,
|
|
838
|
+
restoreStagedUserArtifacts,
|
|
839
|
+
discardStagedUserArtifacts,
|
|
840
|
+
recoverOrphanedUserArtifacts,
|
|
841
|
+
} = require(path.join(_gsdLibDir, 'user-artifact-staging.cjs'));
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Resolve the durable staging root for `configDir`, confined via
|
|
845
|
+
* `assertDestWithinConfigHome` and refused via `hasExistingSymlinkBetween`
|
|
846
|
+
* (test-matrix E1/E4) — mirrors install-engine.cts's
|
|
847
|
+
* `_resolveUserArtifactStagingRoot`, kept local here because bin/install.js's
|
|
848
|
+
* two call sites (uninstall's legacy-migration block, install's mainline
|
|
849
|
+
* gsd-core copy) are not inside that module.
|
|
850
|
+
*/
|
|
851
|
+
function _resolveUserArtifactStagingRoot(configDir) {
|
|
852
|
+
const stagingRoot = assertDestWithinConfigHome(configDir, path.posix.join('.gsd-staging', 'user-artifacts'));
|
|
853
|
+
if (hasExistingSymlinkBetween(path.resolve(configDir), stagingRoot, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
854
|
+
throw new Error(
|
|
855
|
+
`_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.`,
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
return stagingRoot;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Degrade-not-abort wrapper over `_resolveUserArtifactStagingRoot` — mirrors
|
|
863
|
+
* install-engine.cts's own `_tryResolveUserArtifactStagingRoot` (kept local
|
|
864
|
+
* here for the same reason the throwing version above is: bin/install.js's
|
|
865
|
+
* own call sites are not inside that module). A hostile/broken
|
|
866
|
+
* `.gsd-staging` path (or a symlinked configDir itself) must never brick
|
|
867
|
+
* `install()` or `uninstall()` — before this fix, `_resolveUserArtifactStagingRoot`
|
|
868
|
+
* was called UNGUARDED as the first statement of both, so
|
|
869
|
+
* `ln -s /nonexistent ~/.claude/.gsd-staging` killed both commands, including
|
|
870
|
+
* uninstall, the remedy for the first problem. Returns `null` (never throws),
|
|
871
|
+
* logging one warning; every call site MUST treat `null` as "skip the
|
|
872
|
+
* staging-dependent step for this run".
|
|
873
|
+
*/
|
|
874
|
+
function _tryResolveUserArtifactStagingRoot(configDir) {
|
|
875
|
+
try {
|
|
876
|
+
return _resolveUserArtifactStagingRoot(configDir);
|
|
877
|
+
} catch (err) {
|
|
878
|
+
console.warn(` ${yellow}!${reset} user-artifact staging unavailable for "${configDir}" (${err.message}) — proceeding without durable staging for this step.`);
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
679
883
|
// Parse args
|
|
680
884
|
const args = process.argv.slice(2);
|
|
681
885
|
const hasGlobal = args.includes('--global') || args.includes('-g');
|
|
@@ -685,6 +889,17 @@ const hasSkillsRoot = args.includes('--skills-root');
|
|
|
685
889
|
const hasPortableHooks = args.includes('--portable-hooks') || process.env.GSD_PORTABLE_HOOKS === '1';
|
|
686
890
|
const hasMinimal = args.includes('--minimal') || args.includes('--core-only');
|
|
687
891
|
const hasDryRun = args.includes('--dry-run');
|
|
892
|
+
// #3031: opt-in reclaim of the GSD artifacts a PRE-#2755 `--kimi-code` install
|
|
893
|
+
// orphaned in Kimi CLI's `~/.kimi`. Opt-in and not automatic because the stale
|
|
894
|
+
// block is BYTE-IDENTICAL to a legitimate Kimi CLI one — both runtimes render
|
|
895
|
+
// the same bytes for the same root, since the command paths derive from the
|
|
896
|
+
// hooks root and not from the runtime — so no inspection can tell "litter GSD
|
|
897
|
+
// wrote for kimi-code" from "Kimi CLI's working hooks". Cleaning unasked would
|
|
898
|
+
// break #2755's own acceptance criterion ("Uninstalling GSD hooks for one
|
|
899
|
+
// runtime does not touch or remove the other runtime's hooks") for anyone with
|
|
900
|
+
// both products installed. The user, who knows which products they run, is the
|
|
901
|
+
// only party that can decide — so they ask for it explicitly.
|
|
902
|
+
const hasReclaimKimiLegacy = args.includes('--reclaim-kimi-legacy');
|
|
688
903
|
// --profile=<name> or --profile=<n1>,<n2> (composable); mutually exclusive with --minimal
|
|
689
904
|
const _profileArgRaw = (() => {
|
|
690
905
|
for (const arg of args) {
|
|
@@ -784,6 +999,21 @@ function disambiguateKimiVariant(runtimes) {
|
|
|
784
999
|
return notices;
|
|
785
1000
|
}
|
|
786
1001
|
|
|
1002
|
+
// #3031: `--reclaim-kimi-legacy` only ever acts inside the kimi-code GLOBAL
|
|
1003
|
+
// install branch. Say so when it cannot act, rather than exiting 0 having
|
|
1004
|
+
// silently done nothing: the user asked for a cleanup, and silence is
|
|
1005
|
+
// indistinguishable from "it ran and found nothing". Not a hard error — it
|
|
1006
|
+
// stays composable with `--all`, where it is legitimately inert for the other
|
|
1007
|
+
// seventeen runtimes.
|
|
1008
|
+
if (hasReclaimKimiLegacy && !selectedRuntimes.includes('kimi-code')) {
|
|
1009
|
+
console.error(`${yellow}⚠ --reclaim-kimi-legacy ignored${reset} — it applies only to a --kimi-code install; nothing in ~/.kimi was touched.`);
|
|
1010
|
+
} else if (hasReclaimKimiLegacy && hasLocal) {
|
|
1011
|
+
// Scope, checked HERE rather than inside install(): kimi-code declares
|
|
1012
|
+
// hostBehaviors.localInstallDeferred, so install() returns early long before
|
|
1013
|
+
// the kimi-hooks-toml branch — a warning placed there would be unreachable.
|
|
1014
|
+
console.error(`${yellow}⚠ --reclaim-kimi-legacy ignored${reset} — the legacy root is a global location; re-run with --global to reclaim it.`);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
787
1017
|
if (selectedRuntimes.includes('kimi') || selectedRuntimes.includes('kimi-code')) {
|
|
788
1018
|
const kimiNotices = disambiguateKimiVariant(selectedRuntimes);
|
|
789
1019
|
for (const notice of kimiNotices) {
|
|
@@ -861,7 +1091,8 @@ Then re-run: npx ${pkg.name}@latest
|
|
|
861
1091
|
}
|
|
862
1092
|
|
|
863
1093
|
// getDirName (runtime -> local config dir name) now lives in
|
|
864
|
-
// runtime-name-policy.cjs (ADR-1508 / #1510 Phase 1); imported
|
|
1094
|
+
// runtime-name-policy.cjs (ADR-1508 / #1510 Phase 1); imported above for
|
|
1095
|
+
// install.js's own internal use only — #2876 retired the re-export.
|
|
865
1096
|
|
|
866
1097
|
/**
|
|
867
1098
|
* Get the config directory path relative to home directory for a runtime
|
|
@@ -947,6 +1178,14 @@ function parseConfigDirFromArgs(argsArray) {
|
|
|
947
1178
|
return null;
|
|
948
1179
|
}
|
|
949
1180
|
|
|
1181
|
+
// Parse --no-legacy-cleanup (#3799) — skip the legacy get-shit-done-cc scan
|
|
1182
|
+
// entirely. Some users run gsd-core alongside a live legacy install on
|
|
1183
|
+
// purpose; the scan is best-effbelt cleanup, never load-bearing for the
|
|
1184
|
+
// install itself.
|
|
1185
|
+
function parseNoLegacyCleanupArg(args = process.argv) {
|
|
1186
|
+
return args.includes('--no-legacy-cleanup');
|
|
1187
|
+
}
|
|
1188
|
+
|
|
950
1189
|
// Parse --config-dir argument
|
|
951
1190
|
function parseConfigDirArg() {
|
|
952
1191
|
const result = parseConfigDirFromArgs(args);
|
|
@@ -977,23 +1216,30 @@ if (hasUninstall) {
|
|
|
977
1216
|
|
|
978
1217
|
// Show help if requested
|
|
979
1218
|
if (hasHelp) {
|
|
980
|
-
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 (
|
|
1219
|
+
console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--kimi-code${reset} Install for Kimi Code only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--zcode${reset} Install for ZCode only\n ${cyan}--pi${reset} Install for Pi only\n ${cyan}--gemini${reset} Install for Gemini CLI only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}--no-legacy-cleanup${reset} Skip the legacy get-shit-done-cc artifact scan\n (an explicit --config-dir already scopes the scan to it)\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n and resolve the node runner at hook-fire time via\n hooks/gsd-node-runner.sh (WSL/Docker bind-mount\n setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--reclaim-kimi-legacy${reset} With --kimi-code: also remove the GSD hooks a\n pre-1.10.0 --kimi-code install orphaned in ~/.kimi.\n Opt-in — those artifacts are indistinguishable from\n Kimi CLI's own, so skip it if you use Kimi CLI too.\n ${cyan}--profile=<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 Code globally (its own ~/.kimi-code root)${reset}\n npx ${pkg.name} --kimi-code --global\n\n ${dim}# Kimi Code, also reclaiming hooks a pre-1.10.0 install left in ~/.kimi${reset}\n npx ${pkg.name} --kimi-code --global --reclaim-kimi-legacy\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline globally${reset}\n npx ${pkg.name} --cline --global\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# 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 Kimi CLI and Kimi Code are separate products with separate hook roots: use ${cyan}--kimi${reset} (${cyan}~/.kimi${reset}, ${cyan}KIMI_SHARE_DIR${reset}) or ${cyan}--kimi-code${reset} (${cyan}~/.kimi-code${reset}, ${cyan}KIMI_CODE_HOME${reset}).\n`);
|
|
981
1220
|
process.exit(0);
|
|
982
1221
|
}
|
|
983
1222
|
|
|
984
1223
|
// computePathPrefix: implementation moved to runtimeArtifactConversion._computePathPrefix
|
|
985
|
-
// (ADR-1508 / #1511 Phase 2 — single owner). The const binding above
|
|
986
|
-
//
|
|
1224
|
+
// (ADR-1508 / #1511 Phase 2 — single owner). The const binding above re-binds
|
|
1225
|
+
// it here for install.js's own internal call sites only — #2876 retired the
|
|
1226
|
+
// module.exports entry (zero consumers found; tests import
|
|
1227
|
+
// runtimeArtifactConversion._computePathPrefix directly).
|
|
987
1228
|
// Original doc: Compute the path prefix used for `@file` references in installed
|
|
988
1229
|
// command/skill markdown. For global installs under $HOME uses $HOME/... form;
|
|
989
1230
|
// OpenCode always uses the absolute path (#2376 Windows, #2831 macOS/Linux).
|
|
990
1231
|
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
|
|
1232
|
+
// resolveNodeRunner, resolveBashRunner, referencesHook are now owned by the
|
|
1233
|
+
// runtime-hooks-surface module. Import them here so install.js callers
|
|
1234
|
+
// continue to work and so there is a single implementation of these helpers.
|
|
1235
|
+
// (normalizeNodePath was re-bound here too until #2876 found bin/install.js
|
|
1236
|
+
// had no internal caller and no export consumer for it — hooksSurface owns
|
|
1237
|
+
// the single implementation now, used internally by resolveNodeRunner there.)
|
|
996
1238
|
const resolveNodeRunner = hooksSurface.resolveNodeRunner;
|
|
1239
|
+
// #3662: the runtime-resolving runner token for managed JS hooks — the baked
|
|
1240
|
+
// absolute node path tried FIRST, then `command -v node`, then well-known
|
|
1241
|
+
// layouts, resolved by the shell at hook-fire time instead of bake time.
|
|
1242
|
+
const buildNodeRunnerChainToken = hooksSurface.buildNodeRunnerChainToken;
|
|
997
1243
|
const resolveBashRunner = hooksSurface.resolveBashRunner;
|
|
998
1244
|
// referencesHook: pure predicate over hook entry objects, shared between
|
|
999
1245
|
// install() and finishInstall() (ADR-857 phase 5f-1b).
|
|
@@ -1010,38 +1256,27 @@ const removeKimiHooksToml = hooksSurface.removeKimiHooksToml;
|
|
|
1010
1256
|
// callers continue to work and there is a single implementation. (All call
|
|
1011
1257
|
// sites are below this line, so the const binding has no TDZ hazard.)
|
|
1012
1258
|
const processAttribution = runtimeArtifactConversion.processAttribution;
|
|
1013
|
-
// computePathPrefix
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
//
|
|
1022
|
-
// conversion module. install.js re-binds (does not re-define) these so there
|
|
1023
|
-
// is exactly one body — the generative-drift hazard the dedup removes. The two
|
|
1024
|
-
// private helpers (getAugmentSkillAdapterHeader, convertSlashCommandsToAugmentSkillMentions)
|
|
1025
|
-
// live only in the conversion module now; they are no longer duplicated here.
|
|
1259
|
+
// computePathPrefix: implementation lives in runtimeArtifactConversion
|
|
1260
|
+
// (ADR-1508 / #1511 Phase 2 — single owner). Re-bound here so install.js call
|
|
1261
|
+
// sites continue to work. #2876 retired the sibling
|
|
1262
|
+
// applyRuntimeContentRewritesInPlace / applyRuntimeContentRewritesForCommandsInPlace
|
|
1263
|
+
// re-bindings that used to sit alongside it, and the entire #1675 Augment
|
|
1264
|
+
// converter family re-binding (convertClaudeToAugmentMarkdown /
|
|
1265
|
+
// convertClaudeCommandToAugmentSkill / convertClaudeAgentToAugmentAgent) that
|
|
1266
|
+
// used to follow — bin/install.js had no internal caller for any of them (the
|
|
1267
|
+
// descriptor pipeline in runtimeArtifactConversion calls them directly).
|
|
1026
1268
|
// (All call sites are below this line → no TDZ hazard.)
|
|
1027
|
-
const
|
|
1028
|
-
const convertClaudeCommandToAugmentSkill = runtimeArtifactConversion.convertClaudeCommandToAugmentSkill;
|
|
1029
|
-
const convertClaudeAgentToAugmentAgent = runtimeArtifactConversion.convertClaudeAgentToAugmentAgent;
|
|
1269
|
+
const computePathPrefix = runtimeArtifactConversion._computePathPrefix;
|
|
1030
1270
|
// #2931 (ADR-1508): the windsurf converter family is single-sourced in the
|
|
1031
|
-
// conversion module
|
|
1032
|
-
//
|
|
1033
|
-
// generative-drift hazard the dedup removes.
|
|
1034
|
-
//
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
// guard describe block), not tests/enh-1511-rewrite-engine-relocation.test.cjs
|
|
1039
|
-
// as the Augment comment above stated — that reference was stale.
|
|
1271
|
+
// conversion module. install.js re-binds (does not re-define) the one member
|
|
1272
|
+
// it still calls internally so there is exactly one body — the
|
|
1273
|
+
// generative-drift hazard the dedup removes. #2876 retired the sibling
|
|
1274
|
+
// convertClaudeCommandToWindsurfSkill / convertClaudeCommandToWindsurfWorkflow /
|
|
1275
|
+
// convertClaudeAgentToWindsurfAgent re-bindings — bin/install.js had no
|
|
1276
|
+
// internal caller for any of them (the descriptor pipeline in
|
|
1277
|
+
// runtimeArtifactConversion calls them directly).
|
|
1040
1278
|
// (All call sites are below this line → no TDZ hazard.)
|
|
1041
1279
|
const convertClaudeToWindsurfMarkdown = runtimeArtifactConversion.convertClaudeToWindsurfMarkdown;
|
|
1042
|
-
const convertClaudeCommandToWindsurfSkill = runtimeArtifactConversion.convertClaudeCommandToWindsurfSkill;
|
|
1043
|
-
const convertClaudeCommandToWindsurfWorkflow = runtimeArtifactConversion.convertClaudeCommandToWindsurfWorkflow;
|
|
1044
|
-
const convertClaudeAgentToWindsurfAgent = runtimeArtifactConversion.convertClaudeAgentToWindsurfAgent;
|
|
1045
1280
|
// #2931 (ADR-1508): single-sourced in the conversion module — was a second,
|
|
1046
1281
|
// unlinked verbatim copy here (used by the local Cursor/Trae/CodeBuddy/Cline
|
|
1047
1282
|
// converters below), the exact drift class this PR exists to reduce. Verified
|
|
@@ -1057,116 +1292,14 @@ function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
|
|
|
1057
1292
|
return hooksSurface.rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts);
|
|
1058
1293
|
}
|
|
1059
1294
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
* Returns null when `absoluteRunner` is null so callers can warn-and-skip
|
|
1069
|
-
* registration — emitting a broken bare-node hook is strictly worse than
|
|
1070
|
-
* not registering one (the user can re-run install once node is on PATH).
|
|
1071
|
-
*
|
|
1072
|
-
* @param {string} targetDir - Resolved absolute Codex config dir (e.g. ~/.codex).
|
|
1073
|
-
* @param {{ absoluteRunner: string|null, eol?: string }} opts
|
|
1074
|
-
* absoluteRunner: result of resolveNodeRunner() — a JSON-stringified
|
|
1075
|
-
* absolute node path with forward slashes (e.g. `"/usr/local/bin/node"`),
|
|
1076
|
-
* or null when process.execPath was unavailable.
|
|
1077
|
-
* eol: line ending to emit ('\n' or '\r\n') — caller passes
|
|
1078
|
-
* detectLineEnding(configContent) so existing CRLF files stay CRLF.
|
|
1079
|
-
* Defaults to '\n'.
|
|
1080
|
-
* @returns {string|null} The toml block to append, or null on missing runner.
|
|
1081
|
-
*/
|
|
1082
|
-
function buildCodexHookBlock(targetDir, opts) {
|
|
1083
|
-
return hooksSurface.buildCodexHookBlock(targetDir, opts);
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
/**
|
|
1087
|
-
* Rewrite legacy bare-`node` managed-hook command lines in a Codex
|
|
1088
|
-
* config.toml string to use the absolute Node runner. Mirror of
|
|
1089
|
-
* rewriteLegacyManagedNodeHookCommands but for the toml surface (#3017).
|
|
1090
|
-
*
|
|
1091
|
-
* Only rewrites entries whose script basename matches CODEX_MANAGED_HOOK_BASENAMES
|
|
1092
|
-
* (basename equality, not substring containment) — user-authored bare-node
|
|
1093
|
-
* hooks pointing at scripts outside the managed allowlist are left alone.
|
|
1094
|
-
*
|
|
1095
|
-
* @param {string} content - Current config.toml contents.
|
|
1096
|
-
* @param {string|null} absoluteRunner - Result of resolveNodeRunner().
|
|
1097
|
-
* @returns {{ content: string, changed: boolean }}
|
|
1098
|
-
*/
|
|
1099
|
-
function rewriteLegacyCodexHookBlock(content, absoluteRunner, opts) {
|
|
1100
|
-
return hooksSurface.rewriteLegacyCodexHookBlock(content, absoluteRunner, opts);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
/**
|
|
1104
|
-
* Generic reconcile helper: ensure hooks.json contains exactly one managed GSD
|
|
1105
|
-
* hook entry for `eventName`, while preserving all user-owned entries.
|
|
1106
|
-
*
|
|
1107
|
-
* Supports both known hooks.json shapes:
|
|
1108
|
-
* 1) { "<EventName>": [...] }
|
|
1109
|
-
* 2) { "hooks": { "<EventName>": [...] } }
|
|
1110
|
-
*
|
|
1111
|
-
* @param {string} targetDir - Codex config dir (e.g. ~/.codex or <project>/.codex).
|
|
1112
|
-
* @param {string} eventName - Codex hook event name (e.g. 'SessionStart', 'Stop').
|
|
1113
|
-
* @param {{ managedCommand?: string|null, commandWindows?: string|null, matcher?: string|null, timeout?: number|null }} opts
|
|
1114
|
-
* managedCommand: POSIX hook command string to register, or null to remove.
|
|
1115
|
-
* commandWindows: Windows .cmd shim path to emit as `commandWindows` field
|
|
1116
|
-
* (#772). When provided, Codex uses this path on Windows and `managedCommand`
|
|
1117
|
-
* on POSIX without needing per-platform config regeneration.
|
|
1118
|
-
* matcher: optional Codex MatcherGroup pattern (e.g. 'Bash|Edit|Write').
|
|
1119
|
-
* timeout: optional timeout in seconds.
|
|
1120
|
-
* @returns {{ changed: boolean, wrote: boolean, path: string }}
|
|
1121
|
-
*/
|
|
1122
|
-
function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
|
|
1123
|
-
return hooksSurface.reconcileCodexHooksJsonEvent(targetDir, eventName, opts);
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
/**
|
|
1127
|
-
* Reconcile the GSD-managed SessionStart hook entry in hooks.json.
|
|
1128
|
-
* Delegates to the generic reconcileCodexHooksJsonEvent helper.
|
|
1129
|
-
*
|
|
1130
|
-
* @param {string} targetDir
|
|
1131
|
-
* @param {{ managedCommand?: string|null, commandWindows?: string|null }} opts
|
|
1132
|
-
* @returns {{ changed: boolean, wrote: boolean, path: string }}
|
|
1133
|
-
*/
|
|
1134
|
-
function reconcileCodexHooksJsonSessionStart(targetDir, opts = {}) {
|
|
1135
|
-
return hooksSurface.reconcileCodexHooksJsonSessionStart(targetDir, opts);
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
/**
|
|
1139
|
-
* Build a typed IR for the Codex hook .cmd shim used on Windows (#3426).
|
|
1140
|
-
*
|
|
1141
|
-
* On Windows, Codex runs hook commands from a PowerShell/cmd execution
|
|
1142
|
-
* environment. The previous command format was:
|
|
1143
|
-
*
|
|
1144
|
-
* "C:/Program Files/nodejs/node.exe" "C:/path/.codex/hooks/gsd-check-update.js"
|
|
1145
|
-
*
|
|
1146
|
-
* This caused `bash.exe: bash.exe: cannot execute binary file` because
|
|
1147
|
-
* Codex's hook dispatch shell (Git Bash / MSYS) tried to POSIX-exec node.exe
|
|
1148
|
-
* (a Windows PE binary) via execvp(), which fails with ENOEXEC on Windows PE
|
|
1149
|
-
* binaries that the MSYS layer doesn't know how to fork-exec natively.
|
|
1150
|
-
*
|
|
1151
|
-
* Fix: write a .cmd shim (using the same CRLF .cmd shim pattern) whose
|
|
1152
|
-
* content is `@ECHO OFF / @SETLOCAL / @"node.exe" "script.js" %*`.
|
|
1153
|
-
* cmd.exe executes
|
|
1154
|
-
* .cmd natively via CreateProcess — no POSIX exec layer, no MSYS shebang
|
|
1155
|
-
* walk, no PE binary fork-exec failure.
|
|
1156
|
-
*
|
|
1157
|
-
* Returns the typed IR `{ invocation, cmdPath, hookCommand, render }` so
|
|
1158
|
-
* callers can assert on the structured shape (CONTRIBUTING.md L558–L565
|
|
1159
|
-
* IR-first discipline). Returns null when absoluteRunnerToken is null so
|
|
1160
|
-
* callers can warn-and-skip instead of writing a broken hook.
|
|
1161
|
-
*
|
|
1162
|
-
* @param {string} scriptAbsPath - Absolute path to the .js hook script.
|
|
1163
|
-
* @param {string|null} absoluteRunnerToken - JSON-quoted absolute node path
|
|
1164
|
-
* (result of resolveNodeRunner()), e.g. `"C:/Program Files/nodejs/node.exe"`.
|
|
1165
|
-
* @returns {{ invocation: { interpreter: string, target: string }, cmdPath: string, hookCommand: string, render: { cmd: () => string } }|null}
|
|
1166
|
-
*/
|
|
1167
|
-
function buildCodexHookWindowsShimIR(scriptAbsPath, absoluteRunnerToken) {
|
|
1168
|
-
return hooksSurface.buildCodexHookWindowsShimIR(scriptAbsPath, absoluteRunnerToken);
|
|
1169
|
-
}
|
|
1295
|
+
// #2876: reconcileManagedShellHookCommands, buildCodexHookBlock,
|
|
1296
|
+
// rewriteLegacyCodexHookBlock, reconcileCodexHooksJsonEvent, and
|
|
1297
|
+
// reconcileCodexHooksJsonSessionStart used to be re-bound here as one-line
|
|
1298
|
+
// delegates to the equivalent hooksSurface.* implementations. None had an
|
|
1299
|
+
// install.js internal caller or an export consumer (tests import all five
|
|
1300
|
+
// directly from gsd-core/bin/lib/runtime-hooks-surface.cjs), so the retired
|
|
1301
|
+
// bindings were dead code with no reachable body — removed rather than kept
|
|
1302
|
+
// as unreachable wrappers.
|
|
1170
1303
|
|
|
1171
1304
|
/**
|
|
1172
1305
|
* Ensure Codex hooks.json contains exactly one managed SessionStart
|
|
@@ -1359,281 +1492,44 @@ function readSettings(settingsPath) {
|
|
|
1359
1492
|
}
|
|
1360
1493
|
|
|
1361
1494
|
/**
|
|
1362
|
-
* Write settings.json with proper formatting
|
|
1363
|
-
*/
|
|
1364
|
-
function writeSettings(settingsPath, settings) {
|
|
1365
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
/**
|
|
1369
|
-
* Read model_overrides from ~/.gsd/defaults.json at install time.
|
|
1370
|
-
* Returns an object mapping agent names to model IDs, or null if the file
|
|
1371
|
-
* doesn't exist or has no model_overrides entry.
|
|
1372
|
-
* Used by Codex TOML and OpenCode agent file generators to embed per-agent
|
|
1373
|
-
* model assignments so that model_overrides is respected on non-Claude runtimes (#2256).
|
|
1374
|
-
*/
|
|
1375
|
-
function readGsdGlobalModelOverrides(options = {}) {
|
|
1376
|
-
try {
|
|
1377
|
-
const home = options.homedir ? options.homedir() : os.homedir();
|
|
1378
|
-
const defaultsPath = path.join(home, '.gsd', 'defaults.json');
|
|
1379
|
-
if (!fs.existsSync(defaultsPath)) return null;
|
|
1380
|
-
const raw = fs.readFileSync(defaultsPath, 'utf-8');
|
|
1381
|
-
const parsed = JSON.parse(raw);
|
|
1382
|
-
const overrides = parsed.model_overrides;
|
|
1383
|
-
if (!overrides || typeof overrides !== 'object') return null;
|
|
1384
|
-
return overrides;
|
|
1385
|
-
} catch {
|
|
1386
|
-
return null;
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
/**
|
|
1391
|
-
* Effective per-agent model_overrides for the Codex / OpenCode install paths.
|
|
1392
|
-
*
|
|
1393
|
-
* Merges `~/.gsd/defaults.json` (global) with per-project
|
|
1394
|
-
* `<project>/.planning/config.json`. Per-project keys win on conflict so a
|
|
1395
|
-
* user can tune a single agent's model in one repo without re-setting the
|
|
1396
|
-
* global defaults for every other repo. Non-conflicting keys from both
|
|
1397
|
-
* sources are preserved.
|
|
1398
|
-
*
|
|
1399
|
-
* This is the fix for #2256: both adapters previously read only the global
|
|
1400
|
-
* file, so a per-project `model_overrides` (the common case the reporter
|
|
1401
|
-
* described — a per-project override for `gsd-codebase-mapper` in
|
|
1402
|
-
* `.planning/config.json`) was silently dropped and child agents inherited
|
|
1403
|
-
* the session default.
|
|
1404
|
-
*
|
|
1405
|
-
* `targetDir` is the consuming runtime's install root (e.g. `~/.codex` for
|
|
1406
|
-
* a global install, or `<project>/.codex` for a local install). We walk up
|
|
1407
|
-
* from there looking for `.planning/` so both cases resolve the correct
|
|
1408
|
-
* project root. When `targetDir` is null/undefined only the global file is
|
|
1409
|
-
* consulted (matches prior behavior for code paths that have no project
|
|
1410
|
-
* context).
|
|
1411
|
-
*
|
|
1412
|
-
* Returns a plain `{ agentName: modelId }` object, or `null` when neither
|
|
1413
|
-
* source defines `model_overrides`.
|
|
1414
|
-
*/
|
|
1415
|
-
function readGsdEffectiveModelOverrides(targetDir = null, options = {}) {
|
|
1416
|
-
const global = readGsdGlobalModelOverrides(options);
|
|
1417
|
-
|
|
1418
|
-
let projectOverrides = null;
|
|
1419
|
-
if (targetDir) {
|
|
1420
|
-
let probeDir = path.resolve(targetDir);
|
|
1421
|
-
for (let depth = 0; depth < 8; depth += 1) {
|
|
1422
|
-
const candidate = path.join(probeDir, '.planning', 'config.json');
|
|
1423
|
-
if (fs.existsSync(candidate)) {
|
|
1424
|
-
try {
|
|
1425
|
-
const parsed = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
1426
|
-
if (parsed && typeof parsed === 'object' && parsed.model_overrides
|
|
1427
|
-
&& typeof parsed.model_overrides === 'object') {
|
|
1428
|
-
projectOverrides = parsed.model_overrides;
|
|
1429
|
-
}
|
|
1430
|
-
} catch {
|
|
1431
|
-
// Malformed config.json — fall back to global; readGsdRuntimeProfileResolver
|
|
1432
|
-
// surfaces a parse warning via _readGsdConfigFile already.
|
|
1433
|
-
}
|
|
1434
|
-
break;
|
|
1435
|
-
}
|
|
1436
|
-
const parent = path.dirname(probeDir);
|
|
1437
|
-
if (parent === probeDir) break;
|
|
1438
|
-
probeDir = parent;
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
if (!global && !projectOverrides) return null;
|
|
1443
|
-
// Per-project wins on conflict; preserve non-conflicting global keys.
|
|
1444
|
-
return { ...(global || {}), ...(projectOverrides || {}) };
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
/**
|
|
1448
|
-
* #443 — Inject `effort: <value>` into YAML frontmatter of a Claude .md agent
|
|
1449
|
-
* file in a newline-agnostic way (LF and CRLF source files are both handled).
|
|
1450
|
-
*
|
|
1451
|
-
* The function:
|
|
1452
|
-
* - Detects the file's EOL (CRLF if the first `---` line ends with \r\n,
|
|
1453
|
-
* otherwise LF).
|
|
1454
|
-
* - Skips injection if an `effort:` key already exists in the frontmatter
|
|
1455
|
-
* (idempotent).
|
|
1456
|
-
* - Inserts `effort: <value>` immediately before the closing `---` delimiter,
|
|
1457
|
-
* using the same EOL as the surrounding frontmatter so the output file
|
|
1458
|
-
* stays EOL-consistent.
|
|
1459
|
-
* - Returns the original content unchanged when no YAML frontmatter is found.
|
|
1460
|
-
*
|
|
1461
|
-
* @param {string} content Raw file content (may have LF or CRLF endings).
|
|
1462
|
-
* @param {string} effortValue Rendered effort string, e.g. "xhigh".
|
|
1463
|
-
* @returns {string} Updated content with `effort:` injected, or the
|
|
1464
|
-
* original content when no frontmatter is found.
|
|
1465
|
-
*/
|
|
1466
|
-
function injectEffortFrontmatter(content, effortValue) {
|
|
1467
|
-
// Detect the dominant EOL from the first line (the opening `---`).
|
|
1468
|
-
// If the very first `---` is followed by \r\n, treat the whole file as CRLF.
|
|
1469
|
-
const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
|
|
1470
|
-
|
|
1471
|
-
// Build a frontmatter-matching regex that tolerates an optional \r before
|
|
1472
|
-
// each \n, so we handle both LF and CRLF files without needing to normalise
|
|
1473
|
-
// the whole content.
|
|
1474
|
-
//
|
|
1475
|
-
// Breakdown:
|
|
1476
|
-
// ^---\r?\n — opening delimiter (with optional \r)
|
|
1477
|
-
// ([\s\S]*?) — frontmatter body (non-greedy)
|
|
1478
|
-
// ^---\r?$ — closing delimiter line (optional \r, $ before \n in
|
|
1479
|
-
// multiline mode)
|
|
1480
|
-
// (\r?\n|$) — newline after closing --- (or end of string)
|
|
1481
|
-
//
|
|
1482
|
-
// The `m` flag makes ^ / $ match at every line boundary.
|
|
1483
|
-
const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
|
|
1484
|
-
const match = fmRe.exec(content);
|
|
1485
|
-
if (!match) return content; // no YAML frontmatter — leave unchanged
|
|
1486
|
-
|
|
1487
|
-
// Idempotency guard: don't insert a second effort: line.
|
|
1488
|
-
const fmBody = match[1]; // content between the two `---` lines
|
|
1489
|
-
if (/^effort:/m.test(fmBody)) return content;
|
|
1490
|
-
|
|
1491
|
-
// Locate the exact position of the closing `---` line so we can insert
|
|
1492
|
-
// before it using a simple string splice (avoids re-running the regex and
|
|
1493
|
-
// avoids any edge-cases with $ matching \r differently per engine).
|
|
1494
|
-
const closeIdx = match.index + 4 + fmBody.length; // 4 = len("---\n") (opening)
|
|
1495
|
-
// Actually compute based on the full match start + captured group length:
|
|
1496
|
-
// match[0] = full frontmatter block; match.index = start of that block.
|
|
1497
|
-
// The closing `---` starts at: match.index + ("---" + eol).length + fmBody.length
|
|
1498
|
-
const openLen = 3 + eol.length; // "---" + eol
|
|
1499
|
-
const closingStart = match.index + openLen + fmBody.length;
|
|
1500
|
-
|
|
1501
|
-
const before = content.slice(0, closingStart);
|
|
1502
|
-
const after = content.slice(closingStart);
|
|
1503
|
-
return `${before}effort: ${effortValue}${eol}${after}`;
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
/**
|
|
1507
|
-
* #767 — Inject `disallowedTools: <value>` into the YAML frontmatter of a Claude .md agent.
|
|
1508
|
-
* Mirrors injectEffortFrontmatter: idempotent (skips if disallowedTools: already present),
|
|
1509
|
-
* inserts immediately before the closing `---`. Claude-only — never call for other runtimes,
|
|
1510
|
-
* which break on unknown frontmatter keys.
|
|
1511
|
-
*/
|
|
1512
|
-
function injectDisallowedToolsFrontmatter(content, disallowedValue) {
|
|
1513
|
-
// Detect the dominant EOL from the first line (the opening `---`).
|
|
1514
|
-
// If the very first `---` is followed by \r\n, treat the whole file as CRLF.
|
|
1515
|
-
const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
|
|
1516
|
-
|
|
1517
|
-
// Build a frontmatter-matching regex that tolerates an optional \r before
|
|
1518
|
-
// each \n, so we handle both LF and CRLF files without needing to normalise
|
|
1519
|
-
// the whole content.
|
|
1520
|
-
const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
|
|
1521
|
-
const match = fmRe.exec(content);
|
|
1522
|
-
if (!match) return content; // no YAML frontmatter — leave unchanged
|
|
1523
|
-
|
|
1524
|
-
// Idempotency guard: don't insert a second disallowedTools: line.
|
|
1525
|
-
const fmBody = match[1]; // content between the two `---` lines
|
|
1526
|
-
if (/^disallowedTools:/m.test(fmBody)) return content;
|
|
1527
|
-
|
|
1528
|
-
// Locate the exact position of the closing `---` line so we can insert
|
|
1529
|
-
// before it using a simple string splice.
|
|
1530
|
-
const openLen = 3 + eol.length; // "---" + eol
|
|
1531
|
-
const closingStart = match.index + openLen + fmBody.length;
|
|
1532
|
-
|
|
1533
|
-
const before = content.slice(0, closingStart);
|
|
1534
|
-
const after = content.slice(closingStart);
|
|
1535
|
-
return `${before}disallowedTools: ${disallowedValue}${eol}${after}`;
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
// #767 — Read-only verifier/auditor agents get a Claude-Code disallowedTools deny-list.
|
|
1539
|
-
// Group A (pure read-only) deny Write,Edit,MultiEdit. Group B report-writers Write one
|
|
1540
|
-
// output file so they deny only Edit,MultiEdit. gsd-nyquist-auditor is intentionally
|
|
1541
|
-
// excluded (it legitimately uses Write AND Edit to create/patch test files).
|
|
1542
|
-
const READONLY_AGENT_DISALLOWED_TOOLS = {
|
|
1543
|
-
'gsd-plan-checker': 'Write, Edit, MultiEdit',
|
|
1544
|
-
'gsd-integration-checker': 'Write, Edit, MultiEdit',
|
|
1545
|
-
'gsd-ui-checker': 'Write, Edit, MultiEdit',
|
|
1546
|
-
'gsd-verifier': 'Edit, MultiEdit',
|
|
1547
|
-
'gsd-doc-verifier': 'Edit, MultiEdit',
|
|
1548
|
-
'gsd-eval-auditor': 'Edit, MultiEdit',
|
|
1549
|
-
'gsd-ui-auditor': 'Edit, MultiEdit',
|
|
1550
|
-
};
|
|
1551
|
-
|
|
1552
|
-
/**
|
|
1553
|
-
* #2517 — Build a runtime-aware tier resolver for the install path.
|
|
1495
|
+
* Write settings.json with proper formatting.
|
|
1554
1496
|
*
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1559
|
-
* — actually holds end-to-end (review finding #1).
|
|
1497
|
+
* Atomic (temp+rename) because hosts discard the ENTIRE settings file on any
|
|
1498
|
+
* parse failure, so a truncated write costs the user every hook, permission,
|
|
1499
|
+
* and statusline they have — not just GSD's entries. This is the sole writer
|
|
1500
|
+
* of that surface for six runtimes.
|
|
1560
1501
|
*
|
|
1561
|
-
* `
|
|
1562
|
-
*
|
|
1563
|
-
* relative to the user's project. When `targetDir` is null/undefined, only the
|
|
1564
|
-
* global defaults are consulted.
|
|
1565
|
-
*
|
|
1566
|
-
* Returns null if no `runtime` is configured (preserves prior behavior — only
|
|
1567
|
-
* model_overrides is embedded, no tier/reasoning-effort inference). Returns
|
|
1568
|
-
* null when `model_profile` is `inherit` so the literal alias passes through
|
|
1569
|
-
* unchanged.
|
|
1570
|
-
*
|
|
1571
|
-
* Returns { runtime, resolve(agentName) -> { model, reasoning_effort? } | null }
|
|
1502
|
+
* `atomicWriteFileSync` is declared further down this file; it is dereferenced
|
|
1503
|
+
* at call time, after module evaluation, so the ordering is safe.
|
|
1572
1504
|
*/
|
|
1573
|
-
function
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
// project, and vice versa.
|
|
1602
|
-
const merged = {
|
|
1603
|
-
runtime:
|
|
1604
|
-
(projectConfig && projectConfig.runtime) ||
|
|
1605
|
-
(homeDefaults && homeDefaults.runtime) ||
|
|
1606
|
-
null,
|
|
1607
|
-
model_profile:
|
|
1608
|
-
(projectConfig && projectConfig.model_profile) ||
|
|
1609
|
-
(homeDefaults && homeDefaults.model_profile) ||
|
|
1610
|
-
'balanced',
|
|
1611
|
-
model_profile_overrides:
|
|
1612
|
-
(projectConfig && projectConfig.model_profile_overrides) ||
|
|
1613
|
-
(homeDefaults && homeDefaults.model_profile_overrides) ||
|
|
1614
|
-
null,
|
|
1615
|
-
};
|
|
1616
|
-
|
|
1617
|
-
if (!merged.runtime) return null;
|
|
1618
|
-
|
|
1619
|
-
const profile = String(merged.model_profile).toLowerCase();
|
|
1620
|
-
if (profile === 'inherit') return null;
|
|
1621
|
-
|
|
1622
|
-
return {
|
|
1623
|
-
runtime: merged.runtime,
|
|
1624
|
-
resolve(agentName) {
|
|
1625
|
-
const agentModels = GSD_MODEL_PROFILES[agentName];
|
|
1626
|
-
if (!agentModels) return null;
|
|
1627
|
-
const tier = agentModels[profile] || agentModels.balanced;
|
|
1628
|
-
if (!tier) return null;
|
|
1629
|
-
return gsdResolveTierEntry({
|
|
1630
|
-
runtime: merged.runtime,
|
|
1631
|
-
tier,
|
|
1632
|
-
overrides: merged.model_profile_overrides,
|
|
1633
|
-
});
|
|
1634
|
-
},
|
|
1635
|
-
};
|
|
1636
|
-
}
|
|
1505
|
+
function writeSettings(settingsPath, settings) {
|
|
1506
|
+
atomicWriteFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// #2875 Part 2 (J8): model-override resolution (readGsdGlobalModelOverrides /
|
|
1510
|
+
// readGsdEffectiveModelOverrides / readGsdRuntimeProfileResolver, plus the
|
|
1511
|
+
// shared resolveAgentModelOverride precedence chain) was extracted into the
|
|
1512
|
+
// shipped gsd-core/bin/lib/install-model-override-resolver.cjs, mirroring
|
|
1513
|
+
// install-effort-resolver.cjs's existing #2071 precedent, so the descriptor-
|
|
1514
|
+
// driven agents pipeline (runtime-artifact-layout.cts's convertedAgentsKind)
|
|
1515
|
+
// and this installer resolve model_overrides / model_profile_overrides
|
|
1516
|
+
// through the SAME code — a single source of truth for the precedence chain
|
|
1517
|
+
// the inline agent loop below used to duplicate across ~24 lines per runtime
|
|
1518
|
+
// (kilo, opencode). See install-model-override-resolver.cts's module doc.
|
|
1519
|
+
const {
|
|
1520
|
+
readGsdEffectiveModelOverrides,
|
|
1521
|
+
readGsdRuntimeProfileResolver,
|
|
1522
|
+
resolveAgentModelOverride,
|
|
1523
|
+
} = require(path.join(_gsdLibDir, 'install-model-override-resolver.cjs'));
|
|
1524
|
+
|
|
1525
|
+
// #2875 Part 2: effort frontmatter injection moved to runtimeArtifactConversion
|
|
1526
|
+
// (single source of truth with the descriptor pipeline's
|
|
1527
|
+
// applyAgentFrontmatterExtensions step, which now also owns disallowedTools
|
|
1528
|
+
// injection + the read-only agent deny-list internally — see its module doc
|
|
1529
|
+
// in src/runtime-artifact-conversion.cts). injectEffortFrontmatter used to be
|
|
1530
|
+
// re-bound here purely to stay on this module's export surface; #2876 found
|
|
1531
|
+
// no install.js internal caller either (tests import it directly from
|
|
1532
|
+
// gsd-core/bin/lib/runtime-artifact-conversion.cjs) and retired the binding.
|
|
1637
1533
|
|
|
1638
1534
|
// Cache for attribution settings (populated once per runtime during install)
|
|
1639
1535
|
const attributionCache = new Map();
|
|
@@ -1922,10 +1818,6 @@ function buildKiloAgentPermissionBlock(claudeTools) {
|
|
|
1922
1818
|
return lines;
|
|
1923
1819
|
}
|
|
1924
1820
|
|
|
1925
|
-
function escapeRegExp(value) {
|
|
1926
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
1821
|
function replaceRelativePathReference(content, fromPath, toPath) {
|
|
1930
1822
|
const escapedPath = escapeRegExp(fromPath);
|
|
1931
1823
|
return content.replace(
|
|
@@ -2046,12 +1938,6 @@ function skillFrontmatterName(skillDirName) {
|
|
|
2046
1938
|
return skillDirName;
|
|
2047
1939
|
}
|
|
2048
1940
|
|
|
2049
|
-
function normalizeClaudeSkillEffort(effort) {
|
|
2050
|
-
// #3039: `max` is rejected by Anthropic models when extended thinking is disabled.
|
|
2051
|
-
if (effort === 'xhigh' || effort === 'max') return 'high';
|
|
2052
|
-
return effort;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
1941
|
/**
|
|
2056
1942
|
* Qwen Code skills accept an optional numeric `priority` frontmatter field.
|
|
2057
1943
|
* Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified
|
|
@@ -2108,10 +1994,11 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
|
|
|
2108
1994
|
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
2109
1995
|
const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
|
|
2110
1996
|
const agent = extractFrontmatterField(frontmatter, 'agent');
|
|
2111
|
-
// #769: preserve context:
|
|
2112
|
-
//
|
|
1997
|
+
// #769: preserve context: from source command files so it is emitted into
|
|
1998
|
+
// the installed SKILL.md frontmatter unchanged. (#3151: effort: is no longer
|
|
1999
|
+
// emitted into skill frontmatter — a static effort value invalidates the
|
|
2000
|
+
// caller's prompt cache at both scope boundaries.)
|
|
2113
2001
|
const context = extractFrontmatterField(frontmatter, 'context');
|
|
2114
|
-
const effort = extractFrontmatterField(frontmatter, 'effort');
|
|
2115
2002
|
|
|
2116
2003
|
// Preserve allowed-tools as YAML multiline list (Claude native format)
|
|
2117
2004
|
const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
@@ -2145,12 +2032,13 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
|
|
|
2145
2032
|
}
|
|
2146
2033
|
if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`;
|
|
2147
2034
|
if (agent) fm += `agent: ${agent}\n`;
|
|
2148
|
-
// #769: emit context:
|
|
2149
|
-
//
|
|
2150
|
-
//
|
|
2151
|
-
//
|
|
2035
|
+
// #769: emit context: when present so the runtime can honour it natively
|
|
2036
|
+
// (context: fork = isolated subagent window). Claude-specific; unknown
|
|
2037
|
+
// frontmatter fields are silently ignored by other runtimes (backward-compatible).
|
|
2038
|
+
// (#3151: effort: is intentionally NOT emitted into skill frontmatter — a
|
|
2039
|
+
// static effort value changes output_config.effort on invocation and
|
|
2040
|
+
// invalidates the caller's prompt cache at both scope boundaries.)
|
|
2152
2041
|
if (context) fm += `context: ${context}\n`;
|
|
2153
|
-
if (effort) fm += `effort: ${normalizeClaudeSkillEffort(effort)}\n`;
|
|
2154
2042
|
if (toolsBlock) fm += toolsBlock;
|
|
2155
2043
|
fm += '---';
|
|
2156
2044
|
|
|
@@ -2488,7 +2376,8 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) {
|
|
|
2488
2376
|
/**
|
|
2489
2377
|
* Apply Antigravity-specific content conversion — path replacement + command name conversion.
|
|
2490
2378
|
* Path mappings depend on install mode:
|
|
2491
|
-
* Global: ~/.claude/ → ~/.gemini/
|
|
2379
|
+
* Global: ~/.claude/skills/ → ~/.gemini/config/skills/ (#3738),
|
|
2380
|
+
* ~/.claude/ → ~/.gemini/antigravity/, ./.claude/ → ./.agents/
|
|
2492
2381
|
* Local: ~/.claude/ → .agents/, ./.claude/ → ./.agents/
|
|
2493
2382
|
* Applied to ALL Antigravity content (skills, agents, engine files).
|
|
2494
2383
|
* @param {string} content - Source content to convert
|
|
@@ -2497,6 +2386,19 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) {
|
|
|
2497
2386
|
function convertClaudeToAntigravityContent(content, isGlobal = false) {
|
|
2498
2387
|
let c = content;
|
|
2499
2388
|
if (isGlobal) {
|
|
2389
|
+
// #3738: global skills install under ~/.gemini/config/skills (the dir AGY
|
|
2390
|
+
// scans for global discovery), so skills-path references must divert there
|
|
2391
|
+
// — BEFORE the configHome rewrite below, which is correct for gsd-core
|
|
2392
|
+
// runtime-file references (settings, workflows, VERSION) but wrong for the
|
|
2393
|
+
// skills dir itself. Mirrors src/runtime-artifact-conversion.cts (ADR-1508
|
|
2394
|
+
// keeps bin/install.js hand-authored; the two copies must stay in sync).
|
|
2395
|
+
c = c.replace(/\$HOME\/\.claude\/skills\//g, '$HOME/.gemini/config/skills/');
|
|
2396
|
+
c = c.replace(/~\/\.claude\/skills\//g, '~/.gemini/config/skills/');
|
|
2397
|
+
// Bare skills form (no trailing slash) — must also precede the generic
|
|
2398
|
+
// slash rule, which would otherwise divert it to the retired configHome
|
|
2399
|
+
// path ($HOME/.gemini/antigravity/skills).
|
|
2400
|
+
c = c.replace(/\$HOME\/\.claude\/skills\b/g, '$HOME/.gemini/config/skills');
|
|
2401
|
+
c = c.replace(/~\/\.claude\/skills\b/g, '~/.gemini/config/skills');
|
|
2500
2402
|
c = c.replace(/\$HOME\/\.claude\//g, '$HOME/.gemini/antigravity/');
|
|
2501
2403
|
c = c.replace(/~\/\.claude\//g, '~/.gemini/antigravity/');
|
|
2502
2404
|
// Bare form (no trailing slash) — must come after slash form to avoid double-replace
|
|
@@ -3964,7 +3866,7 @@ Typed mapping (agent_type-capable schema only):
|
|
|
3964
3866
|
to \`spawn_agent\` when the runtime/tool supports it. Omit missing, empty,
|
|
3965
3867
|
inherited, or unsupported values; do not invent one-off effort literals in
|
|
3966
3868
|
workflow prose.
|
|
3967
|
-
- \`fork_context: false\` by default — GSD agents load their own context via \`<
|
|
3869
|
+
- \`fork_context: false\` by default — GSD agents load their own context via \`<required_reading>\` blocks
|
|
3968
3870
|
- \`task_name\` — required by the collaboration schema; provide a descriptive name for each spawned task
|
|
3969
3871
|
- \`fork_turns\` — optional parameter controlling turn-forking depth; coexists with \`fork_context\` (not a replacement)
|
|
3970
3872
|
- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
|
|
@@ -4059,15 +3961,19 @@ purpose: ${toSingleLine(description)}
|
|
|
4059
3961
|
/**
|
|
4060
3962
|
* #2310 — True if `model` is an Anthropic-flavored value that must never appear as a
|
|
4061
3963
|
* Codex agent `.toml` `model`. Two forms: (a) a bare Claude Agent-tool tier alias
|
|
4062
|
-
* (opus/sonnet/haiku/fable — the canonical CLAUDE_AGENT_ALIASES
|
|
4063
|
-
*
|
|
4064
|
-
*
|
|
4065
|
-
*
|
|
4066
|
-
*
|
|
4067
|
-
*
|
|
3964
|
+
* (opus/sonnet/haiku/fable — the canonical CLAUDE_AGENT_ALIASES); (b) any Claude model
|
|
3965
|
+
* id in any provider namespacing — `claude-*`, `anthropic/claude-*`, `us.anthropic.claude-*`
|
|
3966
|
+
* (the forms the catalog assigns to opencode/hermes/kilo, reachable on a Codex .toml via
|
|
3967
|
+
* the runtime-resolver path). No OpenAI/Codex model id contains "claude", so a
|
|
3968
|
+
* case-insensitive substring test is a safe, exhaustive guard for (b). Codex/ChatGPT
|
|
3969
|
+
* rejects all of these.
|
|
3970
|
+
*
|
|
3971
|
+
* #3241 — thin delegation to the shared predicate on src/model-catalog.cts (moved there
|
|
3972
|
+
* so it can't diverge across Codex-posture surfaces); kept as a local name because it
|
|
3973
|
+
* reads better at the call sites below.
|
|
4068
3974
|
*/
|
|
4069
3975
|
function _isAnthropicFlavoredModel(model) {
|
|
4070
|
-
return
|
|
3976
|
+
return gsdIsAnthropicFlavoredModel(model);
|
|
4071
3977
|
}
|
|
4072
3978
|
|
|
4073
3979
|
// #2310 — dedupe stderr warnings so repeated agent emits don't spam (mirrors the
|
|
@@ -4086,6 +3992,42 @@ function _warnCodexModelOverrideDropped(agentName, value) {
|
|
|
4086
3992
|
);
|
|
4087
3993
|
}
|
|
4088
3994
|
|
|
3995
|
+
// #3241 — one-time per-install deprecation notice: the automatic runtime-resolver
|
|
3996
|
+
// per-tier Codex model embed was removed (D1/D5, ADR-2313 passive-posture epic). When
|
|
3997
|
+
// the resolver *would have* supplied a model and nothing else ends up pinned, this
|
|
3998
|
+
// notice points the user at model_overrides as the explicit-pin replacement. Dedupes
|
|
3999
|
+
// with a module-level boolean (mirrors _codexModelOverrideDroppedWarned's Set above)
|
|
4000
|
+
// so a multi-agent install — every Codex agent hits this condition simultaneously —
|
|
4001
|
+
// emits exactly one line, not one per agent. Reset once per install() call (see
|
|
4002
|
+
// install()) so the "at most once" window is per-install, not per-process. That
|
|
4003
|
+
// reset lives ONLY inside install() (~:10116) — generateCodexAgentToml is also
|
|
4004
|
+
// exported standalone (~:13460), and a caller invoking it directly/repeatedly
|
|
4005
|
+
// outside install() gets process-lifetime dedupe instead of per-install. No
|
|
4006
|
+
// current test depends on the standalone caller's dedupe window.
|
|
4007
|
+
let _codexResolverModelOmittedWarned = false;
|
|
4008
|
+
function _warnCodexResolverModelOmitted() {
|
|
4009
|
+
if (_codexResolverModelOmittedWarned) return;
|
|
4010
|
+
_codexResolverModelOmittedWarned = true;
|
|
4011
|
+
process.stderr.write(
|
|
4012
|
+
'gsd: notice — Codex agents no longer auto-pin a per-tier model from the runtime ' +
|
|
4013
|
+
'resolver; set model_overrides for an agent if you want a specific Codex model ' +
|
|
4014
|
+
'instead of the session model.\n',
|
|
4015
|
+
);
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
// Test seam only — bin/install.js deliberately keeps per-install warning/notice
|
|
4019
|
+
// dedupe in module scope (both the _codexModelOverrideDroppedWarned Set above and
|
|
4020
|
+
// the _codexResolverModelOmittedWarned boolean; install() resets the latter at
|
|
4021
|
+
// ~:10116). A unit test that drives generateCodexAgentToml() directly, without
|
|
4022
|
+
// going through install(), has no other way to reset either store between
|
|
4023
|
+
// assertions without busting the require.cache (which breaks module-instance
|
|
4024
|
+
// sharing with the rest of the suite). This is the single sanctioned way for a
|
|
4025
|
+
// unit test to clear both dedupe stores — exported so tests can call it instead.
|
|
4026
|
+
function _resetCodexWarningDedupeForTests() {
|
|
4027
|
+
_codexModelOverrideDroppedWarned.clear();
|
|
4028
|
+
_codexResolverModelOmittedWarned = false;
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4089
4031
|
/**
|
|
4090
4032
|
* Generate a per-agent .toml config file for Codex.
|
|
4091
4033
|
* Sets required agent metadata, sandbox_mode, and developer_instructions
|
|
@@ -4098,10 +4040,39 @@ function _warnCodexModelOverrideDropped(agentName, value) {
|
|
|
4098
4040
|
* @param {object|null} effortCfg — #443: merged effort config from readGsdEffectiveEffortConfig
|
|
4099
4041
|
*/
|
|
4100
4042
|
function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, runtimeResolver = null, effortCfg = null, sandboxTier = 'codex-agent-sandbox') {
|
|
4101
|
-
const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only';
|
|
4102
4043
|
const { frontmatter, body } = extractFrontmatterAndBody(agentContent);
|
|
4103
4044
|
const frontmatterText = frontmatter || '';
|
|
4045
|
+
// #3897 list-form parse fix, Fix 3: `toolsRaw` MUST come from the same
|
|
4046
|
+
// shared `extractToolsValue` reader `checkCodexSandboxPosture` uses, not
|
|
4047
|
+
// this file's own `extractFrontmatterField` — the two used to disagree on
|
|
4048
|
+
// YAML block-list `tools:` form (`extractFrontmatterField`'s single-line
|
|
4049
|
+
// regex read only the first list item), which is exactly the generative-
|
|
4050
|
+
// fix-divergence shape CLAUDE.md warns about for two paths feeding one
|
|
4051
|
+
// derivation. `extractToolsValue` does its own `---`-delimited frontmatter
|
|
4052
|
+
// scan of the full `agentContent`, so it is not re-derived from
|
|
4053
|
+
// `frontmatterText` here.
|
|
4054
|
+
const toolsRaw = extractToolsValue(agentContent) ?? '';
|
|
4104
4055
|
const resolvedName = extractFrontmatterField(frontmatterText, 'name') || agentName;
|
|
4056
|
+
// #3897 rung 3 — derived from the role's own tool contract (HALT.md option
|
|
4057
|
+
// 2). The former hand-maintained CODEX_AGENT_SANDBOX map is deleted (ADR-3473
|
|
4058
|
+
// §8.3): it was fully redundant with this derivation, zero disagreements
|
|
4059
|
+
// across all 11 entries. Never a silent `|| 'read-only'` fallback either.
|
|
4060
|
+
// Derivation itself lives in codex-agent-toml.cjs, which does no frontmatter
|
|
4061
|
+
// parsing of its own (no third copy of that extraction) — it takes the
|
|
4062
|
+
// already-resolved `tools:` value, extracted via the shared reader above.
|
|
4063
|
+
//
|
|
4064
|
+
// #3897 security review F1 (blocker): pass BOTH candidate identities —
|
|
4065
|
+
// `agentName` (the caller's filename-stem identity) AND `resolvedName`
|
|
4066
|
+
// (the frontmatter `name:` this function's OWN emitted `name = ...` line
|
|
4067
|
+
// uses, and what `installCodexConfig`'s caller keys the output PATH on) —
|
|
4068
|
+
// never just one. Deciding the sandbox for `agentName` alone and applying
|
|
4069
|
+
// it to an artifact that a DIFFERENT identity (`resolvedName`) names is
|
|
4070
|
+
// exactly how a held role's own `.toml` could end up `workspace-write`
|
|
4071
|
+
// (rename the source file, or plant a sibling whose `name:` collides with
|
|
4072
|
+
// a held role). `deriveCodexSandboxMode` takes the most restrictive result
|
|
4073
|
+
// across every candidate — see its doc and `isSandboxHeld` in
|
|
4074
|
+
// codex-agent-toml.cjs.
|
|
4075
|
+
const sandboxMode = deriveCodexSandboxMode([agentName, resolvedName], toolsRaw);
|
|
4105
4076
|
const resolvedDescription = toSingleLine(
|
|
4106
4077
|
extractFrontmatterField(frontmatterText, 'description') || `GSD agent ${resolvedName}`
|
|
4107
4078
|
);
|
|
@@ -4118,38 +4089,58 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
|
|
|
4118
4089
|
// Embed model override when configured in ~/.gsd/defaults.json so that
|
|
4119
4090
|
// model_overrides is respected on Codex (which uses static TOML, not inline
|
|
4120
4091
|
// Task() model parameters). See #2256.
|
|
4121
|
-
// Precedence: per-agent model_overrides > runtime-aware tier resolution (#2517).
|
|
4122
4092
|
// #2310 — a Codex .toml `model` MUST be a real Codex/OpenAI model id. Codex is a
|
|
4123
4093
|
// passive/session-only model host (ADR-1239): GSD cannot reliably route per-agent
|
|
4124
4094
|
// tiers, and a bare GSD/Claude tier alias (opus/sonnet/haiku/fable) or a claude-*
|
|
4125
4095
|
// id 400s on a ChatGPT-account Codex ("The 'sonnet' model is not supported when
|
|
4126
4096
|
// using Codex with a ChatGPT account"). So: embed ONLY an explicit real-Codex
|
|
4127
4097
|
// model pin from model_overrides; omit anything Anthropic-flavored so the agent
|
|
4128
|
-
// inherits the always-available session model.
|
|
4129
|
-
//
|
|
4098
|
+
// inherits the always-available session model.
|
|
4099
|
+
// #3241 (D1/D5) — the runtime-aware tier-resolver auto-embed that used to fall
|
|
4100
|
+
// through here when model_overrides had nothing was removed: Codex is passive by
|
|
4101
|
+
// default now, and only an explicit model_overrides pin survives. See the
|
|
4102
|
+
// deprecation-notice block below for the population that used to get a pin from
|
|
4103
|
+
// the resolver and no longer does.
|
|
4130
4104
|
const rawModelOverride = modelOverrides?.[resolvedName] || modelOverrides?.[agentName];
|
|
4131
4105
|
let pinnedModel = null;
|
|
4132
4106
|
if (rawModelOverride) {
|
|
4133
|
-
|
|
4134
|
-
|
|
4107
|
+
// Trim before the truthiness test (#3241 defect fix): a whitespace-only value
|
|
4108
|
+
// (e.g. ' ') is a truthy JS string but not a model id — it must not be
|
|
4109
|
+
// embedded verbatim (`model = " "`, a live pre-fix defect) or routed to
|
|
4110
|
+
// _warnCodexModelOverrideDropped, whose "is not a valid Codex model
|
|
4111
|
+
// (Anthropic alias/id)" text would misdescribe a blank config field. It is
|
|
4112
|
+
// silently dropped, matching how '' already behaves (no pin, no warning).
|
|
4113
|
+
const trimmedOverride = typeof rawModelOverride === 'string' ? rawModelOverride.trim() : rawModelOverride;
|
|
4114
|
+
if (typeof trimmedOverride === 'string' && trimmedOverride && !_isAnthropicFlavoredModel(trimmedOverride)) {
|
|
4115
|
+
pinnedModel = trimmedOverride; // explicit real-Codex model pin → embed verbatim (#2256)
|
|
4116
|
+
} else if (typeof rawModelOverride === 'string' && trimmedOverride === '') {
|
|
4117
|
+
// whitespace-only override — no pin, no warning (#3241).
|
|
4135
4118
|
} else {
|
|
4136
4119
|
_warnCodexModelOverrideDropped(resolvedName, rawModelOverride); // alias/claude-* → omit
|
|
4137
4120
|
}
|
|
4138
4121
|
}
|
|
4139
|
-
if (!pinnedModel && runtimeResolver) {
|
|
4140
|
-
// #2517 — runtime-aware tier resolution. Embeds Codex-native model + reasoning_effort
|
|
4141
|
-
// from RUNTIME_PROFILE_MAP / model_profile_overrides for the configured tier.
|
|
4142
|
-
// (Superseded on the default path by the ADR-2310 passive-posture epic.)
|
|
4143
|
-
const entry = runtimeResolver.resolve(resolvedName) || runtimeResolver.resolve(agentName);
|
|
4144
|
-
if (entry?.model) pinnedModel = entry.model;
|
|
4145
|
-
}
|
|
4146
4122
|
// #2310 — final safety gate: never emit an Anthropic-flavored model into a Codex
|
|
4147
|
-
// .toml, even
|
|
4148
|
-
//
|
|
4123
|
+
// .toml, even one that reached here through some other path than the override
|
|
4124
|
+
// check above.
|
|
4149
4125
|
if (pinnedModel && _isAnthropicFlavoredModel(pinnedModel)) {
|
|
4150
4126
|
_warnCodexModelOverrideDropped(resolvedName, pinnedModel);
|
|
4151
4127
|
pinnedModel = null;
|
|
4152
4128
|
}
|
|
4129
|
+
// #3241 — one-time deprecation notice: if nothing ends up pinned but the
|
|
4130
|
+
// runtime resolver would have supplied a per-tier model that would actually
|
|
4131
|
+
// have been EMBEDDED (the population that loses a pin now that the
|
|
4132
|
+
// auto-embed above is gone), point the user at model_overrides. The would-be
|
|
4133
|
+
// model must also clear the #2310 Anthropic-flavored gate above — if it
|
|
4134
|
+
// wouldn't have survived that gate, the user never had that pin pre-Phase-1
|
|
4135
|
+
// either, and the notice would be false. Never fires when the resolver is
|
|
4136
|
+
// null, resolves to nothing, resolves to an Anthropic-flavored model, or an
|
|
4137
|
+
// explicit real-Codex pin survived — in all of those cases nothing was lost.
|
|
4138
|
+
if (!pinnedModel && runtimeResolver) {
|
|
4139
|
+
const wouldHavePinned = runtimeResolver.resolve(resolvedName) || runtimeResolver.resolve(agentName);
|
|
4140
|
+
if (wouldHavePinned?.model && !_isAnthropicFlavoredModel(wouldHavePinned.model)) {
|
|
4141
|
+
_warnCodexResolverModelOmitted();
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4153
4144
|
let hasPinnedModel = false;
|
|
4154
4145
|
if (pinnedModel) {
|
|
4155
4146
|
lines.push(`model = ${JSON.stringify(pinnedModel)}`);
|
|
@@ -4161,16 +4152,26 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
|
|
|
4161
4152
|
// #443 — Unified effort for Codex .toml. Uses the same config-driven precedence chain
|
|
4162
4153
|
// as the Claude .md effort injection (resolveInstallTimeEffort), so both runtimes read
|
|
4163
4154
|
// from the same effort.agent_overrides / effort.routing_tier_defaults / effort.default
|
|
4164
|
-
// config source.
|
|
4165
|
-
//
|
|
4155
|
+
// config source. #3007 — Codex advertises supported_reasoning_levels per model, so the
|
|
4156
|
+
// pinned model id is passed through and the value is resolved against that model's own
|
|
4157
|
+
// set: 'max' now passes, 'minimal' clamps up to 'low', and 'ultra' is refused (no key
|
|
4158
|
+
// emitted) rather than clamped to a fabricated level.
|
|
4166
4159
|
// #838 — Do not pin effort when Codex is intentionally inheriting the parent
|
|
4167
4160
|
// chat model. A TOML with no `model` but a static `model_reasoning_effort`
|
|
4168
4161
|
// creates confusing partial routing: model follows the Codex UI while effort
|
|
4169
4162
|
// follows GSD. Keep those knobs coupled unless GSD also pins the model.
|
|
4170
4163
|
if (hasPinnedModel) {
|
|
4171
4164
|
const _universalEffortCodex = resolveInstallTimeEffort(effortCfg, resolvedName !== agentName ? resolvedName : agentName);
|
|
4172
|
-
|
|
4173
|
-
|
|
4165
|
+
// #3533 (10d): 'inherit' means OMIT the pin — the agent follows the host's
|
|
4166
|
+
// own effort default. Never write the literal.
|
|
4167
|
+
if (_universalEffortCodex !== 'inherit') {
|
|
4168
|
+
const _renderedEffortCodex = _getGsdEffortCatalog().renderEffortForRuntime('codex', _universalEffortCodex, pinnedModel).value;
|
|
4169
|
+
// #3007 — 'ultra' is rejected by the model's supported_reasoning_levels and
|
|
4170
|
+
// renders as null. Omit the key entirely rather than write a literal `null`.
|
|
4171
|
+
if (_renderedEffortCodex !== null) {
|
|
4172
|
+
lines.push(`model_reasoning_effort = ${JSON.stringify(_renderedEffortCodex)}`);
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4174
4175
|
}
|
|
4175
4176
|
|
|
4176
4177
|
// #774 — Emit service_tier and model_verbosity for light-tier agents.
|
|
@@ -6377,6 +6378,31 @@ const __atomicWrittenTmps = hooksSurface.__atomicWrittenTmps;
|
|
|
6377
6378
|
* All writes go through atomicWriteFileSync so a mid-write failure leaves
|
|
6378
6379
|
* the original config.toml untouched (#2760 fix 4).
|
|
6379
6380
|
*/
|
|
6381
|
+
/**
|
|
6382
|
+
* Split TOML content into its leading TOP-LEVEL key lines and everything from
|
|
6383
|
+
* the first table header onward (#3610).
|
|
6384
|
+
*
|
|
6385
|
+
* Top-level keys were file-scoped before a merge. The regenerated GSD block
|
|
6386
|
+
* opens with a table header (`[agents]`, #2088/ADR-1239 upgrade 2), so placing
|
|
6387
|
+
* that block ABOVE surviving top-level keys re-scopes them into `[agents]` —
|
|
6388
|
+
* `validateCodexConfigSchema` then correctly rejects the merged file and the
|
|
6389
|
+
* install aborts. Hoisting the keys above the block preserves their scope.
|
|
6390
|
+
*
|
|
6391
|
+
* Table headers inside multiline strings do not start the "rest" region (the
|
|
6392
|
+
* record parser already excludes them via startsInMultilineString).
|
|
6393
|
+
*/
|
|
6394
|
+
function splitTopLevelKeys(content) {
|
|
6395
|
+
for (const record of getTomlLineRecords(content)) {
|
|
6396
|
+
if (record.tableHeader && !record.startsInMultilineString) {
|
|
6397
|
+
return {
|
|
6398
|
+
topLevel: content.slice(0, record.start).trim(),
|
|
6399
|
+
rest: content.slice(record.start).trim(),
|
|
6400
|
+
};
|
|
6401
|
+
}
|
|
6402
|
+
}
|
|
6403
|
+
return { topLevel: content.trim(), rest: '' };
|
|
6404
|
+
}
|
|
6405
|
+
|
|
6380
6406
|
function mergeCodexConfig(configPath, gsdBlock) {
|
|
6381
6407
|
// Case 1: No config.toml — create fresh
|
|
6382
6408
|
if (!fs.existsSync(configPath)) {
|
|
@@ -6424,10 +6450,25 @@ function mergeCodexConfig(configPath, gsdBlock) {
|
|
|
6424
6450
|
.replace(/^\r?\n# GSD codex_hooks ownership: (?:section|root_dotted)\r?\n/, '');
|
|
6425
6451
|
const afterUser = stripLeakedGsdCodexSections(markerStripped).trim();
|
|
6426
6452
|
|
|
6453
|
+
// #3610: top-level keys that survived BELOW the marker were file-scoped
|
|
6454
|
+
// before this merge; the regenerated block opens with the `[agents]` table
|
|
6455
|
+
// header, so they must be hoisted to FILE scope or TOML re-scopes them
|
|
6456
|
+
// into a table. File scope means BEFORE the first table header of the
|
|
6457
|
+
// pre-marker region too — appending them after a pre-marker table (the
|
|
6458
|
+
// default real-world layout: user tables precede the marker) would merely
|
|
6459
|
+
// capture them into THAT table instead of [agents], and the schema
|
|
6460
|
+
// validator is blind to non-agents tables.
|
|
6461
|
+
const beforeSplit = before ? splitTopLevelKeys(before) : { topLevel: '', rest: '' };
|
|
6462
|
+
const { topLevel: afterTopLevel, rest: afterTables } = splitTopLevelKeys(afterUser);
|
|
6463
|
+
|
|
6427
6464
|
const parts = [];
|
|
6428
|
-
|
|
6465
|
+
const topParts = [];
|
|
6466
|
+
if (beforeSplit.topLevel) topParts.push(beforeSplit.topLevel);
|
|
6467
|
+
if (afterTopLevel) topParts.push(afterTopLevel);
|
|
6468
|
+
if (topParts.length > 0) parts.push(topParts.join(eol + eol));
|
|
6469
|
+
if (beforeSplit.rest) parts.push(beforeSplit.rest);
|
|
6429
6470
|
parts.push(normalizedGsdBlock);
|
|
6430
|
-
if (
|
|
6471
|
+
if (afterTables) parts.push(afterTables);
|
|
6431
6472
|
atomicWriteFileSync(configPath, parts.join(eol + eol) + eol);
|
|
6432
6473
|
return;
|
|
6433
6474
|
}
|
|
@@ -6729,43 +6770,13 @@ function stripGsdFromCopilotInstructions(content) {
|
|
|
6729
6770
|
const GSD_AGENTS_MD_MARKER = '<!-- GSD Configuration — managed by gsd-core installer -->';
|
|
6730
6771
|
const GSD_AGENTS_MD_CLOSE_MARKER = '<!-- End GSD Configuration -->';
|
|
6731
6772
|
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
}
|
|
6740
|
-
|
|
6741
|
-
/** AGENTS.md body for the cross-tool global instruction target (`~/.agents/AGENTS.md`). */
|
|
6742
|
-
function buildClineAgentsMdBody() {
|
|
6743
|
-
return hooksSurface.buildClineAgentsMdBody();
|
|
6744
|
-
}
|
|
6745
|
-
|
|
6746
|
-
/**
|
|
6747
|
-
* The Cline PreToolUse hook script (issue #787).
|
|
6748
|
-
*
|
|
6749
|
-
* Cline invokes hooks as executable scripts named exactly after the event with
|
|
6750
|
-
* no extension, passing the operation context as JSON on stdin and reading a
|
|
6751
|
-
* JSON decision from stdout ({ cancel, errorMessage, contextModification }).
|
|
6752
|
-
*
|
|
6753
|
-
* This hook is a self-standing planning-artifact guard: it cancels write-class
|
|
6754
|
-
* tool calls that target `.planning/` (GSD-owned artifacts), and otherwise
|
|
6755
|
-
* allows the operation. It FAILS OPEN — any parse/IO error allows the call so a
|
|
6756
|
-
* hook bug can never wedge the user. No dependency on the #782 skills work.
|
|
6757
|
-
*/
|
|
6758
|
-
function buildClinePreToolUseHook() {
|
|
6759
|
-
return hooksSurface.buildClinePreToolUseHook();
|
|
6760
|
-
}
|
|
6761
|
-
|
|
6762
|
-
/**
|
|
6763
|
-
* Merge the GSD AGENTS.md block into an existing file (or create it), preserving
|
|
6764
|
-
* any user content. Mirrors mergeCopilotInstructions: marker-delimited, idempotent.
|
|
6765
|
-
*/
|
|
6766
|
-
function mergeGsdAgentsMd(filePath, gsdContent) {
|
|
6767
|
-
return hooksSurface.mergeGsdAgentsMd(filePath, gsdContent);
|
|
6768
|
-
}
|
|
6773
|
+
// #2876: buildClineRulesBody, buildClineAgentsMdBody, buildClinePreToolUseHook,
|
|
6774
|
+
// and mergeGsdAgentsMd used to be re-bound here as one-line delegates to the
|
|
6775
|
+
// equivalent hooksSurface.* implementations. None had an install.js internal
|
|
6776
|
+
// caller or an export consumer (tests import all four directly from
|
|
6777
|
+
// gsd-core/bin/lib/runtime-hooks-surface.cjs), so the retired bindings were
|
|
6778
|
+
// dead code with no reachable body — removed rather than kept as unreachable
|
|
6779
|
+
// wrappers.
|
|
6769
6780
|
|
|
6770
6781
|
/**
|
|
6771
6782
|
* Strip the GSD block from AGENTS.md content. Returns null if the file became
|
|
@@ -6817,47 +6828,13 @@ function writeClineArtifacts(targetDir, isGlobalInstall) {
|
|
|
6817
6828
|
//
|
|
6818
6829
|
// References: https://cursor.com/docs/hooks
|
|
6819
6830
|
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
return hooksSurface.buildCursorHookEntry(scriptPath);
|
|
6828
|
-
}
|
|
6829
|
-
|
|
6830
|
-
/**
|
|
6831
|
-
* Return true if a Cursor hook entry is GSD-managed.
|
|
6832
|
-
* Detection: presence of the GSD_CURSOR_HOOK_MARKER sentinel field.
|
|
6833
|
-
*
|
|
6834
|
-
* @param {object} entry - A hooks array element from hooks.json
|
|
6835
|
-
* @returns {boolean}
|
|
6836
|
-
*/
|
|
6837
|
-
function isManagedCursorHookEntry(entry) {
|
|
6838
|
-
return hooksSurface.isManagedCursorHookEntry(entry);
|
|
6839
|
-
}
|
|
6840
|
-
|
|
6841
|
-
/**
|
|
6842
|
-
* Reconcile the GSD-managed entries in a Cursor hooks.json file.
|
|
6843
|
-
*
|
|
6844
|
-
* Supports both known hooks.json shapes:
|
|
6845
|
-
* 1) { "version": 1, "hooks": { "sessionStart": [...], "postToolUse": [...] } }
|
|
6846
|
-
* 2) { "sessionStart": [...], "postToolUse": [...] } (no wrapper object)
|
|
6847
|
-
*
|
|
6848
|
-
* Managed entries (those with GSD_CURSOR_HOOK_MARKER) are removed then
|
|
6849
|
-
* re-added if managedEntries is non-null/non-empty. User-owned entries are
|
|
6850
|
-
* preserved. File is written atomically only when content changes.
|
|
6851
|
-
*
|
|
6852
|
-
* @param {string} hooksJsonPath - Absolute path to the hooks.json file
|
|
6853
|
-
* @param {{ sessionStart?: object|null, postToolUse?: object|null }|null} managedEntries
|
|
6854
|
-
* Map from event name to the new hook entry to register (or null to remove).
|
|
6855
|
-
* Pass null for the whole param to remove all managed entries.
|
|
6856
|
-
* @returns {{ changed: boolean, wrote: boolean, path: string }}
|
|
6857
|
-
*/
|
|
6858
|
-
function reconcileCursorHooksJson(hooksJsonPath, managedEntries) {
|
|
6859
|
-
return hooksSurface.reconcileCursorHooksJson(hooksJsonPath, managedEntries);
|
|
6860
|
-
}
|
|
6831
|
+
// #2876: buildCursorHookEntry, isManagedCursorHookEntry, and
|
|
6832
|
+
// reconcileCursorHooksJson used to be re-bound here as one-line delegates to
|
|
6833
|
+
// the equivalent hooksSurface.* implementations. None had an install.js
|
|
6834
|
+
// internal caller or an export consumer (tests import all three directly from
|
|
6835
|
+
// gsd-core/bin/lib/runtime-hooks-surface.cjs), so the retired bindings were
|
|
6836
|
+
// dead code with no reachable body — removed rather than kept as unreachable
|
|
6837
|
+
// wrappers.
|
|
6861
6838
|
|
|
6862
6839
|
/**
|
|
6863
6840
|
* #777 — Write GSD-managed Cursor lifecycle hooks into <targetDir>/hooks.json.
|
|
@@ -6917,23 +6894,13 @@ function removeWindsurfHooksJson(targetDir) {
|
|
|
6917
6894
|
return hooksSurface.removeWindsurfHooksJson(targetDir);
|
|
6918
6895
|
}
|
|
6919
6896
|
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
* run inline (no external script file), so the config can never reference a
|
|
6928
|
-
* hook script that the installer did not also install — it is self-contained
|
|
6929
|
-
* by construction. The command is advisory-only (always exits 0) and orients
|
|
6930
|
-
* the agent toward the project's GSD planning state at session start.
|
|
6931
|
-
*
|
|
6932
|
-
* @returns {object} Copilot hooks-configuration object
|
|
6933
|
-
*/
|
|
6934
|
-
function buildCopilotHookConfig() {
|
|
6935
|
-
return hooksSurface.buildCopilotHookConfig();
|
|
6936
|
-
}
|
|
6897
|
+
// #2876: buildCopilotHookConfig used to be re-bound here as a one-line
|
|
6898
|
+
// delegate to hooksSurface.buildCopilotHookConfig. It had no install.js
|
|
6899
|
+
// internal caller or export consumer (tests import it directly from
|
|
6900
|
+
// gsd-core/bin/lib/runtime-hooks-surface.cjs), so the retired binding was
|
|
6901
|
+
// dead code with no reachable body — removed rather than kept as an
|
|
6902
|
+
// unreachable wrapper. writeCopilotHookConfig below is unaffected — it still
|
|
6903
|
+
// has an internal caller (finishInstall).
|
|
6937
6904
|
|
|
6938
6905
|
/**
|
|
6939
6906
|
* #786 — Write the GSD-managed Copilot lifecycle hook config under the runtime
|
|
@@ -6971,29 +6938,50 @@ function writeNonClaudeDefaults(runtime) {
|
|
|
6971
6938
|
if (_hostBehaviors(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return;
|
|
6972
6939
|
const gsdDir = path.join(os.homedir(), '.gsd');
|
|
6973
6940
|
const defaultsPath = path.join(gsdDir, 'defaults.json');
|
|
6941
|
+
let releaseLock = null;
|
|
6974
6942
|
try {
|
|
6975
6943
|
fs.mkdirSync(gsdDir, { recursive: true });
|
|
6944
|
+
// defaults.json is machine-global — every runtime and project on the box
|
|
6945
|
+
// reads it. Serialize the read-modify-write so two concurrent installs
|
|
6946
|
+
// cannot lose each other's key, and apply both mutations in ONE atomic
|
|
6947
|
+
// write so a crash cannot leave the file truncated (the read path swallows
|
|
6948
|
+
// parse errors and treats a corrupt file as absent, which would silently
|
|
6949
|
+
// degrade model resolution everywhere until repaired by hand).
|
|
6950
|
+
releaseLock = acquireInstallMigrationLock(gsdDir);
|
|
6976
6951
|
let defaults = {};
|
|
6977
6952
|
try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* new file */ }
|
|
6978
6953
|
if (defaults === null || typeof defaults !== 'object' || Array.isArray(defaults)) {
|
|
6979
6954
|
defaults = {};
|
|
6980
6955
|
}
|
|
6956
|
+
const applied = [];
|
|
6981
6957
|
// Three-valued domain: false/absent → aliases; true → full IDs; "omit" → ''.
|
|
6982
6958
|
const existing = defaults.resolve_model_ids;
|
|
6983
6959
|
const shouldDefaultToOmit = existing !== true && existing !== 'omit';
|
|
6984
6960
|
if (shouldDefaultToOmit) {
|
|
6985
6961
|
defaults.resolve_model_ids = 'omit';
|
|
6986
|
-
|
|
6987
|
-
console.log(` ${green}✓${reset} Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
|
|
6962
|
+
applied.push(`Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
|
|
6988
6963
|
}
|
|
6989
6964
|
// #2395: persist runtime for non-Claude runtimes.
|
|
6990
6965
|
if (defaults.runtime === undefined || defaults.runtime === null || defaults.runtime === '') {
|
|
6991
6966
|
defaults.runtime = runtime;
|
|
6992
|
-
|
|
6993
|
-
|
|
6967
|
+
applied.push(`Set runtime: "${runtime}" in ~/.gsd/defaults.json`);
|
|
6968
|
+
}
|
|
6969
|
+
if (applied.length > 0) {
|
|
6970
|
+
atomicWriteFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n', 'utf8');
|
|
6971
|
+
for (const message of applied) console.log(` ${green}✓${reset} ${message}`);
|
|
6994
6972
|
}
|
|
6995
6973
|
} catch (e) {
|
|
6996
6974
|
console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
|
|
6975
|
+
} finally {
|
|
6976
|
+
if (releaseLock) {
|
|
6977
|
+
try {
|
|
6978
|
+
releaseLock();
|
|
6979
|
+
} catch (releaseError) {
|
|
6980
|
+
// A leaked lock blocks the next install, so surface it rather than
|
|
6981
|
+
// swallowing; the stale-lock reaper clears it once this pid exits.
|
|
6982
|
+
console.log(` ${yellow}⚠${reset} Could not release the ~/.gsd install lock: ${releaseError.message}`);
|
|
6983
|
+
}
|
|
6984
|
+
}
|
|
6997
6985
|
}
|
|
6998
6986
|
}
|
|
6999
6987
|
|
|
@@ -7017,6 +7005,36 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
7017
7005
|
}
|
|
7018
7006
|
fs.mkdirSync(agentsTomlDir, { recursive: true });
|
|
7019
7007
|
|
|
7008
|
+
// #3897 rung 3 (CAUSE B fix) — validateCodexSandboxHolds is deliberately
|
|
7009
|
+
// NOT called here. The "no stale holds" invariant is a REPO invariant about
|
|
7010
|
+
// the canonical roster in `agents/`, not a property of whatever directory
|
|
7011
|
+
// an install happens to read from: a partial or synthetic `agentsSrc` (a
|
|
7012
|
+
// test fixture, a `--config-dir` subset) legitimately contains only a few
|
|
7013
|
+
// agents, and a held role simply absent from THIS source dir must be
|
|
7014
|
+
// inert, not fatal. Throwing here also masked unrelated failures further
|
|
7015
|
+
// down this same loop (e.g. the name-injection/path-escape guard on
|
|
7016
|
+
// `agentTomlPath` below), since this check ran first and unconditionally.
|
|
7017
|
+
// The invariant is still enforced — as a test over the real `agents/`
|
|
7018
|
+
// roster (tests/codex-config.test.cjs T24/T25) — just never on this
|
|
7019
|
+
// runtime path. See src/codex-agent-toml.cts's `validateCodexSandboxHolds`
|
|
7020
|
+
// docblock for the full rationale.
|
|
7021
|
+
//
|
|
7022
|
+
// #3897 security review F2 (assessed post-F1-fix, verified by execution —
|
|
7023
|
+
// not asserted): before F1's fix, the "runtime detector removed" gap here
|
|
7024
|
+
// was real — a rename (case a) or a sibling `name:` clobber (case b) could
|
|
7025
|
+
// reach this loop and land a held role's `.toml` at `workspace-write` with
|
|
7026
|
+
// nothing here to catch it. After F1 (`deriveCodexSandboxMode` now decides
|
|
7027
|
+
// over BOTH the filename stem and the resolved frontmatter `name:`, most
|
|
7028
|
+
// restrictive wins), both cases were re-run end-to-end through this exact
|
|
7029
|
+
// function and the EMITTED ARTIFACT for both is `read-only` — the
|
|
7030
|
+
// dangerous condition no longer produces a wrong artifact, it produces the
|
|
7031
|
+
// SAFE one. A detector guarding a now-fail-safe condition is not
|
|
7032
|
+
// load-bearing, and restoring a throw here would re-break the legitimate
|
|
7033
|
+
// partial-source-dir case CAUSE B removed it for (see above). Regression
|
|
7034
|
+
// coverage for both cases lives in `tests/codex-config.test.cjs` (F1(a)
|
|
7035
|
+
// rename / F1(b) sibling-clobber rows), asserted on the emitted `.toml`'s
|
|
7036
|
+
// `sandbox_mode`, not on the derivation's return value.
|
|
7037
|
+
|
|
7020
7038
|
const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
|
|
7021
7039
|
const agents = [];
|
|
7022
7040
|
|
|
@@ -7045,7 +7063,20 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
7045
7063
|
// CLAUDE.md neutralization via neutralizeAgentReferences(..., 'AGENTS.md').
|
|
7046
7064
|
content = convertClaudeToCodexMarkdown(content);
|
|
7047
7065
|
const { frontmatter } = extractFrontmatterAndBody(content);
|
|
7048
|
-
|
|
7066
|
+
// #3897 security review F1 (blocker, post-merge): this loop used to key
|
|
7067
|
+
// the sandbox/hold decision off ONLY the filename stem while the emitted
|
|
7068
|
+
// `.toml`'s OUTPUT PATH below is keyed off `name` (frontmatter-derived,
|
|
7069
|
+
// attacker-editable) — so a renamed source file, or a sibling file whose
|
|
7070
|
+
// `name:` collides with a held role, could make the decided identity and
|
|
7071
|
+
// the landed artifact disagree, widening a held role's own file to
|
|
7072
|
+
// `workspace-write`. `generateCodexAgentToml` (below) now derives
|
|
7073
|
+
// `sandbox_mode` over BOTH the filename stem it is given AND the
|
|
7074
|
+
// frontmatter `name:` it resolves internally, taking the most
|
|
7075
|
+
// restrictive result — this loop no longer needs to choose one identity
|
|
7076
|
+
// for that call; see `deriveCodexSandboxMode`'s doc in
|
|
7077
|
+
// `codex-agent-toml.cts` for the resolution.
|
|
7078
|
+
const fileStem = file.replace(/\.md$/, '');
|
|
7079
|
+
const name = extractFrontmatterField(frontmatter, 'name') || fileStem;
|
|
7049
7080
|
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
7050
7081
|
|
|
7051
7082
|
agents.push({ name, description: toSingleLine(description) });
|
|
@@ -7067,7 +7098,10 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
7067
7098
|
// #443 — pass unified effort config so model_reasoning_effort in the .toml
|
|
7068
7099
|
// follows the same config-driven precedence as the Claude .md effort key.
|
|
7069
7100
|
const effortCfg = readGsdEffectiveEffortConfig(targetDir);
|
|
7070
|
-
|
|
7101
|
+
// Pass `fileStem`; `generateCodexAgentToml` itself additionally resolves
|
|
7102
|
+
// and folds in the frontmatter `name:` for the sandbox decision (F1
|
|
7103
|
+
// above) — this call site does not need to pass `name` explicitly.
|
|
7104
|
+
const tomlContent = generateCodexAgentToml(fileStem, content, modelOverrides, runtimeResolver, effortCfg, sandboxTier);
|
|
7071
7105
|
// Confine the per-agent write to the agents/ dir itself: a crafted agent
|
|
7072
7106
|
// `name` containing path separators must not escape agents/ (which would let
|
|
7073
7107
|
// it clobber config.toml or write elsewhere under the configHome).
|
|
@@ -7462,10 +7496,14 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverrid
|
|
|
7462
7496
|
|
|
7463
7497
|
// convertClaudeCommandToOpencodeFamilySkill, convertClaudeCommandToOpencodeSkill,
|
|
7464
7498
|
// convertClaudeCommandToKiloSkill: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
7465
|
-
//
|
|
7499
|
+
// #2876 found no install.js internal caller for the latter two (tests import
|
|
7500
|
+
// them directly from gsd-core/bin/lib/install-engine.cjs) and retired the
|
|
7501
|
+
// destructured bindings above.
|
|
7466
7502
|
|
|
7467
7503
|
// applyOpencodeFamilyPathPrefix: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
7468
|
-
//
|
|
7504
|
+
// #2876 found no install.js internal caller (never had one either — it was
|
|
7505
|
+
// never part of this module's export surface) and retired the destructured
|
|
7506
|
+
// binding above.
|
|
7469
7507
|
//
|
|
7470
7508
|
// copyFlattenedCommands (OpenCode/Kilo flattened command/ writer): moved to
|
|
7471
7509
|
// src/install-engine.cts as installOpencodeFamilyCommands (ADR-1239 / #2087).
|
|
@@ -7567,13 +7605,13 @@ function writeHermesCategoryDescription(categoryDir) {
|
|
|
7567
7605
|
* @param {boolean} isGlobal - Whether this is a global install
|
|
7568
7606
|
*/
|
|
7569
7607
|
|
|
7570
|
-
// USER_OWNED_ARTIFACTS,
|
|
7571
|
-
//
|
|
7572
|
-
//
|
|
7573
|
-
//
|
|
7574
|
-
//
|
|
7575
|
-
//
|
|
7576
|
-
//
|
|
7608
|
+
// USER_OWNED_ARTIFACTS, migrateLegacyDevPreferencesToSkill, _snapshotDir,
|
|
7609
|
+
// installRuntimeArtifacts, installOpencodeFamilySkills, uninstallRuntimeArtifacts:
|
|
7610
|
+
// ALL moved to src/install-engine.cts (ADR-1239 Phase B). Imported from
|
|
7611
|
+
// installEngine above. _copyStaged, _removeGsdEntries,
|
|
7612
|
+
// _runLegacyInstallMigrations, _runLegacyUninstallCleanup, _restoreDir, and
|
|
7613
|
+
// _removeHermesBareStemDirs moved there too but #2876 found no install.js
|
|
7614
|
+
// internal caller for any of them and retired their destructured bindings.
|
|
7577
7615
|
|
|
7578
7616
|
// ---------------------------------------------------------------------------
|
|
7579
7617
|
// Phase 2 — Layout-driven install/uninstall orchestrators (moved to engine)
|
|
@@ -7770,6 +7808,15 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7770
7808
|
const srcPath = path.join(srcDir, entry.name);
|
|
7771
7809
|
const destPath = path.join(destDir, entry.name);
|
|
7772
7810
|
|
|
7811
|
+
// #3333: srcPath was enumerated by readdirSync above, but a filesystem is not
|
|
7812
|
+
// transactional — the file it named can vanish between listing and this read
|
|
7813
|
+
// (a concurrent process, or another test in this suite writing/cleaning up a
|
|
7814
|
+
// fixture inside this same real directory). Treat "gone by the time we get
|
|
7815
|
+
// here" as benign and skip it, never a fatal crash of the whole install.
|
|
7816
|
+
if (!entry.isDirectory() && !fs.existsSync(srcPath)) {
|
|
7817
|
+
continue;
|
|
7818
|
+
}
|
|
7819
|
+
|
|
7773
7820
|
if (entry.isDirectory()) {
|
|
7774
7821
|
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand, isGlobal, confinementRoot);
|
|
7775
7822
|
} else if (entry.name.endsWith('.md')) {
|
|
@@ -7822,8 +7869,20 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7822
7869
|
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
7823
7870
|
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
7824
7871
|
content = content.replace(localClaudeRegex, `./${dirName}/`);
|
|
7825
|
-
|
|
7826
|
-
|
|
7872
|
+
// #3544 review (Finding 1 fallout): guarded with the SAME
|
|
7873
|
+
// negative-lookahead convention already used at ~:2859-2860 below
|
|
7874
|
+
// ("preserve .claude-plugin and .claudeignore"). A naive `\b` here
|
|
7875
|
+
// is satisfied by ANY non-word character, including '-' — so for a
|
|
7876
|
+
// --config-dir whose name EXTENDS '.claude' (e.g. '.claude-work',
|
|
7877
|
+
// pathPrefix '$HOME/.claude-work/'), this pass re-matched the
|
|
7878
|
+
// '$HOME/.claude' PREFIX of its own slash-form output (lines above)
|
|
7879
|
+
// and re-appended the full prefix, corrupting every emitted path to
|
|
7880
|
+
// '$HOME/.claude-work-work/...'. Harmless no-op for the literal
|
|
7881
|
+
// default '.claude' (self-replace with an identical string), which
|
|
7882
|
+
// is why this went undetected until a non-default config-dir name
|
|
7883
|
+
// was exercised.
|
|
7884
|
+
content = content.replace(/~\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
|
|
7885
|
+
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
|
|
7827
7886
|
content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
|
|
7828
7887
|
content = content.replace(/~\/\.qwen\//g, pathPrefix);
|
|
7829
7888
|
content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
|
|
@@ -7831,6 +7890,17 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7831
7890
|
content = content.replace(/~\/\.hermes\//g, pathPrefix);
|
|
7832
7891
|
content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
|
|
7833
7892
|
content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
|
|
7893
|
+
// #3544: restore @-file-reference lines to the tilde form Claude Code
|
|
7894
|
+
// actually expands — the SAME correction #3133 already applies to
|
|
7895
|
+
// skill/command bodies via _applyRuntimeRewrites's 'claude' case (see
|
|
7896
|
+
// restoreClaudeGlobalAtRefTilde's doc comment in
|
|
7897
|
+
// runtime-artifact-conversion.cts). This is the gsd-core/ spec-tree
|
|
7898
|
+
// emit path, which never had it: every @~/.claude/gsd-core/… include
|
|
7899
|
+
// in a global install's workflows/references tree silently resolved
|
|
7900
|
+
// to nothing (54 includes across 22 files on a live install).
|
|
7901
|
+
if (runtime === 'claude') {
|
|
7902
|
+
content = runtimeArtifactConversion._restoreClaudeGlobalAtRefTilde(content, pathPrefix);
|
|
7903
|
+
}
|
|
7834
7904
|
}
|
|
7835
7905
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
7836
7906
|
|
|
@@ -8031,6 +8101,133 @@ function validateHookFields(settings) {
|
|
|
8031
8101
|
*/
|
|
8032
8102
|
const GSD_UNINSTALL_HOOKS = [..._HOOKS_TO_COPY, 'gsd-check-update.cmd'];
|
|
8033
8103
|
|
|
8104
|
+
/**
|
|
8105
|
+
* Whether two paths denote the SAME directory — used to stop a reclaim from
|
|
8106
|
+
* deleting the very root the current install just wrote (#3031).
|
|
8107
|
+
*
|
|
8108
|
+
* A plain `path.resolve` comparison is not enough here, because both roots come
|
|
8109
|
+
* from user-controlled env vars (`KIMI_SHARE_DIR`, `KIMI_CODE_HOME`) and two
|
|
8110
|
+
* different strings routinely name one directory:
|
|
8111
|
+
* - case-insensitive filesystems (macOS, Windows): `~/Kimi` vs `~/kimi`
|
|
8112
|
+
* - symlinks / bind mounts: `~/link-to-kimi` vs the real target
|
|
8113
|
+
* Getting this wrong is not cosmetic — it is the difference between skipping a
|
|
8114
|
+
* reclaim and deleting a live install's own hooks.
|
|
8115
|
+
*
|
|
8116
|
+
* Strategy, cheapest-first: string equality after `resolve`, then identity by
|
|
8117
|
+
* `dev`+`ino` (definitive when both exist and the platform reports them), then
|
|
8118
|
+
* `realpath` string equality (resolves symlinks AND canonicalizes case). Any
|
|
8119
|
+
* rung answering "same" wins; a path that does not exist cannot be the root we
|
|
8120
|
+
* just wrote, so a failed stat simply falls through.
|
|
8121
|
+
*
|
|
8122
|
+
* @returns {boolean} true only when both paths are proven to be one directory.
|
|
8123
|
+
*/
|
|
8124
|
+
function isSameDirectory(a, b) {
|
|
8125
|
+
if (path.resolve(a) === path.resolve(b)) return true;
|
|
8126
|
+
try {
|
|
8127
|
+
const sa = fs.statSync(a);
|
|
8128
|
+
const sb = fs.statSync(b);
|
|
8129
|
+
// `ino` is 0 on some Windows filesystems; only trust a positive match.
|
|
8130
|
+
if (sa.ino && sb.ino && sa.dev === sb.dev && sa.ino === sb.ino) return true;
|
|
8131
|
+
} catch (_) { /* one side missing — fall through to realpath */ }
|
|
8132
|
+
try {
|
|
8133
|
+
return fs.realpathSync.native(a) === fs.realpathSync.native(b);
|
|
8134
|
+
} catch (_) {
|
|
8135
|
+
return false;
|
|
8136
|
+
}
|
|
8137
|
+
}
|
|
8138
|
+
|
|
8139
|
+
/**
|
|
8140
|
+
* Remove every GSD-owned artifact from a Kimi hooks root (`~/.kimi` for kimi,
|
|
8141
|
+
* `~/.kimi-code` for kimi-code — resolveKimiHooksTomlDir, #2755): the managed
|
|
8142
|
+
* `[[hooks]]` block in the native config.toml, the hook scripts, hooks/lib/,
|
|
8143
|
+
* and the CommonJS marker at both its current (hooks/) and pre-#2544 (root)
|
|
8144
|
+
* locations.
|
|
8145
|
+
*
|
|
8146
|
+
* This root is Kimi's own native config home — SHARED space that may hold the
|
|
8147
|
+
* user's real config.toml, providers and their own scripts — so only exact
|
|
8148
|
+
* GSD-owned filenames are removed and directories are pruned only when that
|
|
8149
|
+
* removal leaves them empty.
|
|
8150
|
+
*
|
|
8151
|
+
* TWO callers, deliberately one implementation (#3031). `uninstall()` calls it
|
|
8152
|
+
* for the runtime being uninstalled; the opt-in `--reclaim-kimi-legacy` path in
|
|
8153
|
+
* `install()` calls it for the LEGACY `~/.kimi` root a pre-#2755 `--kimi-code`
|
|
8154
|
+
* install orphaned. Duplicating this sequence for the second caller would be
|
|
8155
|
+
* exactly the generative-divergence hazard the repo bans — the reclaim must
|
|
8156
|
+
* remove precisely what a real uninstall removes, forever, by construction.
|
|
8157
|
+
*
|
|
8158
|
+
* @param {string} kimiHooksRoot - Absolute path to the Kimi hooks root.
|
|
8159
|
+
* @returns {number} count of removal steps performed (0 when nothing matched).
|
|
8160
|
+
*/
|
|
8161
|
+
function reclaimKimiHooksRoot(kimiHooksRoot) {
|
|
8162
|
+
let steps = 0;
|
|
8163
|
+
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
8164
|
+
const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath);
|
|
8165
|
+
if (kimiHooksCleanup.changed) {
|
|
8166
|
+
steps++;
|
|
8167
|
+
console.log(` ${green}✓${reset} Removed GSD hooks from ${kimiHooksTomlPath}`);
|
|
8168
|
+
}
|
|
8169
|
+
|
|
8170
|
+
// Kimi's shared hook scripts + CommonJS package.json marker are installed
|
|
8171
|
+
// into this SAME ~/.kimi root (installSharedHooksBundle, install()'s
|
|
8172
|
+
// kimi-hooks-toml branch) rather than under targetDir — mirror steps "4.
|
|
8173
|
+
// Remove GSD hooks" / "5. Remove GSD package.json" below, but scoped to
|
|
8174
|
+
// kimiHooksRoot. ~/.kimi is Kimi's own native config home (shared space —
|
|
8175
|
+
// may hold the user's real config.toml/providers), so only the exact
|
|
8176
|
+
// GSD-owned filenames are removed, and directories are pruned only if left
|
|
8177
|
+
// empty by that removal.
|
|
8178
|
+
const kimiHooksDir = path.join(kimiHooksRoot, 'hooks');
|
|
8179
|
+
if (fs.existsSync(kimiHooksDir)) {
|
|
8180
|
+
let kimiHookCount = 0;
|
|
8181
|
+
for (const hook of GSD_UNINSTALL_HOOKS) {
|
|
8182
|
+
const hookPath = path.join(kimiHooksDir, hook);
|
|
8183
|
+
if (fs.existsSync(hookPath)) {
|
|
8184
|
+
fs.unlinkSync(hookPath);
|
|
8185
|
+
kimiHookCount++;
|
|
8186
|
+
}
|
|
8187
|
+
}
|
|
8188
|
+
if (kimiHookCount > 0) {
|
|
8189
|
+
steps++;
|
|
8190
|
+
console.log(` ${green}✓${reset} Removed ${kimiHookCount} GSD hooks from ${kimiHooksDir}`);
|
|
8191
|
+
}
|
|
8192
|
+
|
|
8193
|
+
const kimiHooksLibDir = path.join(kimiHooksDir, 'lib');
|
|
8194
|
+
if (fs.existsSync(kimiHooksLibDir)) {
|
|
8195
|
+
let removedKimiLibFiles = 0;
|
|
8196
|
+
for (const file of GSD_HOOK_LIB_FILES) {
|
|
8197
|
+
try {
|
|
8198
|
+
fs.unlinkSync(path.join(kimiHooksLibDir, file));
|
|
8199
|
+
removedKimiLibFiles++;
|
|
8200
|
+
} catch (_) { /* best-effort */ }
|
|
8201
|
+
}
|
|
8202
|
+
try { fs.rmdirSync(kimiHooksLibDir); } catch (_) { /* not empty or other error — leave it */ }
|
|
8203
|
+
if (removedKimiLibFiles > 0) {
|
|
8204
|
+
steps++;
|
|
8205
|
+
console.log(` ${green}✓${reset} Removed ${removedKimiLibFiles} hooks/lib/ helper(s) from ${kimiHooksLibDir}`);
|
|
8206
|
+
}
|
|
8207
|
+
}
|
|
8208
|
+
|
|
8209
|
+
// #2544: the marker now lives inside kimi's hooks/ dir — remove it
|
|
8210
|
+
// before the emptiness check below, or the dir would never prune.
|
|
8211
|
+
if (removeCommonJsMarker(kimiHooksDir)) {
|
|
8212
|
+
steps++;
|
|
8213
|
+
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksDir}`);
|
|
8214
|
+
}
|
|
8215
|
+
|
|
8216
|
+
try {
|
|
8217
|
+
if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir);
|
|
8218
|
+
} catch (_) { /* not empty — leave it */ }
|
|
8219
|
+
}
|
|
8220
|
+
|
|
8221
|
+
// Retire the pre-#2544 marker at kimi's root (~/.kimi), where the bundle
|
|
8222
|
+
// used to write it. Exact content match — a user's own package.json in
|
|
8223
|
+
// kimi's native config home is never touched.
|
|
8224
|
+
if (removeCommonJsMarker(kimiHooksRoot)) {
|
|
8225
|
+
steps++;
|
|
8226
|
+
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot} (pre-#2544 marker)`);
|
|
8227
|
+
}
|
|
8228
|
+
return steps;
|
|
8229
|
+
}
|
|
8230
|
+
|
|
8034
8231
|
/**
|
|
8035
8232
|
* Uninstall GSD from the specified directory for a specific runtime
|
|
8036
8233
|
* Removes only GSD-specific files/directories, preserves user content
|
|
@@ -8109,6 +8306,23 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8109
8306
|
|
|
8110
8307
|
let removedCount = 0;
|
|
8111
8308
|
|
|
8309
|
+
// #2875 (#1874-F19 anti-inertness, test-matrix C7): recover any user
|
|
8310
|
+
// artifact orphaned by a PRIOR uninstall run that died between staging and
|
|
8311
|
+
// its own restore/discard, BEFORE this run's own preserve steps (sites 2,
|
|
8312
|
+
// 3, 5 below) stage anything new. Uninstall's own gsd-core/ removal and
|
|
8313
|
+
// legacy-commands cleanup are exactly as crash-exposed as install's —
|
|
8314
|
+
// without this, an orphan from a crashed uninstall is recoverable only if
|
|
8315
|
+
// the user later re-installs.
|
|
8316
|
+
// #2875 defect fix: DEGRADE, never abort uninstall, when the staging root
|
|
8317
|
+
// itself cannot be resolved — skip this recovery pass rather than throw
|
|
8318
|
+
// out of uninstall() before it does anything.
|
|
8319
|
+
{
|
|
8320
|
+
const _uninstallEntryStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
8321
|
+
if (_uninstallEntryStagingRoot !== null) {
|
|
8322
|
+
recoverOrphanedUserArtifacts(_uninstallEntryStagingRoot, targetDir);
|
|
8323
|
+
}
|
|
8324
|
+
}
|
|
8325
|
+
|
|
8112
8326
|
// Remove profile marker so a clean reinstall defaults to full surface.
|
|
8113
8327
|
try {
|
|
8114
8328
|
fs.unlinkSync(path.join(targetDir, '.gsd-profile'));
|
|
@@ -8116,7 +8330,16 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8116
8330
|
} catch {}
|
|
8117
8331
|
|
|
8118
8332
|
// 1. Remove GSD commands/skills (layout-driven)
|
|
8119
|
-
|
|
8333
|
+
// #2870: scope id resolved ONCE here and reused below (was two independent
|
|
8334
|
+
// isGlobal-derived re-derivations). Routed through the Install Scope
|
|
8335
|
+
// Module (src/install-scope.cts) when the capability registry is
|
|
8336
|
+
// available; degrades to the plain id on failure (unknown/non-installable
|
|
8337
|
+
// runtime, broken bundle) so this function's scope-id uses — which never
|
|
8338
|
+
// depended on registry availability before this migration — keep working
|
|
8339
|
+
// exactly as they did pre-migration.
|
|
8340
|
+
const _uninstallScopeId = isGlobal ? 'global' : 'local';
|
|
8341
|
+
const _resolvedUninstallScope = _resolveScopeSafe(_uninstallScopeId, runtime);
|
|
8342
|
+
const scope = _resolvedUninstallScope ? _resolvedUninstallScope.id : _uninstallScopeId;
|
|
8120
8343
|
// ADR-1239 / #2086: drive uninstall through the public Host-Integration Interface.
|
|
8121
8344
|
// Fail-open to the engine directly if the composed-registry adapter can't load.
|
|
8122
8345
|
const _uninstallAdapter = _runtimeAdapter(runtime);
|
|
@@ -8202,72 +8425,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8202
8425
|
// cleanup can't be driven by anything under targetDir the way every other
|
|
8203
8426
|
// hook surface above is.
|
|
8204
8427
|
if (resolveInstallPlan(runtime).hooksSurface === 'kimi-hooks-toml') {
|
|
8205
|
-
|
|
8206
|
-
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
8207
|
-
const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath);
|
|
8208
|
-
if (kimiHooksCleanup.changed) {
|
|
8209
|
-
removedCount++;
|
|
8210
|
-
console.log(` ${green}✓${reset} Removed GSD hooks from ${kimiHooksTomlPath}`);
|
|
8211
|
-
}
|
|
8212
|
-
|
|
8213
|
-
// Kimi's shared hook scripts + CommonJS package.json marker are installed
|
|
8214
|
-
// into this SAME ~/.kimi root (installSharedHooksBundle, install()'s
|
|
8215
|
-
// kimi-hooks-toml branch) rather than under targetDir — mirror steps "4.
|
|
8216
|
-
// Remove GSD hooks" / "5. Remove GSD package.json" below, but scoped to
|
|
8217
|
-
// kimiHooksRoot. ~/.kimi is Kimi's own native config home (shared space —
|
|
8218
|
-
// may hold the user's real config.toml/providers), so only the exact
|
|
8219
|
-
// GSD-owned filenames are removed, and directories are pruned only if left
|
|
8220
|
-
// empty by that removal.
|
|
8221
|
-
const kimiHooksDir = path.join(kimiHooksRoot, 'hooks');
|
|
8222
|
-
if (fs.existsSync(kimiHooksDir)) {
|
|
8223
|
-
let kimiHookCount = 0;
|
|
8224
|
-
for (const hook of GSD_UNINSTALL_HOOKS) {
|
|
8225
|
-
const hookPath = path.join(kimiHooksDir, hook);
|
|
8226
|
-
if (fs.existsSync(hookPath)) {
|
|
8227
|
-
fs.unlinkSync(hookPath);
|
|
8228
|
-
kimiHookCount++;
|
|
8229
|
-
}
|
|
8230
|
-
}
|
|
8231
|
-
if (kimiHookCount > 0) {
|
|
8232
|
-
removedCount++;
|
|
8233
|
-
console.log(` ${green}✓${reset} Removed ${kimiHookCount} GSD hooks from ${kimiHooksDir}`);
|
|
8234
|
-
}
|
|
8235
|
-
|
|
8236
|
-
const kimiHooksLibDir = path.join(kimiHooksDir, 'lib');
|
|
8237
|
-
if (fs.existsSync(kimiHooksLibDir)) {
|
|
8238
|
-
let removedKimiLibFiles = 0;
|
|
8239
|
-
for (const file of GSD_HOOK_LIB_FILES) {
|
|
8240
|
-
try {
|
|
8241
|
-
fs.unlinkSync(path.join(kimiHooksLibDir, file));
|
|
8242
|
-
removedKimiLibFiles++;
|
|
8243
|
-
} catch (_) { /* best-effort */ }
|
|
8244
|
-
}
|
|
8245
|
-
try { fs.rmdirSync(kimiHooksLibDir); } catch (_) { /* not empty or other error — leave it */ }
|
|
8246
|
-
if (removedKimiLibFiles > 0) {
|
|
8247
|
-
removedCount++;
|
|
8248
|
-
console.log(` ${green}✓${reset} Removed ${removedKimiLibFiles} hooks/lib/ helper(s) from ${kimiHooksLibDir}`);
|
|
8249
|
-
}
|
|
8250
|
-
}
|
|
8251
|
-
|
|
8252
|
-
// #2544: the marker now lives inside kimi's hooks/ dir — remove it
|
|
8253
|
-
// before the emptiness check below, or the dir would never prune.
|
|
8254
|
-
if (removeCommonJsMarker(kimiHooksDir)) {
|
|
8255
|
-
removedCount++;
|
|
8256
|
-
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksDir}`);
|
|
8257
|
-
}
|
|
8258
|
-
|
|
8259
|
-
try {
|
|
8260
|
-
if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir);
|
|
8261
|
-
} catch (_) { /* not empty — leave it */ }
|
|
8262
|
-
}
|
|
8263
|
-
|
|
8264
|
-
// Retire the pre-#2544 marker at kimi's root (~/.kimi), where the bundle
|
|
8265
|
-
// used to write it. Exact content match — a user's own package.json in
|
|
8266
|
-
// kimi's native config home is never touched.
|
|
8267
|
-
if (removeCommonJsMarker(kimiHooksRoot)) {
|
|
8268
|
-
removedCount++;
|
|
8269
|
-
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot} (pre-#2544 marker)`);
|
|
8270
|
-
}
|
|
8428
|
+
removedCount += reclaimKimiHooksRoot(resolveKimiHooksTomlDir({ runtime }));
|
|
8271
8429
|
}
|
|
8272
8430
|
|
|
8273
8431
|
// 1b. Non-layout Copilot side-effect: copilot-instructions.md cleanup
|
|
@@ -8443,18 +8601,42 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8443
8601
|
// Preserve user-owned dev-preferences.md if present (#1423 parity).
|
|
8444
8602
|
const legacyGsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
|
|
8445
8603
|
if (fs.existsSync(legacyGsdCommandsDir)) {
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8604
|
+
// Stage user-owned dev-preferences.md DURABLY before wiping (#2875 /
|
|
8605
|
+
// #1874-F19 "site 7" — found by sweeping bin/install.js for the
|
|
8606
|
+
// read-then-wipe-then-write PATTERN, not for preserveUserArtifacts'
|
|
8607
|
+
// callers; this uninstall-path block open-coded the same round-trip).
|
|
8608
|
+
// #2875 defect fix: DEGRADE, never abort uninstall, when the staging
|
|
8609
|
+
// root cannot be resolved — skip this legacy-cleanup block entirely
|
|
8610
|
+
// (leave the stale dir in place) rather than wipe without a durable
|
|
8611
|
+
// backup for dev-preferences.md.
|
|
8612
|
+
const _legacyGsdCommandsStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
8613
|
+
if (_legacyGsdCommandsStagingRoot !== null) {
|
|
8614
|
+
const stagedDevPrefs = stageUserArtifacts(legacyGsdCommandsDir, ['dev-preferences.md'], _legacyGsdCommandsStagingRoot);
|
|
8615
|
+
// Preserve the ORIGINAL truthy-content check exactly: an existing but
|
|
8616
|
+
// EMPTY dev-preferences.md was (and still is) silently not restored.
|
|
8617
|
+
const savedDevPrefs = stagedDevPrefs.names.includes('dev-preferences.md')
|
|
8618
|
+
? fs.readFileSync(path.join(stagedDevPrefs.filesDir, 'dev-preferences.md'), 'utf8')
|
|
8619
|
+
: null;
|
|
8620
|
+
fs.rmSync(legacyGsdCommandsDir, { recursive: true });
|
|
8621
|
+
removedCount++;
|
|
8622
|
+
console.log(` ${green}✓${reset} Removed legacy commands/gsd/`);
|
|
8623
|
+
if (savedDevPrefs) {
|
|
8624
|
+
try {
|
|
8625
|
+
restoreStagedUserArtifacts(legacyGsdCommandsDir, stagedDevPrefs);
|
|
8626
|
+
discardStagedUserArtifacts(stagedDevPrefs);
|
|
8627
|
+
console.log(` ${green}✓${reset} Preserved commands/gsd/dev-preferences.md`);
|
|
8628
|
+
} catch (err) {
|
|
8629
|
+
console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`);
|
|
8630
|
+
}
|
|
8631
|
+
} else {
|
|
8632
|
+
// #2875 defect fix: an existing-but-EMPTY dev-preferences.md was (and
|
|
8633
|
+
// still is) never restored — the original truthy-content check is
|
|
8634
|
+
// preserved byte-for-byte above — but the staged batch was never
|
|
8635
|
+
// discarded either, leaking a <configDir>/.gsd-staging/ record
|
|
8636
|
+
// forever and re-materializing the just-deleted file on a future
|
|
8637
|
+
// install's orphan-recovery pass. Discard unconditionally when there
|
|
8638
|
+
// is nothing to restore.
|
|
8639
|
+
discardStagedUserArtifacts(stagedDevPrefs);
|
|
8458
8640
|
}
|
|
8459
8641
|
}
|
|
8460
8642
|
}
|
|
@@ -8472,21 +8654,77 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8472
8654
|
// so this is a best-effort guard.
|
|
8473
8655
|
const legacyDir = path.join(targetDir, 'commands', 'gsd');
|
|
8474
8656
|
if (fs.existsSync(legacyDir)) {
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
const
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8657
|
+
// #2875 (#1874-F19): staged DURABLY to disk before the wipe below,
|
|
8658
|
+
// instead of an in-memory Map only — a crash between the wipe and the
|
|
8659
|
+
// restore-on-failure branch below now survives via
|
|
8660
|
+
// recoverOrphanedUserArtifacts on the next run.
|
|
8661
|
+
// #2875 defect fix: DEGRADE, never abort uninstall, when the staging
|
|
8662
|
+
// root cannot be resolved — skip this legacy-migration block entirely
|
|
8663
|
+
// (leave the stale dir in place) rather than wipe without a durable
|
|
8664
|
+
// backup.
|
|
8665
|
+
const stagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
8666
|
+
if (stagingRoot !== null) {
|
|
8667
|
+
const stagedLegacyArtifacts = stageUserArtifacts(legacyDir, ['dev-preferences.md'], stagingRoot);
|
|
8668
|
+
fs.rmSync(legacyDir, { recursive: true });
|
|
8669
|
+
removedCount++;
|
|
8670
|
+
console.log(` ${green}✓${reset} Removed legacy commands/gsd/`);
|
|
8671
|
+
const _uninstallScope = scope;
|
|
8672
|
+
// migrateLegacyDevPreferencesToSkill's Map<string,string> contract is
|
|
8673
|
+
// unchanged — read the staged content back from disk (not an in-memory
|
|
8674
|
+
// value held across the wipe above).
|
|
8675
|
+
//
|
|
8676
|
+
// #2875 defect fix (readFileSync following a staged symlink) — matches
|
|
8677
|
+
// install-engine.cts's _runLegacyInstallMigrations call site 1 exactly:
|
|
8678
|
+
// readFileSync ALWAYS follows a symlink, so a staged artifact that is
|
|
8679
|
+
// itself a symlink (user-artifact-staging.cts's "Symlink safety": a
|
|
8680
|
+
// symlinked user artifact is recreated AS a symlink in the staging
|
|
8681
|
+
// tree, never copied by content) would have its REFERENT's bytes read
|
|
8682
|
+
// here and land in SKILL.md. Excluded from migration below and
|
|
8683
|
+
// restored to its original location unchanged instead.
|
|
8684
|
+
// #2875 defect fix (regression closed — was previously unguarded and
|
|
8685
|
+
// BRICKED uninstall, the very command that should recover from this):
|
|
8686
|
+
// legacyDir was already removed above, so stagedLegacyArtifacts is
|
|
8687
|
+
// the only surviving copy. migrateLegacyDevPreferencesToSkill
|
|
8688
|
+
// correctly THROWS when it finds a planted/dangling symlink at the
|
|
8689
|
+
// skill-file leaf (security fix); a raw `fs.lstatSync` in the loop
|
|
8690
|
+
// below can also throw on a TOCTOU-vanished staged file. Either one,
|
|
8691
|
+
// left unguarded, propagated straight out of uninstall, aborting it
|
|
8692
|
+
// WITHOUT ever reaching the restore-or-discard branch below — the
|
|
8693
|
+
// staged batch was orphaned on disk and every retry hit the same
|
|
8694
|
+
// throw again. Degrade identically to every other #2875 staging step
|
|
8695
|
+
// in this function: catch, warn once, and treat the batch as
|
|
8696
|
+
// unmigrated so the restore branch below always fires.
|
|
8697
|
+
let _legacyMigrated = false;
|
|
8698
|
+
let migratableLegacyNames = [];
|
|
8699
|
+
try {
|
|
8700
|
+
const savedLegacyArtifacts = new Map();
|
|
8701
|
+
for (const name of stagedLegacyArtifacts.names) {
|
|
8702
|
+
const stagedPath = path.join(stagedLegacyArtifacts.filesDir, name);
|
|
8703
|
+
if (fs.lstatSync(stagedPath).isSymbolicLink()) continue;
|
|
8704
|
+
savedLegacyArtifacts.set(name, fs.readFileSync(stagedPath, 'utf8'));
|
|
8705
|
+
migratableLegacyNames.push(name);
|
|
8706
|
+
}
|
|
8707
|
+
_legacyMigrated = migrateLegacyDevPreferencesToSkill(targetDir, savedLegacyArtifacts, runtime, _uninstallScope);
|
|
8708
|
+
} catch (err) {
|
|
8709
|
+
console.warn(` ${yellow}!${reset} dev-preferences.md migration skipped (${err.message}) — restoring the legacy copy instead.`);
|
|
8710
|
+
_legacyMigrated = false;
|
|
8711
|
+
migratableLegacyNames = [];
|
|
8712
|
+
}
|
|
8713
|
+
if (_legacyMigrated && migratableLegacyNames.length === stagedLegacyArtifacts.names.length) {
|
|
8714
|
+
// Compute the actual path written so the log line is accurate per-runtime
|
|
8715
|
+
const _layout = resolveRuntimeArtifactLayout(runtime, targetDir, _uninstallScope);
|
|
8716
|
+
const _sk = _layout.kinds.find((k) => k.kind === 'skills');
|
|
8717
|
+
const _stem = _sk && _sk.prefix === '' ? 'dev-preferences' : 'gsd-dev-preferences';
|
|
8718
|
+
const _skillRelPath = _sk ? `${_sk.destSubpath}/${_stem}/SKILL.md` : 'skills/gsd-dev-preferences/SKILL.md';
|
|
8719
|
+
console.log(` ${green}✓${reset} Migrated dev-preferences.md → ${_skillRelPath} (#2973)`);
|
|
8720
|
+
discardStagedUserArtifacts(stagedLegacyArtifacts);
|
|
8721
|
+
} else {
|
|
8722
|
+
// Migration failed, already exists, or a symlinked name was excluded
|
|
8723
|
+
// above — restore the WHOLE batch to the legacy location so no user
|
|
8724
|
+
// content is silently lost.
|
|
8725
|
+
restoreStagedUserArtifacts(legacyDir, stagedLegacyArtifacts);
|
|
8726
|
+
discardStagedUserArtifacts(stagedLegacyArtifacts);
|
|
8727
|
+
}
|
|
8490
8728
|
}
|
|
8491
8729
|
}
|
|
8492
8730
|
}
|
|
@@ -8494,22 +8732,49 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8494
8732
|
// 2. Remove gsd-core directory
|
|
8495
8733
|
const gsdDir = path.join(targetDir, 'gsd-core');
|
|
8496
8734
|
if (fs.existsSync(gsdDir)) {
|
|
8497
|
-
//
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8735
|
+
// Stage user-generated files DURABLY to disk before wipe (#1423; #2875 /
|
|
8736
|
+
// #1874-F19 "site 5" — this block open-coded its own preserve/restore
|
|
8737
|
+
// instead of calling preserveUserArtifacts, which is why it was missed
|
|
8738
|
+
// by the original symbol-search measurement).
|
|
8739
|
+
// #2875 defect fix: this IS the core uninstall step (removing gsd-core/)
|
|
8740
|
+
// — unlike the optional legacy-cleanup blocks above, uninstall must
|
|
8741
|
+
// still be able to proceed and actually remove gsd-core/ even when the
|
|
8742
|
+
// staging root cannot be resolved. Degrade by skipping ONLY the
|
|
8743
|
+
// USER-PROFILE.md preserve/restore wrapper (warn), never the removal
|
|
8744
|
+
// itself.
|
|
8745
|
+
const _gsdDirStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
8746
|
+
if (_gsdDirStagingRoot === null) {
|
|
8747
|
+
console.warn(` ${yellow}!${reset} Skipping gsd-core/USER-PROFILE.md preservation (staging unavailable) — it will be lost if present.`);
|
|
8748
|
+
fs.rmSync(gsdDir, { recursive: true });
|
|
8749
|
+
removedCount++;
|
|
8750
|
+
console.log(` ${green}✓${reset} Removed gsd-core/`);
|
|
8751
|
+
} else {
|
|
8752
|
+
const stagedProfile = stageUserArtifacts(gsdDir, USER_OWNED_ARTIFACTS, _gsdDirStagingRoot);
|
|
8753
|
+
// Preserve the ORIGINAL truthy-content check exactly: an existing but
|
|
8754
|
+
// EMPTY USER-PROFILE.md was (and still is) silently not restored —
|
|
8755
|
+
// matching prior behavior byte-for-byte rather than widening scope.
|
|
8756
|
+
const preservedProfile = stagedProfile.names.includes('USER-PROFILE.md')
|
|
8757
|
+
? fs.readFileSync(path.join(stagedProfile.filesDir, 'USER-PROFILE.md'), 'utf8')
|
|
8758
|
+
: null;
|
|
8759
|
+
|
|
8760
|
+
fs.rmSync(gsdDir, { recursive: true });
|
|
8761
|
+
removedCount++;
|
|
8762
|
+
console.log(` ${green}✓${reset} Removed gsd-core/`);
|
|
8504
8763
|
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8764
|
+
// Restore user-generated files
|
|
8765
|
+
if (preservedProfile) {
|
|
8766
|
+
try {
|
|
8767
|
+
restoreStagedUserArtifacts(gsdDir, stagedProfile);
|
|
8768
|
+
discardStagedUserArtifacts(stagedProfile);
|
|
8769
|
+
console.log(` ${green}✓${reset} Preserved gsd-core/USER-PROFILE.md`);
|
|
8770
|
+
} catch (err) {
|
|
8771
|
+
console.error(` ${red}✗${reset} Failed to restore USER-PROFILE.md: ${err.message}`);
|
|
8772
|
+
}
|
|
8773
|
+
} else {
|
|
8774
|
+
// #2875 defect fix: same empty-file orphan leak as the legacy
|
|
8775
|
+
// commands/gsd/ site above — discard the staging batch regardless of
|
|
8776
|
+
// whether the staged content was truthy.
|
|
8777
|
+
discardStagedUserArtifacts(stagedProfile);
|
|
8513
8778
|
}
|
|
8514
8779
|
}
|
|
8515
8780
|
}
|
|
@@ -8642,13 +8907,8 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8642
8907
|
// Any file NOT in this set is user-owned and must survive uninstall.
|
|
8643
8908
|
// After removing GSD files, attempt to rmdir — if the directory is still
|
|
8644
8909
|
// non-empty (user has custom helpers) it stays; otherwise it goes cleanly.
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
'github-release-notes.cjs', 'lint.cjs', 'new.cjs',
|
|
8648
|
-
'README.md', // documentation only — not user-authored
|
|
8649
|
-
];
|
|
8650
|
-
const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs'];
|
|
8651
|
-
|
|
8910
|
+
// GSD_CHANGESET_FILES / GSD_SCRIPTS_LIB_FILES are module-scoped (#3184) so
|
|
8911
|
+
// tests can assert their parity against the real directory contents.
|
|
8652
8912
|
const changesetUninstallDir = path.join(targetDir, 'scripts', 'changeset');
|
|
8653
8913
|
if (fs.existsSync(changesetUninstallDir)) {
|
|
8654
8914
|
let removedChangeset = 0;
|
|
@@ -9525,23 +9785,48 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9525
9785
|
// so the manifest records what's actually on disk. _resolveSkillsRootDir already
|
|
9526
9786
|
// resolves destSubpath (which includes hermes's 'skills/gsd' nesting) — do not
|
|
9527
9787
|
// re-append 'gsd' or the hermes dir gets double-nested to skills/gsd/gsd.
|
|
9528
|
-
|
|
9788
|
+
// #2872 (ADR-2866 Phase 3): the scope used to pick the skills root and the
|
|
9789
|
+
// scope RECORDED in the manifest are one value, resolved once. Two reads of
|
|
9790
|
+
// `options.scope` could drift; one cannot.
|
|
9791
|
+
const resolvedScope = options.scope === 'local' ? 'local' : 'global';
|
|
9792
|
+
const codexSkillsDir = _resolveSkillsRootDir(runtime, configDir, resolvedScope);
|
|
9529
9793
|
const codexSkillsManifestPrefix = _hostBehaviors(runtime).skillsManifestPrefix || 'skills/';
|
|
9530
|
-
|
|
9794
|
+
// #3738: resolve the ACTUAL agents-install dir honoring an agents-kind `home`
|
|
9795
|
+
// override (antigravity global → $HOME/.gemini/config/agents), mirroring
|
|
9796
|
+
// _resolveSkillsRootDir for skills. Hardcoding configDir/agents left the
|
|
9797
|
+
// manifest blind to the whole agents surface the moment the override landed —
|
|
9798
|
+
// no drift detection, no patch backup. Falls back to <configDir>/agents.
|
|
9799
|
+
const agentsDir = _kindDestDirSafe(runtime, configDir, resolvedScope, 'agents')
|
|
9800
|
+
|| _kindDestDirSafe(runtime, configDir, resolvedScope, 'kimi-agents')
|
|
9801
|
+
|| path.join(configDir, 'agents');
|
|
9531
9802
|
const manifest = {
|
|
9803
|
+
// Schema version of this DOCUMENT (#2872) — distinct from `version`
|
|
9804
|
+
// below, which is the GSD package version. Absent ⇒ a pre-#2872 (v1)
|
|
9805
|
+
// manifest, which readInstallManifest still reads without error and
|
|
9806
|
+
// without requiring a reinstall. Read from the Installer Migration
|
|
9807
|
+
// Module rather than repeated as a second literal: the writer here and
|
|
9808
|
+
// the reader's normalizeManifestVersion are two surfaces over one
|
|
9809
|
+
// constant, and this repo's "generative fix divergence" class is exactly
|
|
9810
|
+
// two such literals drifting apart.
|
|
9811
|
+
manifestVersion: MANIFEST_SCHEMA_VERSION,
|
|
9532
9812
|
version: pkg.version,
|
|
9533
9813
|
timestamp: new Date().toISOString(),
|
|
9534
9814
|
mode: options.mode === 'minimal' ? 'minimal' : 'full',
|
|
9815
|
+
// Recorded so an Installed Surface Resolver can answer "which surfaces
|
|
9816
|
+
// are installed, at which scopes, for which runtimes" without re-deriving
|
|
9817
|
+
// it from the directory it happened to be found in (#2872).
|
|
9818
|
+
runtime,
|
|
9819
|
+
scope: resolvedScope,
|
|
9535
9820
|
files: {},
|
|
9536
9821
|
};
|
|
9537
9822
|
|
|
9538
9823
|
const gsdHashes = generateManifest(gsdDir);
|
|
9539
9824
|
for (const [rel, hash] of Object.entries(gsdHashes)) {
|
|
9540
|
-
// Skip user-owned artifacts (e.g. USER-PROFILE.md). They are
|
|
9541
|
-
//
|
|
9542
|
-
//
|
|
9543
|
-
// as a "local patch"
|
|
9544
|
-
// USER_OWNED_ARTIFACTS at top of file.
|
|
9825
|
+
// Skip user-owned artifacts (e.g. USER-PROFILE.md). They are staged
|
|
9826
|
+
// durably and restored across reinstalls (user-artifact-staging.cts,
|
|
9827
|
+
// #2875) and must NOT be hashed into the manifest — otherwise
|
|
9828
|
+
// saveLocalPatches() would flag every refresh as a "local patch"
|
|
9829
|
+
// (bug #2771). Single source of truth: USER_OWNED_ARTIFACTS at top of file.
|
|
9545
9830
|
if (USER_OWNED_ARTIFACTS.includes(rel)) continue;
|
|
9546
9831
|
manifest.files['gsd-core/' + rel] = hash;
|
|
9547
9832
|
}
|
|
@@ -10029,21 +10314,24 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10029
10314
|
// below were removed, leaving isKimi unused in this function (the kimi
|
|
10030
10315
|
// local-install-deferred branch above already reads
|
|
10031
10316
|
// _hostBehaviors(runtime).localInstallDeferred instead of this flag).
|
|
10032
|
-
// #2096: isAntigravity dropped — antigravity
|
|
10033
|
-
//
|
|
10034
|
-
// (the path-rewrite skip and the converter dispatch) were
|
|
10035
|
-
// code; both were removed rather than re-gated on
|
|
10036
|
-
// #
|
|
10037
|
-
// _DESCRIPTOR_AGENTS_RUNTIMES
|
|
10038
|
-
//
|
|
10317
|
+
// #2096: isAntigravity dropped — antigravity's agents were already
|
|
10318
|
+
// descriptor-driven (installRuntimeArtifacts), so its two legacy-agent-loop
|
|
10319
|
+
// branches (the path-rewrite skip and the converter dispatch) were
|
|
10320
|
+
// unreachable dead code; both were removed rather than re-gated on
|
|
10321
|
+
// hostBehaviors. (#2875 Part 2 later deleted that legacy loop and its
|
|
10322
|
+
// `_DESCRIPTOR_AGENTS_RUNTIMES` gate entirely — EVERY runtime is now
|
|
10323
|
+
// descriptor-driven for agents, not just this subset.)
|
|
10324
|
+
// #2098: isCodebuddy dropped — codebuddy's agents were likewise already
|
|
10325
|
+
// descriptor-driven, so its legacy converter-dispatch branch (the
|
|
10326
|
+
// `isCodebuddy` arm calling convertClaudeAgentToCodebuddyAgent) was
|
|
10039
10327
|
// unreachable dead code and was removed rather than re-gated.
|
|
10040
|
-
// #2099: isCopilot dropped — copilot
|
|
10041
|
-
//
|
|
10042
|
-
// the converter dispatch, and the .agent.md destName
|
|
10043
|
-
// unreachable dead code and were removed rather than
|
|
10044
|
-
// .agent.md suffix now lives on
|
|
10045
|
-
// src/install-engine.cts, and the
|
|
10046
|
-
// longer needs `&& !isCopilot`.
|
|
10328
|
+
// #2099: isCopilot dropped — copilot's agents were likewise already
|
|
10329
|
+
// descriptor-driven, so its three legacy-agent-loop branches (the
|
|
10330
|
+
// path-rewrite skip, the converter dispatch, and the .agent.md destName
|
|
10331
|
+
// ternary) were unreachable dead code and were removed rather than
|
|
10332
|
+
// re-gated; the .agent.md suffix now lives on
|
|
10333
|
+
// hostBehaviors.agentFileExtension in src/install-engine.cts, and the
|
|
10334
|
+
// skipSharedHooksInstall check above no longer needs `&& !isCopilot`.
|
|
10047
10335
|
// #2100: isWindsurf dropped — its four former isWindsurf-gated branches
|
|
10048
10336
|
// (legacy .devin/skills/gsd-* cleanup, the #1629 command-bodies copy, the
|
|
10049
10337
|
// workflow-verification report, and the shared-hooks-install exclusion) are
|
|
@@ -10051,14 +10339,20 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10051
10339
|
// hostBehaviors.installsCommandBodiesForWorkflowDelegation,
|
|
10052
10340
|
// hostBehaviors.verificationStyle === 'windsurf-workflows', and
|
|
10053
10341
|
// hostBehaviors.skipSharedHooksInstall respectively; its legacy-agent-loop
|
|
10054
|
-
// converter arm was likewise unreachable dead code (windsurf
|
|
10055
|
-
//
|
|
10342
|
+
// converter arm was likewise unreachable dead code (windsurf's agents were
|
|
10343
|
+
// already descriptor-driven) and was removed above.
|
|
10056
10344
|
// #2101: isZcode dropped — folded onto hostBehaviors.skipSharedHooksInstall.
|
|
10057
10345
|
const { isOpencode, isCodex, isCursor, isAugment, isTrae, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
|
10058
10346
|
const plan = resolveInstallPlan(runtime);
|
|
10059
10347
|
const dirName = getDirName(runtime);
|
|
10060
10348
|
const src = path.join(__dirname, '..');
|
|
10061
10349
|
|
|
10350
|
+
// #3241 — the Codex resolver-model-omitted notice dedupes "at most once", but
|
|
10351
|
+
// scoped per install() call rather than per process — each install() run gets
|
|
10352
|
+
// its own fresh window so a second install (e.g. a second runtime, or a test
|
|
10353
|
+
// re-running install()) can warn again if the same condition recurs.
|
|
10354
|
+
_codexResolverModelOmittedWarned = false;
|
|
10355
|
+
|
|
10062
10356
|
if (_hostBehaviors(runtime).localInstallDeferred && !isGlobal) {
|
|
10063
10357
|
console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 2.`);
|
|
10064
10358
|
console.log(` No .kimi-code/skills or .agents/skills project artifacts were written.`);
|
|
@@ -10076,6 +10370,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10076
10370
|
};
|
|
10077
10371
|
}
|
|
10078
10372
|
|
|
10373
|
+
// #2870: scope id resolved ONCE here and reused at every use below (was 10
|
|
10374
|
+
// independent isGlobal-derived re-derivations). Placed AFTER the kimi
|
|
10375
|
+
// local-deferred early return above so that return path does no extra
|
|
10376
|
+
// work. Routed through the Install Scope Module (src/install-scope.cts)
|
|
10377
|
+
// when the capability registry is available; degrades to the plain id on
|
|
10378
|
+
// failure (unknown/non-installable runtime, broken bundle) so this
|
|
10379
|
+
// function's plain scope-id uses — which never depended on registry
|
|
10380
|
+
// availability before this migration — keep working exactly as they did
|
|
10381
|
+
// pre-migration. `_installScope` (the full resolved value, not just the
|
|
10382
|
+
// id) additionally backs the settingsFileByScope routing below.
|
|
10383
|
+
const _installScopeId = isGlobal ? 'global' : 'local';
|
|
10384
|
+
const _installScope = _resolveScopeSafe(_installScopeId, runtime);
|
|
10385
|
+
|
|
10079
10386
|
// Reusable helper to copy hooks/lib/ (git-cmd.js + gsd-graphify-rebuild.sh).
|
|
10080
10387
|
// Defined early so it is visible to both the main and Codex code paths.
|
|
10081
10388
|
// `allowlist` (when non-empty) restricts copying to the named top-level entries,
|
|
@@ -10121,6 +10428,32 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10121
10428
|
? process.cwd()
|
|
10122
10429
|
: path.join(process.cwd(), dirName);
|
|
10123
10430
|
|
|
10431
|
+
// #3664: a --config-dir destination holding foreign agent files gets an
|
|
10432
|
+
// explicit install-time warning — never a silent Claude-shaped emit.
|
|
10433
|
+
if (isGlobal) {
|
|
10434
|
+
warnIfForeignAgentDest(runtime, targetDir, _installScopeId, Boolean(explicitConfigDir));
|
|
10435
|
+
}
|
|
10436
|
+
|
|
10437
|
+
// #2875 (#1874-F19 anti-inertness, test-matrix C7): recover any user
|
|
10438
|
+
// artifact orphaned by a PRIOR install run that died between staging and
|
|
10439
|
+
// its own restore/discard, BEFORE this run's own preserve step stages
|
|
10440
|
+
// anything new. This is the production entry point every install() call
|
|
10441
|
+
// reaches — the only place this phase's durability fix is complete rather
|
|
10442
|
+
// than merely callable (40-design.md "The inertness trap this design must
|
|
10443
|
+
// avoid" / #1879-F15). Runs for every runtime, ahead of both the
|
|
10444
|
+
// layout-driven path's _runLegacyInstallMigrations (site 1, inside
|
|
10445
|
+
// installRuntimeArtifacts) and this function's own mainline gsd-core copy
|
|
10446
|
+
// (site 4, below).
|
|
10447
|
+
// #2875 defect fix: DEGRADE, never abort install, when the staging root
|
|
10448
|
+
// itself cannot be resolved — skip this recovery pass rather than throw
|
|
10449
|
+
// out of install() before it does anything.
|
|
10450
|
+
{
|
|
10451
|
+
const _installEntryStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
10452
|
+
if (_installEntryStagingRoot !== null) {
|
|
10453
|
+
recoverOrphanedUserArtifacts(_installEntryStagingRoot, targetDir);
|
|
10454
|
+
}
|
|
10455
|
+
}
|
|
10456
|
+
|
|
10124
10457
|
const locationLabel = isGlobal
|
|
10125
10458
|
? targetDir.replace(os.homedir(), '~')
|
|
10126
10459
|
: targetDir.replace(process.cwd(), '.');
|
|
@@ -10273,7 +10606,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10273
10606
|
const codexPreInstallAgentContents = new Map();
|
|
10274
10607
|
let codexPreInstallVersionBytes = null;
|
|
10275
10608
|
if (_hostBehaviors(runtime).tomlConfigInstall && !isMinimalMode(_effectiveInstallMode)) {
|
|
10276
|
-
const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir,
|
|
10609
|
+
const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
|
|
10277
10610
|
if (fs.existsSync(_preSkillsDir)) {
|
|
10278
10611
|
for (const entry of fs.readdirSync(_preSkillsDir, { withFileTypes: true })) {
|
|
10279
10612
|
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
|
@@ -10329,7 +10662,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10329
10662
|
const _codexPreConfigRollback = !_hostBehaviors(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
|
|
10330
10663
|
rollbackInstallerMigrations();
|
|
10331
10664
|
// skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
|
|
10332
|
-
const _earlySkillsDir = _resolveSkillsRootDir(runtime, targetDir,
|
|
10665
|
+
const _earlySkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
|
|
10333
10666
|
for (const skillName of codexPreInstallSkillNames) {
|
|
10334
10667
|
const skillDirPath = path.join(_earlySkillsDir, skillName);
|
|
10335
10668
|
const fileMap = codexPreInstallSkillContents.get(skillName);
|
|
@@ -10427,7 +10760,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10427
10760
|
installerMigrationResult = runInstallerMigrations({
|
|
10428
10761
|
configDir: targetDir,
|
|
10429
10762
|
runtime,
|
|
10430
|
-
scope:
|
|
10763
|
+
scope: _installScopeId,
|
|
10431
10764
|
migrations: options.installerMigrations,
|
|
10432
10765
|
baselineScan: true,
|
|
10433
10766
|
});
|
|
@@ -10512,6 +10845,17 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10512
10845
|
// (copyWithPathReplacement + stale-skills cleanup).
|
|
10513
10846
|
const _isSkillsRuntime = (() => {
|
|
10514
10847
|
if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086)
|
|
10848
|
+
// #2875 Part 2 defect fix: a runtime whose LOCAL commands are embedded in a
|
|
10849
|
+
// rules file rather than materialized as files (hostBehaviors.localCommandsViaRules
|
|
10850
|
+
// — cline is the only declarant, capabilities/cline/capability.json) must not
|
|
10851
|
+
// flip into this skills/commands-reporting branch merely because its local
|
|
10852
|
+
// artifactLayout now also declares an `agents` kind (#2875 Part 2 cline-local
|
|
10853
|
+
// agents regression fix). That branch's own verification reporting expects a
|
|
10854
|
+
// skills/ or commands/ directory this runtime never writes locally and would
|
|
10855
|
+
// spuriously fail; the `localCommandsViaRules` branch below (unchanged
|
|
10856
|
+
// messaging) and the unconditional agents-materialization block further down
|
|
10857
|
+
// (installAgentsKindStandalone) already cover this runtime/scope correctly.
|
|
10858
|
+
if (!isGlobal && _hostBehaviors(runtime).localCommandsViaRules) return false;
|
|
10515
10859
|
const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
|
|
10516
10860
|
const layout = cap && cap.runtime && cap.runtime.artifactLayout;
|
|
10517
10861
|
if (!layout) return false;
|
|
@@ -10560,7 +10904,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10560
10904
|
|
|
10561
10905
|
if (_isSkillsRuntime) {
|
|
10562
10906
|
// Layout-driven install for skills-based runtimes (full and minimal modes)
|
|
10563
|
-
const scope =
|
|
10907
|
+
const scope = _installScopeId;
|
|
10564
10908
|
// ADR-1239 upgrade 3 / #2088: a kind may declare an alternate install `home`
|
|
10565
10909
|
// (e.g. Codex skills -> $HOME/.agents/skills) instead of the runtime's normal
|
|
10566
10910
|
// configDir. Resolve the ACTUAL on-disk skills root here, descriptor-driven
|
|
@@ -10796,15 +11140,34 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10796
11140
|
// that used the namespaced layout (wrote bare-name files under commands/gsd/).
|
|
10797
11141
|
const legacyGsdDir = path.join(commandsDir, 'gsd');
|
|
10798
11142
|
if (fs.existsSync(legacyGsdDir)) {
|
|
10799
|
-
//
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
11143
|
+
// Stage user-owned dev-preferences.md DURABLY before wiping (#2875 /
|
|
11144
|
+
// #1874-F19 "site 6" — this Claude commands-install path open-coded
|
|
11145
|
+
// its own preserve/restore instead of calling preserveUserArtifacts,
|
|
11146
|
+
// found by sweeping for the read-then-wipe-then-write PATTERN rather
|
|
11147
|
+
// than for that helper's callers).
|
|
11148
|
+
// #2875 defect fix: DEGRADE, never abort install, when the staging
|
|
11149
|
+
// root cannot be resolved — skip this legacy-migration block entirely
|
|
11150
|
+
// (leave the stale dir in place) rather than wipe without a durable
|
|
11151
|
+
// backup.
|
|
11152
|
+
const _legacyGsdStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
11153
|
+
if (_legacyGsdStagingRoot !== null) {
|
|
11154
|
+
const stagedDevPrefs = stageUserArtifacts(legacyGsdDir, ['dev-preferences.md'], _legacyGsdStagingRoot);
|
|
11155
|
+
// Preserve the ORIGINAL truthy-content check exactly: an existing but
|
|
11156
|
+
// EMPTY dev-preferences.md was (and still is) silently not migrated —
|
|
11157
|
+
// matching prior behavior byte-for-byte rather than widening scope.
|
|
11158
|
+
const preservedDevPrefs = stagedDevPrefs.names.includes('dev-preferences.md')
|
|
11159
|
+
? fs.readFileSync(path.join(stagedDevPrefs.filesDir, 'dev-preferences.md'), 'utf8')
|
|
11160
|
+
: null;
|
|
11161
|
+
fs.rmSync(legacyGsdDir, { recursive: true });
|
|
11162
|
+
console.log(` ${green}✓${reset} Removed legacy commands/gsd/ (migrated to flat gsd-<cmd>.md layout)`);
|
|
11163
|
+
if (preservedDevPrefs) {
|
|
11164
|
+
// Migrate dev-preferences to the new flat form — a RENAME on
|
|
11165
|
+
// restore (staged as 'dev-preferences.md', restored as
|
|
11166
|
+
// 'gsd-dev-preferences.md'), not a round-trip.
|
|
11167
|
+
restoreStagedUserArtifacts(commandsDir, stagedDevPrefs, { rename: { 'dev-preferences.md': 'gsd-dev-preferences.md' } });
|
|
11168
|
+
console.log(` ${green}✓${reset} Migrated dev-preferences.md to commands/gsd-dev-preferences.md`);
|
|
11169
|
+
}
|
|
11170
|
+
discardStagedUserArtifacts(stagedDevPrefs);
|
|
10808
11171
|
}
|
|
10809
11172
|
}
|
|
10810
11173
|
|
|
@@ -10835,12 +11198,29 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10835
11198
|
}
|
|
10836
11199
|
|
|
10837
11200
|
// Copy gsd-core skill with path replacement
|
|
10838
|
-
//
|
|
11201
|
+
// Stage user-generated files DURABLY to disk before the wipe-and-copy so
|
|
11202
|
+
// they survive re-install even if the process dies mid-copy (#2875 /
|
|
11203
|
+
// #1874-F19) — copyWithPathReplacement wipes and recursively re-copies the
|
|
11204
|
+
// entire gsd-core/ tree, the single longest operation in the install, on
|
|
11205
|
+
// the path every user takes (40-design.md "Site 4 is far worse...").
|
|
10839
11206
|
const skillSrc = path.join(src, 'gsd-core');
|
|
10840
11207
|
const skillDest = path.join(targetDir, 'gsd-core');
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
11208
|
+
// #2875 defect fix: this IS the mainline install step (installing
|
|
11209
|
+
// gsd-core/ itself) — unlike the optional legacy-cleanup blocks above,
|
|
11210
|
+
// install must still be able to proceed and actually write gsd-core/ even
|
|
11211
|
+
// when the staging root cannot be resolved. Degrade by skipping ONLY the
|
|
11212
|
+
// USER_OWNED_ARTIFACTS preserve/restore wrapper around the copy (warn),
|
|
11213
|
+
// never the copy itself.
|
|
11214
|
+
const _gsdArtifactsStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
11215
|
+
if (_gsdArtifactsStagingRoot === null) {
|
|
11216
|
+
console.warn(` ${yellow}!${reset} Skipping gsd-core/${USER_OWNED_ARTIFACTS.join(', gsd-core/')} preservation (staging unavailable) — it will be lost if present.`);
|
|
11217
|
+
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
|
|
11218
|
+
} else {
|
|
11219
|
+
const stagedGsdArtifacts = stageUserArtifacts(skillDest, USER_OWNED_ARTIFACTS, _gsdArtifactsStagingRoot);
|
|
11220
|
+
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
|
|
11221
|
+
restoreStagedUserArtifacts(skillDest, stagedGsdArtifacts);
|
|
11222
|
+
discardStagedUserArtifacts(stagedGsdArtifacts);
|
|
11223
|
+
}
|
|
10844
11224
|
if (verifyInstalled(skillDest, 'gsd-core')) {
|
|
10845
11225
|
console.log(` ${green}✓${reset} Installed workflow assets`);
|
|
10846
11226
|
} else {
|
|
@@ -10902,219 +11282,89 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10902
11282
|
}
|
|
10903
11283
|
}
|
|
10904
11284
|
|
|
10905
|
-
//
|
|
10906
|
-
//
|
|
10907
|
-
//
|
|
10908
|
-
//
|
|
10909
|
-
//
|
|
10910
|
-
//
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
//
|
|
10915
|
-
//
|
|
10916
|
-
//
|
|
10917
|
-
//
|
|
10918
|
-
//
|
|
10919
|
-
//
|
|
10920
|
-
//
|
|
10921
|
-
//
|
|
10922
|
-
//
|
|
10923
|
-
//
|
|
10924
|
-
//
|
|
10925
|
-
//
|
|
10926
|
-
//
|
|
10927
|
-
//
|
|
10928
|
-
//
|
|
10929
|
-
//
|
|
10930
|
-
//
|
|
10931
|
-
//
|
|
10932
|
-
const _DESCRIPTOR_AGENTS_RUNTIMES = new Set(['cursor', 'windsurf', 'augment', 'trae', 'codebuddy', 'copilot', 'antigravity', 'qwen', 'kimi']);
|
|
10933
|
-
|
|
10934
|
-
// Always remove stale gsd-* agents first so re-installing with
|
|
10935
|
-
// `--minimal` actually shrinks a previously-full install.
|
|
10936
|
-
// For Codex this also covers per-agent `.toml` files alongside the `.md`
|
|
10937
|
-
// sources so a full → minimal switch doesn't leave stale registrations.
|
|
10938
|
-
// Skipped for descriptor-agent runtimes (installRuntimeArtifacts prunes) and
|
|
10939
|
-
// for pluginOnlyInstall runtimes (pi, ADR-1239 / #2102 Stage 1 — no agents/
|
|
10940
|
-
// dir is ever written for them, see the leading branch below).
|
|
10941
|
-
if (!_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime) && !_hostBehaviors(runtime).pluginOnlyInstall && fs.existsSync(agentsDest)) {
|
|
10942
|
-
for (const file of fs.readdirSync(agentsDest)) {
|
|
10943
|
-
if (
|
|
10944
|
-
file.startsWith('gsd-') &&
|
|
10945
|
-
(file.endsWith('.md') || (_hostBehaviors(runtime).agentTomlFiles && file.endsWith('.toml')))
|
|
10946
|
-
) {
|
|
10947
|
-
fs.unlinkSync(path.join(agentsDest, file));
|
|
10948
|
-
}
|
|
10949
|
-
}
|
|
10950
|
-
}
|
|
10951
|
-
|
|
11285
|
+
// Agents directory materialization.
|
|
11286
|
+
// #2875 Part 2 (the agents-bypass closure): EVERY runtime is now
|
|
11287
|
+
// descriptor-driven for agents — installRuntimeArtifacts (called earlier in
|
|
11288
|
+
// this function, the `_isSkillsRuntime` branch above) already wrote
|
|
11289
|
+
// agents/ for any runtime whose capability.json declares an `agents` kind,
|
|
11290
|
+
// via convertedAgentsKind/agentsKind (generic layout loop) or
|
|
11291
|
+
// installAgentsKindStandalone (OpenCode/Kilo's combinedFamilyInstall
|
|
11292
|
+
// branch, called from within installOpencodeFamilyArtifacts) — both reuse
|
|
11293
|
+
// the SAME stageAgentsForRuntimeWithConverter pipeline (path-rewrite →
|
|
11294
|
+
// attribution → converter → frontmatter extensions → normalize) the
|
|
11295
|
+
// inline loop this replaces used to hand-roll, and both prune stale gsd-*
|
|
11296
|
+
// entries via their own _removeGsdEntries pass BEFORE copying (broader
|
|
11297
|
+
// than this loop's old extension-gated stale check — see
|
|
11298
|
+
// runtime-artifact-layout.cts's convertedAgentsKind doc comment).
|
|
11299
|
+
// Minimal-mode agent filtering is handled the SAME way it already was for
|
|
11300
|
+
// the ten runtimes cut over before this change: via resolvedProfile.agents
|
|
11301
|
+
// at staging time, not a separate branch here.
|
|
11302
|
+
//
|
|
11303
|
+
// `!_isSkillsRuntime` runtimes (claude-local's legacy-flat local path, and
|
|
11304
|
+
// pi) never reach that loop at all — installAgentsKindStandalone is called
|
|
11305
|
+
// here explicitly to cover them (install-engine.cts's own doc comment
|
|
11306
|
+
// explains why; a regression here was caught by the install-tree golden
|
|
11307
|
+
// fixture, tests/fixtures/install-tree/claude-local.json). It is a no-op
|
|
11308
|
+
// for pi: its capability.json declares an EMPTY artifactLayout for both
|
|
11309
|
+
// scopes (programmatic dispatch, no named-dispatch subagent toolkit, no
|
|
11310
|
+
// host-read markdown surface), so the resolved layout has no `agents` kind
|
|
11311
|
+
// to stage and the function returns `null` without writing anything.
|
|
10952
11312
|
if (_hostBehaviors(runtime).pluginOnlyInstall) {
|
|
10953
|
-
// pi (ADR-1239 / #2102 Stage 1): programmatic dispatch has no named-dispatch
|
|
10954
|
-
// subagent toolkit (dispatch.subagentToolkit: "undocumented", no Agent-tool
|
|
10955
|
-
// equivalent) and no host-read markdown surface — skip writing agents/ entirely.
|
|
10956
11313
|
console.log(` ${green}✓${reset} pi: no subagent files (programmatic dispatch, no named-dispatch toolkit)`);
|
|
10957
|
-
} else if (
|
|
10958
|
-
// installRuntimeArtifacts already wrote agents + handles stale-file cleanup
|
|
10959
|
-
// via its own prune pass. No further action needed.
|
|
11314
|
+
} else if (_isSkillsRuntime) {
|
|
10960
11315
|
console.log(` ${dim}↳${reset} Agents installed via descriptor-driven layout (${runtime})`);
|
|
10961
|
-
} else
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
|
|
10972
|
-
|
|
10973
|
-
}
|
|
10974
|
-
|
|
10975
|
-
|
|
11316
|
+
} else {
|
|
11317
|
+
const _standaloneAgentsResult = installAgentsKindStandalone(runtime, targetDir, _installScopeId, _resolvedProfile, pathPrefix, getCommitAttribution, _installedCapabilityRegistry);
|
|
11318
|
+
if (_standaloneAgentsResult) {
|
|
11319
|
+
// #2875 defect fix: installAgentsKindStandalone now returns `null`
|
|
11320
|
+
// (rather than a truthy result pointing at an empty destDir) whenever a
|
|
11321
|
+
// restricted (non-'*') resolvedProfile — --minimal being the common
|
|
11322
|
+
// case — legitimately stages ZERO agents, matching the pre-#2875-Part-2
|
|
11323
|
+
// inline loop's behavior of never creating agentsDest under --minimal
|
|
11324
|
+
// at all (see the deleted `isMinimalMode` branch). `destDir` is
|
|
11325
|
+
// therefore guaranteed non-empty whenever we reach this branch, so a
|
|
11326
|
+
// real staging failure still fails loudly via verifyInstalled below.
|
|
11327
|
+
if (verifyInstalled(_standaloneAgentsResult.destDir, 'agents')) {
|
|
11328
|
+
console.log(` ${green}✓${reset} Installed agents`);
|
|
11329
|
+
} else {
|
|
11330
|
+
failures.push('agents');
|
|
10976
11331
|
}
|
|
10977
|
-
}
|
|
10978
|
-
|
|
10979
|
-
} else if (fs.existsSync(agentsSrc)) {
|
|
10980
|
-
fs.mkdirSync(agentsDest, { recursive: true });
|
|
10981
|
-
|
|
10982
|
-
// Copy new agents
|
|
10983
|
-
const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
|
|
10984
|
-
for (const entry of agentEntries) {
|
|
10985
|
-
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
10986
|
-
const agentSourcePath = path.join(agentsSrc, entry.name);
|
|
10987
|
-
let content = fs.readFileSync(agentSourcePath, 'utf8');
|
|
10988
|
-
// #2995 (epic #1671 Phase 6.4): strip `<!-- gsd:section -->` markers BEFORE
|
|
10989
|
-
// the path-rewrite regexes below, so a rewrite can never reach inside a
|
|
10990
|
-
// marker attribute. No-op (byte-identical) for an unmarked agent.
|
|
10991
|
-
content = composeWorkflow(content, { sourcePath: agentSourcePath });
|
|
10992
|
-
// Replace ~/.claude/ and $HOME/.claude/ as they are the source of truth in the repo
|
|
10993
|
-
const dirRegex = /~\/\.claude\//g;
|
|
10994
|
-
const homeDirRegex = /\$HOME\/\.claude\//g;
|
|
10995
|
-
const bareDirRegex = /~\/\.claude\b/g;
|
|
10996
|
-
const bareHomeDirRegex = /\$HOME\/\.claude\b/g;
|
|
10997
|
-
const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
|
|
10998
|
-
// #2096: `&& !isAntigravity` dropped — antigravity is in
|
|
10999
|
-
// _DESCRIPTOR_AGENTS_RUNTIMES above, so this whole branch is already
|
|
11000
|
-
// unreachable for it; the path-rewrite skip for antigravity now lives
|
|
11001
|
-
// in the descriptor-driven `applyAgentPathRewrites` (hostBehaviors.noPathRewrite).
|
|
11002
|
-
// #2099: `if (!isCopilot)` guard dropped — copilot is ALSO in
|
|
11003
|
-
// _DESCRIPTOR_AGENTS_RUNTIMES (line ~9564 above), so this whole
|
|
11004
|
-
// `else if (fs.existsSync(agentsSrc))` branch is unreachable for it;
|
|
11005
|
-
// isCopilot was therefore always false here, making the guard a no-op.
|
|
11006
|
-
content = content.replace(dirRegex, pathPrefix);
|
|
11007
|
-
content = content.replace(homeDirRegex, pathPrefix);
|
|
11008
|
-
content = content.replace(bareDirRegex, normalizedPathPrefix);
|
|
11009
|
-
content = content.replace(bareHomeDirRegex, normalizedPathPrefix);
|
|
11010
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
11011
|
-
// Convert frontmatter for runtime compatibility (agents need different handling)
|
|
11012
|
-
if (_hostBehaviors(runtime).frontmatterDialect === 'opencode') {
|
|
11013
|
-
// Resolve per-agent model for OpenCode agents.
|
|
11014
|
-
// Precedence: model_overrides[agent] > model_profile_overrides.opencode.<tier> > omit.
|
|
11015
|
-
// model_overrides (#2256): explicit per-agent override, highest precedence.
|
|
11016
|
-
// model_profile_overrides (#2794): tier-based runtime resolver, same parity as Codex.
|
|
11017
|
-
const _ocAgentName = entry.name.replace(/\.md$/, '');
|
|
11018
|
-
const _ocModelOverrides = readGsdEffectiveModelOverrides(targetDir);
|
|
11019
|
-
let _ocModelOverride = _ocModelOverrides?.[_ocAgentName] || null;
|
|
11020
|
-
if (!_ocModelOverride) {
|
|
11021
|
-
// Fall back to tier-based resolution via model_profile_overrides.opencode.<tier>.
|
|
11022
|
-
const _ocRuntimeResolver = readGsdRuntimeProfileResolver(targetDir);
|
|
11023
|
-
if (_ocRuntimeResolver) {
|
|
11024
|
-
const _ocEntry = _ocRuntimeResolver.resolve(_ocAgentName);
|
|
11025
|
-
if (_ocEntry?.model) {
|
|
11026
|
-
_ocModelOverride = _ocEntry.model;
|
|
11027
|
-
}
|
|
11028
|
-
}
|
|
11029
|
-
}
|
|
11030
|
-
content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true, modelOverride: _ocModelOverride });
|
|
11031
|
-
} else if (_hostBehaviors(runtime).frontmatterDialect === 'kilo') {
|
|
11032
|
-
// Resolve per-agent model for Kilo agents (#2093 UPGRADE 2; Kilo is an
|
|
11033
|
-
// OpenCode fork with the same static-frontmatter model constraint).
|
|
11034
|
-
// Precedence: model_overrides[agent] > model_profile_overrides.kilo.<tier> > omit.
|
|
11035
|
-
// model_overrides (#2256): explicit per-agent override, highest precedence.
|
|
11036
|
-
// model_profile_overrides (#2794): tier-based runtime resolver, same parity as OpenCode.
|
|
11037
|
-
const _kiloAgentName = entry.name.replace(/\.md$/, '');
|
|
11038
|
-
const _kiloModelOverrides = readGsdEffectiveModelOverrides(targetDir);
|
|
11039
|
-
let _kiloModelOverride = _kiloModelOverrides?.[_kiloAgentName] || null;
|
|
11040
|
-
if (!_kiloModelOverride) {
|
|
11041
|
-
// Fall back to tier-based resolution via model_profile_overrides.kilo.<tier>.
|
|
11042
|
-
const _kiloRuntimeResolver = readGsdRuntimeProfileResolver(targetDir);
|
|
11043
|
-
if (_kiloRuntimeResolver) {
|
|
11044
|
-
const _kiloEntry = _kiloRuntimeResolver.resolve(_kiloAgentName);
|
|
11045
|
-
if (_kiloEntry?.model) {
|
|
11046
|
-
_kiloModelOverride = _kiloEntry.model;
|
|
11047
|
-
}
|
|
11048
|
-
}
|
|
11049
|
-
}
|
|
11050
|
-
content = convertClaudeToKiloFrontmatter(content, { isAgent: true, modelOverride: _kiloModelOverride });
|
|
11051
|
-
} else if (_hostBehaviors(runtime).frontmatterDialect === 'codex') {
|
|
11052
|
-
content = convertClaudeAgentToCodexAgent(content);
|
|
11053
|
-
// #2099: `else if (isCopilot)` arm dropped — copilot is unreachable
|
|
11054
|
-
// here (see the isCopilot-guard-drop comment above); its content
|
|
11055
|
-
// conversion is applied pre-staging via the descriptor's
|
|
11056
|
-
// artifactLayout.converter (runtime-artifact-layout.cts), independent
|
|
11057
|
-
// of this legacy loop.
|
|
11058
|
-
// #2100: `else if (isWindsurf)` arm dropped — windsurf is ALSO in
|
|
11059
|
-
// _DESCRIPTOR_AGENTS_RUNTIMES (line ~9575 above), so this whole
|
|
11060
|
-
// `else if (fs.existsSync(agentsSrc))` branch is unreachable for it;
|
|
11061
|
-
// isWindsurf was therefore always false here, making the arm dead.
|
|
11062
|
-
// Its content conversion is applied pre-staging via the descriptor's
|
|
11063
|
-
// artifactLayout.converter (convertClaudeAgentToWindsurfAgent),
|
|
11064
|
-
// independent of this legacy loop.
|
|
11065
|
-
} else if (_hostBehaviors(runtime).frontmatterDialect === 'cline') {
|
|
11066
|
-
// Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
|
|
11067
|
-
// hostBehaviors.frontmatterDialect === 'cline'.
|
|
11068
|
-
content = convertClaudeAgentToClineAgent(content);
|
|
11069
|
-
} else if (_hostBehaviors(runtime).brandingRewrites) {
|
|
11070
|
-
// Descriptor-driven (ADR-1239 / #2092): folded from separate
|
|
11071
|
-
// `isQwen` / hermes-hardcoded branches into a single read of
|
|
11072
|
-
// runtime.hostBehaviors.brandingRewrites (qwen -> QWEN.md/Qwen
|
|
11073
|
-
// Code/.qwen/, hermes -> HERMES.md/Hermes Agent/.hermes/).
|
|
11074
|
-
const _b = _hostBehaviors(runtime).brandingRewrites;
|
|
11075
|
-
content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
|
|
11076
|
-
content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
|
|
11077
|
-
content = content.replace(/\.claude\//g, _b['.claude/']);
|
|
11078
|
-
}
|
|
11079
|
-
// #443 — Inject `effort:` into the Claude .md frontmatter ONLY.
|
|
11080
|
-
// OpenCode/Qwen/Hermes also produce .md files but break on
|
|
11081
|
-
// unknown frontmatter keys (the repo bans skills:/permissionMode: for
|
|
11082
|
-
// the same reason — see tests/agent-frontmatter.test.cjs).
|
|
11083
|
-
// Claude Code reads per-subagent `effort:` frontmatter (anthropics/claude-code #31536).
|
|
11084
|
-
// Injection is per-runtime at install time because the canonical source
|
|
11085
|
-
// agents/*.md must stay runtime-safe (no effort: key in source).
|
|
11086
|
-
if ((_hostBehaviors(runtime).agentFrontmatterExtensions || []).includes('effort')) {
|
|
11087
|
-
const _effortCfg = readGsdEffectiveEffortConfig(targetDir);
|
|
11088
|
-
const _agentName = entry.name.replace(/\.md$/, '');
|
|
11089
|
-
const _universalEffort = resolveInstallTimeEffort(_effortCfg, _agentName);
|
|
11090
|
-
const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime(runtime, _universalEffort).value;
|
|
11091
|
-
content = injectEffortFrontmatter(content, _renderedEffort);
|
|
11092
|
-
const _disallowedTools = READONLY_AGENT_DISALLOWED_TOOLS[_agentName];
|
|
11093
|
-
if (_disallowedTools) content = injectDisallowedToolsFrontmatter(content, _disallowedTools);
|
|
11094
|
-
}
|
|
11095
|
-
// #3677 — normalize retired `/gsd:<cmd>` colon refs in the agent body
|
|
11096
|
-
// to the canonical hyphen form `/gsd-<cmd>` for hyphen-`name:`
|
|
11097
|
-
// runtimes (claude / qwen / hermes). Self-converting and
|
|
11098
|
-
// colon-canonical runtimes are skipped by the predicate — see
|
|
11099
|
-
// shouldNormalizeHyphenNamespaceInAgentBody above. Mirrors the
|
|
11100
|
-
// SKILL.md-body fix shipped via #3629.
|
|
11101
|
-
content = normalizeAgentBodyForRuntime(content, runtime, readGsdCommandNames());
|
|
11102
|
-
// #2099: `isCopilot ? ... : entry.name` ternary dropped — copilot is
|
|
11103
|
-
// unreachable here (see the isCopilot-guard-drop comment above), so
|
|
11104
|
-
// the ternary always evaluated to entry.name in practice; its
|
|
11105
|
-
// .agent.md suffix is applied by the descriptor-driven fold in
|
|
11106
|
-
// src/install-engine.cts (hostBehaviors.agentFileExtension).
|
|
11107
|
-
const destName = entry.name;
|
|
11108
|
-
fs.writeFileSync(path.join(agentsDest, destName), content);
|
|
11109
|
-
}
|
|
11110
|
-
}
|
|
11111
|
-
if (verifyInstalled(agentsDest, 'agents')) {
|
|
11112
|
-
console.log(` ${green}✓${reset} Installed agents`);
|
|
11332
|
+
} else if (_resolvedProfile.skills !== '*') {
|
|
11333
|
+
console.log(` ${dim}↳${reset} Skipping agents (${_resolvedProfile.name} profile excludes all agents — run \`gsd update\` with a broader profile to add them)`);
|
|
11113
11334
|
} else {
|
|
11114
|
-
|
|
11335
|
+
console.log(` ${dim}↳${reset} No agents kind declared for ${runtime} at this scope`);
|
|
11336
|
+
}
|
|
11337
|
+
}
|
|
11338
|
+
|
|
11339
|
+
// Codex registers agents in `config.toml` via `[agents.gsd-*]` sections —
|
|
11340
|
+
// NOT agents-directory materialization (design doc "Deliberately not in
|
|
11341
|
+
// scope"), so this stays independent of the agents/ write above. Without
|
|
11342
|
+
// stripping these on a full → minimal reinstall, the runtime would keep
|
|
11343
|
+
// advertising the old full agent surface even though the descriptor-driven
|
|
11344
|
+
// write above already skipped writing the .md files for a minimal-tier
|
|
11345
|
+
// resolvedProfile. Reuse the same helper that powers `--uninstall`.
|
|
11346
|
+
if (isMinimalMode(_effectiveInstallMode) && _hostBehaviors(runtime).tomlConfigInstall) {
|
|
11347
|
+
const codexConfigPath = path.join(targetDir, 'config.toml');
|
|
11348
|
+
if (fs.existsSync(codexConfigPath)) {
|
|
11349
|
+
const existing = fs.readFileSync(codexConfigPath, 'utf8');
|
|
11350
|
+
const cleaned = stripGsdFromCodexConfig(existing);
|
|
11351
|
+
if (cleaned === null) {
|
|
11352
|
+
fs.unlinkSync(codexConfigPath);
|
|
11353
|
+
} else if (cleaned !== existing) {
|
|
11354
|
+
fs.writeFileSync(codexConfigPath, cleaned);
|
|
11355
|
+
}
|
|
11115
11356
|
}
|
|
11116
11357
|
}
|
|
11117
11358
|
|
|
11359
|
+
// agentsSrc is declared as `let` before the enclosing try block (not const)
|
|
11360
|
+
// so it is accessible by installCodexConfig() in the Codex config section
|
|
11361
|
+
// below — that function reads RAW source agents/*.md (not the
|
|
11362
|
+
// descriptor-staged output above) to build Codex's per-agent config.toml
|
|
11363
|
+
// sidecar files, a separate writer this migration deliberately does not
|
|
11364
|
+
// touch (design doc: "Codex's config.toml [agents.gsd-*] strip... is not
|
|
11365
|
+
// agents-directory materialization").
|
|
11366
|
+
agentsSrc = _stageAgents(path.join(src, 'agents'));
|
|
11367
|
+
|
|
11118
11368
|
// Copy CHANGELOG.md
|
|
11119
11369
|
const changelogSrc = path.join(src, 'CHANGELOG.md');
|
|
11120
11370
|
const changelogDest = path.join(targetDir, 'gsd-core', 'CHANGELOG.md');
|
|
@@ -11478,10 +11728,21 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11478
11728
|
// abort a successful install — log a warning and continue.
|
|
11479
11729
|
// install() is never reached in --dry-run mode (the early-exit at the CLI
|
|
11480
11730
|
// dispatch handles preview), so cleanup here always applies for real.
|
|
11481
|
-
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11731
|
+
//
|
|
11732
|
+
// #3799: when --config-dir redirected the install, the scan is SCOPED to
|
|
11733
|
+
// that destination ([targetDir]) — the default home's live install must
|
|
11734
|
+
// never be planned for removal from a sandboxed install. --no-legacy-cleanup
|
|
11735
|
+
// skips the scan entirely.
|
|
11736
|
+
const skipNoLegacyCleanup = parseNoLegacyCleanupArg();
|
|
11737
|
+
const legacyCleanupScope = (explicitConfigDir !== null && isGlobal)
|
|
11738
|
+
? [targetDir]
|
|
11739
|
+
: undefined;
|
|
11740
|
+
if (!skipNoLegacyCleanup) {
|
|
11741
|
+
try {
|
|
11742
|
+
cleanupLegacyGsdCc({ dryRun: false, ...(legacyCleanupScope ? { configDirs: legacyCleanupScope } : {}) });
|
|
11743
|
+
} catch (cleanupErr) {
|
|
11744
|
+
console.warn(` ${yellow}Warning: legacy cleanup failed: ${cleanupErr.message}${reset}`);
|
|
11745
|
+
}
|
|
11485
11746
|
}
|
|
11486
11747
|
|
|
11487
11748
|
if (failures.length > 0) {
|
|
@@ -11490,12 +11751,35 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11490
11751
|
}
|
|
11491
11752
|
|
|
11492
11753
|
// Write file manifest for future modification detection
|
|
11493
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope:
|
|
11754
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
11494
11755
|
console.log(` ${green}✓${reset} Wrote file manifest (${MANIFEST_NAME})`);
|
|
11495
11756
|
|
|
11496
11757
|
// Report any backed-up local patches
|
|
11497
11758
|
reportLocalPatches(targetDir, runtime);
|
|
11498
11759
|
|
|
11760
|
+
// #2873: cross-scope shadow report. Fires ONCE per install (this is the
|
|
11761
|
+
// only writeManifest call site that gets it — the other four sites are
|
|
11762
|
+
// sub-writes within a single install, not separate installs). A shadowed
|
|
11763
|
+
// install is a warning, never a failure (ADR-2866 Consequences), so this
|
|
11764
|
+
// never touches `failures` or `process.exit`, and the whole block is
|
|
11765
|
+
// wrapped in a try/catch that swallows everything: a report failure must
|
|
11766
|
+
// never fail an otherwise-successful install (design row C5). No options
|
|
11767
|
+
// are injected into buildShadowReport — this is the production call shape,
|
|
11768
|
+
// resolving the real machine via os.homedir()/process.cwd() defaults
|
|
11769
|
+
// inside the resolver.
|
|
11770
|
+
try {
|
|
11771
|
+
const shadowReport = buildShadowReport(runtime);
|
|
11772
|
+
const shadowLines = renderShadowReport(shadowReport);
|
|
11773
|
+
if (shadowLines.length > 0) {
|
|
11774
|
+
console.warn(`\n ${yellow}⚠${reset} ${shadowLines[0]}`);
|
|
11775
|
+
for (const line of shadowLines.slice(1)) {
|
|
11776
|
+
console.warn(` ${dim}${line}${reset}`);
|
|
11777
|
+
}
|
|
11778
|
+
}
|
|
11779
|
+
} catch (_shadowReportErr) {
|
|
11780
|
+
// Never fail an install over a reporting concern — see comment above.
|
|
11781
|
+
}
|
|
11782
|
+
|
|
11499
11783
|
// Verify no leaked .claude paths in non-Claude runtimes (manifest-scoped)
|
|
11500
11784
|
if (!_hostBehaviors(runtime).ownsClaudePaths) {
|
|
11501
11785
|
const leakedPaths = [];
|
|
@@ -11561,7 +11845,20 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11561
11845
|
// #3245 CR finding 2 — any throw in the pre-config install operations (skills copy,
|
|
11562
11846
|
// agents copy, VERSION write, manifest write, etc.) triggers the Codex pre-config
|
|
11563
11847
|
// rollback so the caller is never left in a partially-installed state.
|
|
11564
|
-
rollbackInstallerMigrations()
|
|
11848
|
+
// (The second, identical rollbackInstallerMigrations() that used to sit here was
|
|
11849
|
+
// a duplicate of the line above, not a second phase — removed in #3725 review.)
|
|
11850
|
+
// #3712 — the test-home guard refuses before any LAYOUT-DRIVEN write, so no
|
|
11851
|
+
// gsd-* directory in the skills root has been touched and there is nothing
|
|
11852
|
+
// there to undo. (Legacy install migrations DO run first; that is why the
|
|
11853
|
+
// rollbackInstallerMigrations() calls above still execute, and why the one
|
|
11854
|
+
// migration that can reach a `home` override carries its own assertion.)
|
|
11855
|
+
// Running the codex rollback anyway would delete and recreate every
|
|
11856
|
+
// snapshotted gsd-* directory in the resolved skills root, which for an
|
|
11857
|
+
// un-sandboxed codex install IS the real ~/.agents/skills: the guard's own
|
|
11858
|
+
// refusal would provoke the mutation it exists to prevent. This is the only
|
|
11859
|
+
// _codexPreConfigRollback() call site, and applySurface/uninstall cannot
|
|
11860
|
+
// reach it. Every other error still rolls back. Found by review, not by CI.
|
|
11861
|
+
if (isTestHomeGuardRefusal(_earlyInstallErr)) throw _earlyInstallErr;
|
|
11565
11862
|
if (_codexPreConfigRollback) {
|
|
11566
11863
|
_codexPreConfigRollback();
|
|
11567
11864
|
}
|
|
@@ -11641,7 +11938,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11641
11938
|
// (copyCommandsAsCodexSkills removes pre-existing gsd-* dirs before re-writing)
|
|
11642
11939
|
// are restored even when they are absent from disk at rollback time (#3245 CR).
|
|
11643
11940
|
// • Dirs that did not pre-exist: remove entirely.
|
|
11644
|
-
const _rollbackSkillsDir = _resolveSkillsRootDir(runtime, targetDir,
|
|
11941
|
+
const _rollbackSkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
|
|
11645
11942
|
// Pass 1 — restore snapshot entries (may be absent from disk if deleted mid-install).
|
|
11646
11943
|
for (const skillName of codexPreInstallSkillNames) {
|
|
11647
11944
|
const skillDirPath = path.join(_rollbackSkillsDir, skillName);
|
|
@@ -11756,7 +12053,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11756
12053
|
// Re-write the manifest now that .toml agent files exist on disk.
|
|
11757
12054
|
// The initial writeManifest call (before Codex config generation) could
|
|
11758
12055
|
// not include agents/gsd-*.toml because those files did not yet exist.
|
|
11759
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope:
|
|
12056
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
11760
12057
|
} else {
|
|
11761
12058
|
console.log(` ${dim}↳${reset} Skipping Codex agent config generation (minimal install)`);
|
|
11762
12059
|
}
|
|
@@ -12044,7 +12341,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12044
12341
|
// manifest-tracked (verified) — uninstall removes them explicitly via
|
|
12045
12342
|
// removeCursorHooksJson + its script list, and reconcile is idempotent.
|
|
12046
12343
|
// The re-run is retained for parity with the settings.json install path.
|
|
12047
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope:
|
|
12344
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
12048
12345
|
persistActiveProfileMarker();
|
|
12049
12346
|
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
12050
12347
|
}
|
|
@@ -12105,6 +12402,44 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12105
12402
|
if (kimiHooksResult.changed) {
|
|
12106
12403
|
console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
|
|
12107
12404
|
}
|
|
12405
|
+
|
|
12406
|
+
// #3031: opt-in reclaim of the pre-#2755 legacy root. Runs LAST in this
|
|
12407
|
+
// branch so the kimi-code install above is already complete and durable —
|
|
12408
|
+
// a reclaim can only ever remove, never leave the install half-written.
|
|
12409
|
+
//
|
|
12410
|
+
// Gated on `runtime === 'kimi-code'`: a `--kimi` install resolves this
|
|
12411
|
+
// very same `~/.kimi` as its own hooks root, so reclaiming there would
|
|
12412
|
+
// delete the hooks it just wrote. The flag is silently inert for kimi
|
|
12413
|
+
// rather than an error — `--all` passes every runtime through this branch,
|
|
12414
|
+
// and one opt-in flag must not fail an otherwise valid multi-runtime run.
|
|
12415
|
+
//
|
|
12416
|
+
// ALSO gated on kimi NOT being installed by this same invocation. The
|
|
12417
|
+
// flag asserts "I only use Kimi Code"; `--all`, or an explicit `--kimi
|
|
12418
|
+
// --kimi-code`, falsifies that outright. Both orderings put `kimi` BEFORE
|
|
12419
|
+
// `kimi-code` (selectRuntimesFromArgs), so without this guard the run
|
|
12420
|
+
// installs Kimi CLI's hooks and then deletes them moments later — the run
|
|
12421
|
+
// reports success and the user is left with the very breakage the opt-in
|
|
12422
|
+
// exists to prevent. Verified reproducible before this guard existed.
|
|
12423
|
+
const kimiInstalledThisRun = selectedRuntimes.includes('kimi');
|
|
12424
|
+
if (hasReclaimKimiLegacy && runtime === 'kimi-code' && kimiInstalledThisRun) {
|
|
12425
|
+
console.log(` ${dim}•${reset} Skipped --reclaim-kimi-legacy: this run also installs --kimi, so ${resolveKimiHooksTomlDir({ runtime: 'kimi' })} is a live Kimi CLI install`);
|
|
12426
|
+
} else if (hasReclaimKimiLegacy && runtime === 'kimi-code') {
|
|
12427
|
+
const legacyKimiRoot = resolveKimiHooksTomlDir({ runtime: 'kimi' });
|
|
12428
|
+
// Both roots honor their own env override (KIMI_SHARE_DIR /
|
|
12429
|
+
// KIMI_CODE_HOME). A user who points both at ONE directory collapses
|
|
12430
|
+
// "the legacy root" onto "the root this install just wrote", and an
|
|
12431
|
+
// unguarded reclaim would delete its own output. isSameDirectory compares
|
|
12432
|
+
// the DIRECTORIES, not the strings — case-insensitive filesystems and
|
|
12433
|
+
// symlinked aliases both name one dir with two spellings.
|
|
12434
|
+
if (isSameDirectory(legacyKimiRoot, kimiHooksRoot)) {
|
|
12435
|
+
console.log(` ${dim}•${reset} Skipped --reclaim-kimi-legacy: ${legacyKimiRoot} is this install's own hooks root`);
|
|
12436
|
+
} else {
|
|
12437
|
+
const reclaimed = reclaimKimiHooksRoot(legacyKimiRoot);
|
|
12438
|
+
console.log(reclaimed > 0
|
|
12439
|
+
? ` ${green}✓${reset} Reclaimed ${reclaimed} orphaned GSD artifact group(s) from ${legacyKimiRoot} (pre-#2755)`
|
|
12440
|
+
: ` ${dim}•${reset} No orphaned GSD artifacts found in ${legacyKimiRoot}`);
|
|
12441
|
+
}
|
|
12442
|
+
}
|
|
12108
12443
|
}
|
|
12109
12444
|
|
|
12110
12445
|
// ADR-1239 / #2100 Stage 2: Windsurf's own independent hooksSurface —
|
|
@@ -12131,7 +12466,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12131
12466
|
// explicitly via removeWindsurfHooksJson, and reconcileWindsurfHooksJson
|
|
12132
12467
|
// is idempotent on repeated installs, so manifest tracking isn't needed
|
|
12133
12468
|
// for correctness here.
|
|
12134
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope:
|
|
12469
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
12135
12470
|
}
|
|
12136
12471
|
|
|
12137
12472
|
persistActiveProfileMarker();
|
|
@@ -12145,7 +12480,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12145
12480
|
writeClineArtifacts(targetDir, isGlobal);
|
|
12146
12481
|
// Re-run the manifest pass: these artifacts are written *after* the earlier
|
|
12147
12482
|
// writeManifest() call, so a second pass is needed to hash-track them.
|
|
12148
|
-
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope:
|
|
12483
|
+
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
12149
12484
|
persistActiveProfileMarker();
|
|
12150
12485
|
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
12151
12486
|
}
|
|
@@ -12160,10 +12495,24 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12160
12495
|
// #338: local Claude installs write to settings.local.json (Claude Code's per-user/gitignored slot)
|
|
12161
12496
|
// so engineer-specific absolute paths (Node binary, home dir) never land in the repo-shared
|
|
12162
12497
|
// settings.json. Global installs and all other runtimes continue to use settings.json.
|
|
12498
|
+
// #2870: the CURRENT scope's settings filename is sourced from the Install
|
|
12499
|
+
// Scope Module (_installScope.settingsFile, resolveScope's per-scope field)
|
|
12500
|
+
// instead of indexing _scopedSettings by hand. _scopedSettings itself is
|
|
12501
|
+
// retained unchanged as the #338-privacy fail-safe path: _hostBehaviors
|
|
12502
|
+
// already degrades to FALLBACK_HOST_BEHAVIORS (see that constant's comment
|
|
12503
|
+
// above) when the registry fails to load, whereas resolveScope's registry
|
|
12504
|
+
// lookup throws in that same scenario (_installScope is null when it did).
|
|
12505
|
+
// Falling back to _scopedSettings[_installScopeId] there — and keeping the
|
|
12506
|
+
// non-local-claude branch's expression untouched — means this is
|
|
12507
|
+
// byte-identical to the pre-migration computation in every case, including
|
|
12508
|
+
// the broken-registry fail-safe floor.
|
|
12163
12509
|
const _scopedSettings = _hostBehaviors(runtime).settingsFileByScope || null;
|
|
12164
|
-
const
|
|
12510
|
+
const _currentScopeSettingsFile = _installScope
|
|
12511
|
+
? _installScope.settingsFile
|
|
12512
|
+
: (_scopedSettings ? (_scopedSettings[_installScopeId] ?? null) : null);
|
|
12513
|
+
const isLocalClaude = (!isGlobal && !!_currentScopeSettingsFile);
|
|
12165
12514
|
const settingsFileName = isLocalClaude
|
|
12166
|
-
?
|
|
12515
|
+
? _currentScopeSettingsFile
|
|
12167
12516
|
: ((_scopedSettings && _scopedSettings.global) || 'settings.json');
|
|
12168
12517
|
// ADR-1239 Phase B write-confinement: the descriptor-sourced settings filename
|
|
12169
12518
|
// must resolve under targetDir (this path also drives a recursive mkdirSync).
|
|
@@ -12184,9 +12533,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12184
12533
|
);
|
|
12185
12534
|
const hasGsdStatusline = sharedRaw.statusLine && sharedRaw.statusLine.command &&
|
|
12186
12535
|
isManagedHookCommand(sharedRaw.statusLine.command, { surface: 'settings-json' });
|
|
12187
|
-
|
|
12536
|
+
const needsMigration = hasGsdHooks || hasGsdStatusline;
|
|
12537
|
+
// readSettings returns null ONLY for an unparseable file — its documented
|
|
12538
|
+
// "preserve existing, don't touch" signal. Stand the WHOLE migration down
|
|
12539
|
+
// in that case: skipping just the local merge while still stripping the
|
|
12540
|
+
// shared file below would destroy the GSD entries outright instead of
|
|
12541
|
+
// relocating them. Leaving both files untouched lets the migration retry
|
|
12542
|
+
// once the user repairs the local file.
|
|
12543
|
+
const localRaw = needsMigration ? readSettings(settingsPath) : null;
|
|
12544
|
+
if (needsMigration && localRaw === null) {
|
|
12545
|
+
console.log(' ' + yellow + 'i' + reset + ' Skipping #338 migration — ' + settingsFileName +
|
|
12546
|
+
' could not be parsed. Your existing settings are preserved.');
|
|
12547
|
+
} else if (needsMigration) {
|
|
12188
12548
|
// Merge GSD entries into settings.local.json
|
|
12189
|
-
const localRaw = readSettings(settingsPath) || {};
|
|
12190
12549
|
if (hasGsdStatusline && !localRaw.statusLine) {
|
|
12191
12550
|
localRaw.statusLine = sharedRaw.statusLine;
|
|
12192
12551
|
}
|
|
@@ -12246,16 +12605,22 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12246
12605
|
if (rawSettings === null) {
|
|
12247
12606
|
console.log(' ' + yellow + 'i' + reset + ' Skipping settings.local.json configuration — file could not be parsed (comments or malformed JSON). Your existing settings are preserved.');
|
|
12248
12607
|
persistActiveProfileMarker();
|
|
12249
|
-
|
|
12608
|
+
// Callers index this result by `runtime` (installAllRuntimes' statusline
|
|
12609
|
+
// lookup), so every early exit must return the full shape — a bare return
|
|
12610
|
+
// crashes the install rather than skipping one file.
|
|
12611
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
12250
12612
|
}
|
|
12251
12613
|
const settings = validateHookFields(cleanupOrphanedHooks(rawSettings));
|
|
12252
|
-
// #3002 CR: rewrite legacy `node .../gsd-*.js` command strings
|
|
12253
|
-
//
|
|
12254
|
-
//
|
|
12255
|
-
//
|
|
12256
|
-
|
|
12614
|
+
// #3002 CR / #3662: rewrite legacy `node .../gsd-*.js` command strings (pre-
|
|
12615
|
+
// #2979 installs) AND entries baked with another environment's absolute node
|
|
12616
|
+
// path onto the runtime-resolving runner. Without this, existing managed
|
|
12617
|
+
// hook entries stay bare-`node`-prefixed or foreign-absolute across
|
|
12618
|
+
// reinstalls and remain broken under GUI/minimal-PATH runtimes and shared
|
|
12619
|
+
// config roots — the #3662 mixed state where no environment can run all
|
|
12620
|
+
// hooks.
|
|
12621
|
+
const settingsRunner = buildNodeRunnerChainToken();
|
|
12257
12622
|
if (settingsRunner && rewriteLegacyManagedNodeHookCommands(settings, settingsRunner, { platform: process.platform, runtime })) {
|
|
12258
|
-
console.log(` ${green}✓${reset} Rewrote legacy
|
|
12623
|
+
console.log(` ${green}✓${reset} Rewrote legacy managed-hook commands to the runtime-resolving node runner (#2979/#3662)`);
|
|
12259
12624
|
}
|
|
12260
12625
|
// Local installs anchor hook paths so they resolve regardless of cwd (#1906).
|
|
12261
12626
|
// Claude Code sets $CLAUDE_PROJECT_DIR; Antigravity does not — and on
|
|
@@ -12266,12 +12631,14 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12266
12631
|
// check inside projectLocalHookPrefix.
|
|
12267
12632
|
const localPrefix = projectLocalHookPrefix({ runtime, dirName, hookPathStyle: _hostBehaviors(runtime).hookPathStyle });
|
|
12268
12633
|
const hookOpts = { portableHooks: hasPortableHooks, runtime };
|
|
12269
|
-
// #2979: local-install hook commands also use
|
|
12270
|
-
//
|
|
12271
|
-
//
|
|
12272
|
-
|
|
12634
|
+
// #2979: local-install hook commands also use a runner GUI/minimal-PATH
|
|
12635
|
+
// runtimes can resolve. Bare `node` fails when the host launches the
|
|
12636
|
+
// runtime with a stripped PATH (Finder/Antigravity/etc) — #3662 replaces
|
|
12637
|
+
// the baked absolute path with the runtime-resolving chain (baked path
|
|
12638
|
+
// first, so the minimal-PATH guarantee is unchanged).
|
|
12639
|
+
const localNodeRunner = buildNodeRunnerChainToken();
|
|
12273
12640
|
const localBashRunner = resolveBashRunner({ platform: process.platform });
|
|
12274
|
-
// If we cannot resolve
|
|
12641
|
+
// If we cannot resolve a node runner AND this is a local install,
|
|
12275
12642
|
// skip managed-hook registration. Returning null from buildHookCommand on
|
|
12276
12643
|
// global installs has the same effect. Better to skip than to emit a bare
|
|
12277
12644
|
// `node` command that recreates the #2979 failure.
|
|
@@ -13276,31 +13643,49 @@ const _LEGACY_SCAN_SUBDIR_NAMES = [
|
|
|
13276
13643
|
* @param {object} [opts.logger=console] - injectable logger
|
|
13277
13644
|
* @returns {{ plan: {path:string,reason:string}[], result: object }}
|
|
13278
13645
|
*/
|
|
13279
|
-
function cleanupLegacyGsdCc({ homeDir = os.homedir(), dryRun = false, logger = console } = {}) {
|
|
13646
|
+
function cleanupLegacyGsdCc({ homeDir = os.homedir(), configDirs = null, dryRun = false, logger = console } = {}) {
|
|
13280
13647
|
// Build de-duplicated list of candidate config dirs to scan.
|
|
13281
13648
|
// Only scan under homeDir — never cwd — to prevent accidental deletion of
|
|
13282
13649
|
// the user's active-project hooks when the installer is invoked from a
|
|
13283
13650
|
// project directory that has .claude/hooks or similar subdirs.
|
|
13651
|
+
// #3799: an explicit configDirs override (install() passes [targetDir]
|
|
13652
|
+
// whenever --config-dir redirected the destination) scopes the WHOLE scan
|
|
13653
|
+
// to that dir — the default-home scan must never plan removals of a live
|
|
13654
|
+
// install that lives outside the destination the user chose.
|
|
13284
13655
|
const seen = new Set();
|
|
13285
|
-
const
|
|
13286
|
-
|
|
13287
|
-
const candidate
|
|
13288
|
-
|
|
13289
|
-
|
|
13290
|
-
|
|
13656
|
+
const scanDirs = [];
|
|
13657
|
+
if (Array.isArray(configDirs) && configDirs.length > 0) {
|
|
13658
|
+
for (const candidate of configDirs) {
|
|
13659
|
+
if (!seen.has(candidate) && fs.existsSync(candidate)) {
|
|
13660
|
+
seen.add(candidate);
|
|
13661
|
+
scanDirs.push(candidate);
|
|
13662
|
+
}
|
|
13663
|
+
}
|
|
13664
|
+
} else {
|
|
13665
|
+
for (const name of _LEGACY_SCAN_SUBDIR_NAMES) {
|
|
13666
|
+
const candidate = path.join(homeDir, name);
|
|
13667
|
+
if (!seen.has(candidate) && fs.existsSync(candidate)) {
|
|
13668
|
+
seen.add(candidate);
|
|
13669
|
+
scanDirs.push(candidate);
|
|
13670
|
+
}
|
|
13291
13671
|
}
|
|
13292
13672
|
}
|
|
13293
13673
|
|
|
13294
13674
|
// planLegacyCleanup scans each configDir and already includes the legacy
|
|
13295
13675
|
// shared cache (gsd-update-check.json) as a plan entry.
|
|
13296
|
-
const plan = planLegacyCleanup(
|
|
13676
|
+
const plan = planLegacyCleanup(scanDirs, { homeDir, ...(Array.isArray(configDirs) && configDirs.length > 0 ? { configDirs } : {}) });
|
|
13297
13677
|
|
|
13298
13678
|
// Apply the plan (dryRun honors the flag).
|
|
13299
13679
|
const result = applyLegacyCleanup(plan, { dryRun, logger });
|
|
13300
13680
|
|
|
13301
13681
|
// Also clear / preview the per-package cache so next session re-evaluates
|
|
13302
13682
|
// hook versions (replaces the former inline unlinkSync on line ~9104).
|
|
13303
|
-
|
|
13683
|
+
// #3799: under a configDirs override the cache is read/cleared under the
|
|
13684
|
+
// SCOPE root, never the default home — same invariant as the scan itself.
|
|
13685
|
+
const perPkgCacheRoot = (Array.isArray(configDirs) && configDirs.length > 0)
|
|
13686
|
+
? configDirs[0]
|
|
13687
|
+
: homeDir;
|
|
13688
|
+
const perPkgCacheFile = path.join(perPkgCacheRoot, '.cache', 'gsd', updateCacheFileName);
|
|
13304
13689
|
if (dryRun) {
|
|
13305
13690
|
logger.log('[dry-run] would remove: ' + perPkgCacheFile + ' (per-package-update-cache)');
|
|
13306
13691
|
} else {
|
|
@@ -13419,7 +13804,10 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
13419
13804
|
});
|
|
13420
13805
|
};
|
|
13421
13806
|
|
|
13422
|
-
|
|
13807
|
+
// `settings` is null on every early exit (unparseable file, skipped runtime),
|
|
13808
|
+
// and handleStatusline dereferences it — an install that declined to touch a
|
|
13809
|
+
// settings file has no statusline to prompt about, so fall through.
|
|
13810
|
+
if (primaryStatuslineResult && primaryStatuslineResult.settings) {
|
|
13423
13811
|
handleStatusline(primaryStatuslineResult.settings, isInteractive, continueAfterStatusline);
|
|
13424
13812
|
} else if (canInstallBanner) {
|
|
13425
13813
|
// No statusline-capable runtime, but at least one runtime can host the
|
|
@@ -13439,15 +13827,16 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
13439
13827
|
module.exports = {
|
|
13440
13828
|
// #3677 — hyphen-namespace normalization seam for agent bodies
|
|
13441
13829
|
shouldNormalizeHyphenNamespaceInAgentBody,
|
|
13830
|
+
// #3664: --config-dir foreign-agent-destination warning (warn-and-proceed)
|
|
13831
|
+
warnIfForeignAgentDest,
|
|
13442
13832
|
normalizeAgentBodyForRuntime,
|
|
13443
13833
|
yamlIdentifier,
|
|
13444
|
-
computePathPrefix,
|
|
13445
|
-
applyRuntimeContentRewritesInPlace,
|
|
13446
13834
|
getCodexSkillAdapterHeader,
|
|
13447
13835
|
convertClaudeCommandToCursorSkill,
|
|
13448
13836
|
convertClaudeAgentToCursorAgent,
|
|
13449
13837
|
convertClaudeAgentToCodexAgent,
|
|
13450
13838
|
generateCodexAgentToml,
|
|
13839
|
+
_resetCodexWarningDedupeForTests,
|
|
13451
13840
|
cleanupCodexSkillMetadataSidecars,
|
|
13452
13841
|
cleanupWindsurfLegacyDevinSkills,
|
|
13453
13842
|
cleanupMovedSkillsOldLocation,
|
|
@@ -13455,7 +13844,6 @@ module.exports = {
|
|
|
13455
13844
|
_resolveSkillsRootDir,
|
|
13456
13845
|
codexBareAgentsHasOnlyKnownScalars,
|
|
13457
13846
|
extractCodexUserAgentsScalars,
|
|
13458
|
-
spliceCodexAgentsScalars,
|
|
13459
13847
|
CODEX_EXTENDED_HOOK_EVENTS,
|
|
13460
13848
|
generateCodexConfigBlock,
|
|
13461
13849
|
stripGsdFromCodexConfig,
|
|
@@ -13466,13 +13854,6 @@ module.exports = {
|
|
|
13466
13854
|
validateCodexConfigSchema,
|
|
13467
13855
|
mergeCodexConfig,
|
|
13468
13856
|
installCodexConfig,
|
|
13469
|
-
readGsdRuntimeProfileResolver,
|
|
13470
|
-
readGsdEffectiveModelOverrides,
|
|
13471
|
-
readGsdEffectiveEffortConfig,
|
|
13472
|
-
resolveInstallTimeEffort,
|
|
13473
|
-
injectEffortFrontmatter,
|
|
13474
|
-
get _GSD_EFFORT_MANIFEST_TIER_DEFAULTS() { return _getGsdEffortCatalog().EFFORT_MANIFEST_TIER_DEFAULTS; },
|
|
13475
|
-
get _GSD_EFFORT_MANIFEST_DEFAULT() { return _getGsdEffortCatalog().EFFORT_MANIFEST_DEFAULT; },
|
|
13476
13857
|
install,
|
|
13477
13858
|
installAllRuntimes,
|
|
13478
13859
|
uninstall,
|
|
@@ -13482,6 +13863,10 @@ module.exports = {
|
|
|
13482
13863
|
// #3023 — shared hook bundle directory name, descriptor-driven
|
|
13483
13864
|
SHARED_HOOKS_DIR_DEFAULT,
|
|
13484
13865
|
resolveSharedHooksDirName,
|
|
13866
|
+
// #3184 — uninstall-side GSD-managed file enumerations, exported for
|
|
13867
|
+
// parity assertions against the wholesale-copy source directories
|
|
13868
|
+
GSD_CHANGESET_FILES,
|
|
13869
|
+
GSD_SCRIPTS_LIB_FILES,
|
|
13485
13870
|
convertSlashCommandsToCodexSkillMentions,
|
|
13486
13871
|
convertClaudeCommandToCodexSkill,
|
|
13487
13872
|
convertClaudeCommandToKimiSkill,
|
|
@@ -13490,8 +13875,6 @@ module.exports = {
|
|
|
13490
13875
|
buildKimiAgentArtifacts,
|
|
13491
13876
|
convertClaudeToOpencodeFrontmatter,
|
|
13492
13877
|
convertClaudeToKiloFrontmatter,
|
|
13493
|
-
convertClaudeCommandToOpencodeSkill,
|
|
13494
|
-
convertClaudeCommandToKiloSkill,
|
|
13495
13878
|
configureOpencodePermissions,
|
|
13496
13879
|
neutralizeAgentReferences,
|
|
13497
13880
|
// #768 — Claude Code permissions pre-population
|
|
@@ -13500,8 +13883,10 @@ module.exports = {
|
|
|
13500
13883
|
GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS,
|
|
13501
13884
|
GSD_CLAUDE_DENY_PERMISSIONS,
|
|
13502
13885
|
GSD_CODEX_MARKER,
|
|
13503
|
-
|
|
13504
|
-
|
|
13886
|
+
// #3897 rung 3 (ADR-3473 §8.3, HALT.md option 2)
|
|
13887
|
+
CODEX_SANDBOX_HOLDS,
|
|
13888
|
+
deriveCodexSandboxMode,
|
|
13889
|
+
validateCodexSandboxHolds,
|
|
13505
13890
|
getGlobalDir,
|
|
13506
13891
|
getConfigDirFromHome,
|
|
13507
13892
|
resolveKiloConfigPath,
|
|
@@ -13523,20 +13908,11 @@ module.exports = {
|
|
|
13523
13908
|
mergeCopilotInstructions,
|
|
13524
13909
|
stripGsdFromCopilotInstructions,
|
|
13525
13910
|
GSD_COPILOT_HOOK_FILE,
|
|
13526
|
-
buildCopilotHookConfig,
|
|
13527
|
-
writeCopilotHookConfig,
|
|
13528
13911
|
convertClaudeToAntigravityContent,
|
|
13529
13912
|
convertClaudeCommandToAntigravitySkill,
|
|
13530
13913
|
convertClaudeAgentToAntigravityAgent,
|
|
13531
13914
|
convertClaudeCommandToClaudeSkill,
|
|
13532
13915
|
skillFrontmatterName,
|
|
13533
|
-
convertClaudeToWindsurfMarkdown,
|
|
13534
|
-
convertClaudeCommandToWindsurfSkill,
|
|
13535
|
-
convertClaudeCommandToWindsurfWorkflow,
|
|
13536
|
-
convertClaudeAgentToWindsurfAgent,
|
|
13537
|
-
convertClaudeToAugmentMarkdown,
|
|
13538
|
-
convertClaudeCommandToAugmentSkill,
|
|
13539
|
-
convertClaudeAgentToAugmentAgent,
|
|
13540
13916
|
convertClaudeToTraeMarkdown,
|
|
13541
13917
|
convertClaudeCommandToTraeSkill,
|
|
13542
13918
|
convertClaudeAgentToTraeAgent,
|
|
@@ -13547,8 +13923,6 @@ module.exports = {
|
|
|
13547
13923
|
convertClaudeToCliineMarkdown,
|
|
13548
13924
|
convertClaudeCommandToClineSkill,
|
|
13549
13925
|
convertClaudeAgentToClineAgent,
|
|
13550
|
-
// #2284(b) — cross-cutting branding protected-region helper
|
|
13551
|
-
applyClaudeCodeBrandSwap,
|
|
13552
13926
|
// #2284 — Hermes named-dispatch → delegate_task projection
|
|
13553
13927
|
convertClaudeToHermesMarkdown,
|
|
13554
13928
|
projectNamedDispatchToStructuralDelegate,
|
|
@@ -13558,30 +13932,11 @@ module.exports = {
|
|
|
13558
13932
|
maskStringLiterals,
|
|
13559
13933
|
findDispatchCallSpans,
|
|
13560
13934
|
_assertProjectionComplete,
|
|
13561
|
-
_normalizeDispatchCallSpan,
|
|
13562
|
-
buildClineRulesBody,
|
|
13563
|
-
buildClineAgentsMdBody,
|
|
13564
|
-
buildClinePreToolUseHook,
|
|
13565
|
-
writeClineArtifacts,
|
|
13566
|
-
mergeGsdAgentsMd,
|
|
13567
13935
|
GSD_CURSOR_SESSION_HOOK_SCRIPT,
|
|
13568
13936
|
GSD_CURSOR_POST_TOOL_HOOK_SCRIPT,
|
|
13569
|
-
GSD_CURSOR_PRE_TOOL_HOOK_SCRIPT,
|
|
13570
|
-
GSD_CURSOR_STOP_HOOK_SCRIPT,
|
|
13571
|
-
GSD_CURSOR_SUBAGENT_START_HOOK_SCRIPT,
|
|
13572
|
-
GSD_CURSOR_SUBAGENT_STOP_HOOK_SCRIPT,
|
|
13573
|
-
GSD_CURSOR_HOOK_SCRIPTS,
|
|
13574
13937
|
GSD_CURSOR_HOOK_MARKER,
|
|
13575
|
-
buildCursorHookEntry,
|
|
13576
|
-
isManagedCursorHookEntry,
|
|
13577
|
-
reconcileCursorHooksJson,
|
|
13578
|
-
writeCursorHooksJson,
|
|
13579
|
-
removeCursorHooksJson,
|
|
13580
13938
|
GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT,
|
|
13581
13939
|
GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT,
|
|
13582
|
-
GSD_WINDSURF_HOOK_SCRIPTS,
|
|
13583
|
-
writeWindsurfHooksJson,
|
|
13584
|
-
removeWindsurfHooksJson,
|
|
13585
13940
|
stripGsdFromAgentsMd,
|
|
13586
13941
|
GSD_AGENTS_MD_MARKER,
|
|
13587
13942
|
GSD_AGENTS_MD_CLOSE_MARKER,
|
|
@@ -13589,11 +13944,9 @@ module.exports = {
|
|
|
13589
13944
|
saveLocalPatches,
|
|
13590
13945
|
reportLocalPatches,
|
|
13591
13946
|
validateHookFields,
|
|
13592
|
-
preserveUserArtifacts,
|
|
13593
|
-
restoreUserArtifacts,
|
|
13594
|
-
migrateLegacyDevPreferencesToSkill,
|
|
13595
13947
|
populatePristineDir,
|
|
13596
|
-
|
|
13948
|
+
_resolveUserArtifactStagingRoot,
|
|
13949
|
+
_tryResolveUserArtifactStagingRoot,
|
|
13597
13950
|
finishInstall,
|
|
13598
13951
|
homePathCoveredByRc,
|
|
13599
13952
|
homePathCoveredByFishConfig,
|
|
@@ -13608,34 +13961,13 @@ module.exports = {
|
|
|
13608
13961
|
buildUpdateBannerPromptText,
|
|
13609
13962
|
parseUpdateBannerInput,
|
|
13610
13963
|
buildUpdateBannerHookEntry,
|
|
13611
|
-
buildHookCommand,
|
|
13612
|
-
normalizeNodePath,
|
|
13613
|
-
resolveNodeRunner,
|
|
13614
|
-
referencesHook,
|
|
13615
|
-
applySettingsJsonHooks,
|
|
13616
|
-
rewriteLegacyManagedNodeHookCommands,
|
|
13617
|
-
buildCodexHookBlock,
|
|
13618
|
-
rewriteLegacyCodexHookBlock,
|
|
13619
|
-
buildCodexHookWindowsShimIR,
|
|
13620
|
-
ensureCodexHooksJsonSessionStart,
|
|
13621
|
-
ensureCodexHooksJsonEvent,
|
|
13622
|
-
removeCodexHooksJsonEvent,
|
|
13623
|
-
reconcileCodexHooksJsonEvent,
|
|
13624
|
-
readGsdCommandNames,
|
|
13625
|
-
installRuntimeArtifacts,
|
|
13626
|
-
installOpencodeFamilySkills,
|
|
13627
|
-
uninstallRuntimeArtifacts,
|
|
13628
13964
|
parseConfigDirFromArgs,
|
|
13629
13965
|
cleanupLegacyGsdCc,
|
|
13630
|
-
_applyRuntimeRewrites,
|
|
13631
13966
|
// #1191 — exported so tests exercise the REAL readSettings, not a replica
|
|
13632
13967
|
readSettings,
|
|
13968
|
+
writeSettings,
|
|
13969
|
+
writeNonClaudeDefaults,
|
|
13633
13970
|
stripJsonComments,
|
|
13634
|
-
// Compatibility relays retained after auditing the former broad
|
|
13635
|
-
// runtimeArtifactConversion spread (#1559).
|
|
13636
|
-
processAttribution,
|
|
13637
|
-
applyRuntimeContentRewritesForCommandsInPlace,
|
|
13638
|
-
_copyStaged,
|
|
13639
13971
|
copyWithPathReplacement,
|
|
13640
13972
|
};
|
|
13641
13973
|
|
|
@@ -13649,10 +13981,23 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
|
|
|
13649
13981
|
console.log('Dry run — no files will be modified.\n');
|
|
13650
13982
|
// cleanupLegacyGsdCc with dryRun:true is the single source of truth for
|
|
13651
13983
|
// both the legacy artifacts and the per-package cache path — no duplicate
|
|
13652
|
-
// printing here.
|
|
13653
|
-
|
|
13654
|
-
|
|
13655
|
-
|
|
13984
|
+
// printing here. #3799: the preview honors the SAME scope and skip the
|
|
13985
|
+
// real install would apply (--config-dir scopes; --no-legacy-cleanup
|
|
13986
|
+
// skips) — a preview that listed default-home paths a real install would
|
|
13987
|
+
// never touch misrepresents the run.
|
|
13988
|
+
if (parseNoLegacyCleanupArg()) {
|
|
13989
|
+
console.log(' (--no-legacy-cleanup — legacy scan skipped)');
|
|
13990
|
+
} else {
|
|
13991
|
+
const previewScope = (explicitConfigDir !== null)
|
|
13992
|
+
? [getGlobalConfigDir(DEFAULT_RUNTIME, explicitConfigDir)]
|
|
13993
|
+
: undefined;
|
|
13994
|
+
const { plan } = cleanupLegacyGsdCc({
|
|
13995
|
+
dryRun: true,
|
|
13996
|
+
...(previewScope ? { configDirs: previewScope } : {}),
|
|
13997
|
+
});
|
|
13998
|
+
if (plan.length === 0) {
|
|
13999
|
+
console.log(' (no legacy get-shit-done-cc artifacts found)');
|
|
14000
|
+
}
|
|
13656
14001
|
}
|
|
13657
14002
|
process.exit(0);
|
|
13658
14003
|
} else if (hasSkillsRoot) {
|