@opengsd/gsd-core 1.13.0 → 1.15.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/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +459 -0
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +360 -0
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +848 -163
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +8 -3
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/review.md +2 -3
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +672 -146
- package/gsd-core/bin/lib/adr-parser.cjs +4 -2
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +119 -34
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +96 -189
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -2
- package/gsd-core/bin/lib/check-command-router.cjs +213 -49
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +823 -112
- package/gsd-core/bin/lib/config-loader.cjs +66 -4
- package/gsd-core/bin/lib/config.cjs +186 -45
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +164 -45
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +614 -86
- package/gsd-core/bin/lib/install-engine.cjs +29 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +37 -13
- package/gsd-core/bin/lib/model-resolver.cjs +253 -53
- package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +268 -27
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +393 -88
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +356 -42
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +181 -30
- package/gsd-core/bin/lib/state.cjs +265 -27
- package/gsd-core/bin/lib/surface.cjs +77 -3
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +9 -1
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +315 -30
- package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
- package/gsd-core/bin/lib/verify.cjs +320 -48
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
- package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +13 -4
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +14 -2
- package/gsd-core/references/tdd.md +30 -4
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +433 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/templates/verification-report.md +1 -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 +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +6 -5
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +29 -16
- package/gsd-core/workflows/check-todos.md +6 -4
- package/gsd-core/workflows/cleanup.md +5 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +216 -73
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +41 -264
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +17 -158
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- 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 +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +83 -172
- package/gsd-core/workflows/execute-plan.md +24 -10
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +52 -5
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +56 -14
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +39 -209
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +45 -187
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +132 -20
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +29 -10
- package/gsd-core/workflows/reapply-patches.md +86 -6
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +22 -9
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +14 -12
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +101 -196
- package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +71 -19
- package/hooks/dist/gsd-statusline.js +81 -20
- package/hooks/dist/gsd-validate-commit.sh +97 -8
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +66 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +71 -19
- package/hooks/gsd-statusline.js +81 -20
- package/hooks/gsd-validate-commit.sh +97 -8
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +11 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +131 -12
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +35 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +623 -13
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +29 -9
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
- package/scripts/prompt-injection-scan.sh +18 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +8 -3
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review/SKILL.md +2 -3
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
package/bin/install.js
CHANGED
|
@@ -436,7 +436,18 @@ const GSD_CHANGESET_FILES = [
|
|
|
436
436
|
'github-release-notes.cjs', 'lint.cjs', 'new.cjs',
|
|
437
437
|
'README.md', // documentation only — not user-authored
|
|
438
438
|
];
|
|
439
|
-
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', 'shellcheck-fetch.cjs'];
|
|
439
|
+
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', 'shellcheck-fetch.cjs', 'npm-version-check-diagnosis.cjs', 'platform-conformance-tier.generated.cjs', 'suite-detection.cjs', 'macos-conformance-tier.generated.cjs'];
|
|
440
|
+
|
|
441
|
+
// #4544 — the Codex hook payload the install stages into <targetDir>/hooks/.
|
|
442
|
+
// Hoisted to module scope (the #3184 precedent above) so the rollback's
|
|
443
|
+
// incomplete-capture path can name exactly the files GSD owns without a
|
|
444
|
+
// second copy of the list drifting away from the staging site, which lives
|
|
445
|
+
// inside the Codex config block where the constant used to be declared.
|
|
446
|
+
const CODEX_HOOKS_TO_COPY = [
|
|
447
|
+
'gsd-check-update.js',
|
|
448
|
+
'gsd-check-update-worker.js',
|
|
449
|
+
'managed-hooks-registry.cjs',
|
|
450
|
+
];
|
|
440
451
|
|
|
441
452
|
/**
|
|
442
453
|
* Resolve a runtime's shared-hooks directory name from its descriptor.
|
|
@@ -544,6 +555,13 @@ const {
|
|
|
544
555
|
RUNTIME_PROFILE_MAP: GSD_RUNTIME_PROFILE_MAP,
|
|
545
556
|
isAnthropicFlavoredModel: gsdIsAnthropicFlavoredModel,
|
|
546
557
|
} = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
|
|
558
|
+
// #4145: shared hash-first recovery for gsd-pristine/ baselines stored at an
|
|
559
|
+
// unexpected path (e.g. without the gsd-core/ prefix an earlier release's
|
|
560
|
+
// writer dropped). Same module the reapply verifier uses, so the two readers
|
|
561
|
+
// cannot drift apart again.
|
|
562
|
+
const {
|
|
563
|
+
findPristineByHash: gsdFindPristineByHash,
|
|
564
|
+
} = require(path.join(_gsdLibDir, 'pristine-baseline.cjs'));
|
|
547
565
|
// #2875 Part 2: MODEL_PROFILES + resolveTierEntry are now consumed only by
|
|
548
566
|
// install-model-override-resolver.cjs's readGsdRuntimeProfileResolver
|
|
549
567
|
// (required below) — this installer no longer needs its own bindings.
|
|
@@ -805,6 +823,7 @@ const {
|
|
|
805
823
|
applyInstallerMigrationPlan,
|
|
806
824
|
discoverInstallerMigrations,
|
|
807
825
|
MANIFEST_SCHEMA_VERSION,
|
|
826
|
+
readInstallManifest,
|
|
808
827
|
runInstallerMigrations,
|
|
809
828
|
} = require(path.join(_gsdLibDir, 'installer-migrations.cjs'));
|
|
810
829
|
const {
|
|
@@ -918,6 +937,25 @@ const hasSkillsRoot = args.includes('--skills-root');
|
|
|
918
937
|
const hasPortableHooks = args.includes('--portable-hooks') || process.env.GSD_PORTABLE_HOOKS === '1';
|
|
919
938
|
const hasMinimal = args.includes('--minimal') || args.includes('--core-only');
|
|
920
939
|
const hasDryRun = args.includes('--dry-run');
|
|
940
|
+
// #4377: emit project-relative `@` includes (`.claude/gsd-core/...`) for a
|
|
941
|
+
// LOCAL install instead of this checkout's absolute path.
|
|
942
|
+
//
|
|
943
|
+
// Opt-in, and it stays opt-in: absolute includes work for a single checkout,
|
|
944
|
+
// which is nearly everyone, and flipping the default would change every
|
|
945
|
+
// existing local install to solve a problem those users do not have. The
|
|
946
|
+
// people who need it know they do — they run the same repo from several git
|
|
947
|
+
// worktrees, where a baked absolute path means every worktree reads its
|
|
948
|
+
// workflow prose out of whichever checkout happened to run the installer, and
|
|
949
|
+
// updating that one checkout breaks all the others at once with no way to
|
|
950
|
+
// stage it.
|
|
951
|
+
//
|
|
952
|
+
// Exported through the environment rather than threaded as a parameter,
|
|
953
|
+
// exactly like --portable-hooks/GSD_PORTABLE_HOOKS above: five separate seams
|
|
954
|
+
// compute a path prefix (the install engine, both rewrite entry points, the
|
|
955
|
+
// install plan, and applySurface), and one variable they all read cannot fall
|
|
956
|
+
// out of sync the way five signatures can.
|
|
957
|
+
const hasRelativeIncludes = args.includes('--relative-includes') || process.env.GSD_RELATIVE_INCLUDES === '1';
|
|
958
|
+
if (hasRelativeIncludes) process.env.GSD_RELATIVE_INCLUDES = '1';
|
|
921
959
|
// #3031: opt-in reclaim of the GSD artifacts a PRE-#2755 `--kimi-code` install
|
|
922
960
|
// orphaned in Kimi CLI's `~/.kimi`. Opt-in and not automatic because the stale
|
|
923
961
|
// block is BYTE-IDENTICAL to a legitimate Kimi CLI one — both runtimes render
|
|
@@ -1245,7 +1283,7 @@ if (hasUninstall) {
|
|
|
1245
1283
|
|
|
1246
1284
|
// Show help if requested
|
|
1247
1285
|
if (hasHelp) {
|
|
1248
|
-
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`);
|
|
1286
|
+
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}--relative-includes${reset} With --local: write project-relative @ includes\n (.claude/gsd-core/...) instead of this checkout's\n absolute path, so several git worktrees of one repo\n each read their own copy (also GSD_RELATIVE_INCLUDES=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}# Local install for a repo worked from several git worktrees${reset}\n npx ${pkg.name} --claude --local --relative-includes\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`);
|
|
1249
1287
|
process.exit(0);
|
|
1250
1288
|
}
|
|
1251
1289
|
|
|
@@ -1285,6 +1323,7 @@ const removeKimiHooksToml = hooksSurface.removeKimiHooksToml;
|
|
|
1285
1323
|
// callers continue to work and there is a single implementation. (All call
|
|
1286
1324
|
// sites are below this line, so the const binding has no TDZ hazard.)
|
|
1287
1325
|
const processAttribution = runtimeArtifactConversion.processAttribution;
|
|
1326
|
+
const filterRuntimeNotesForTarget = runtimeArtifactConversion.filterRuntimeNotesForTarget;
|
|
1288
1327
|
// computePathPrefix: implementation lives in runtimeArtifactConversion
|
|
1289
1328
|
// (ADR-1508 / #1511 Phase 2 — single owner). Re-bound here so install.js call
|
|
1290
1329
|
// sites continue to work. #2876 retired the sibling
|
|
@@ -1362,34 +1401,13 @@ function ensureCodexHooksJsonSessionStart(targetDir, opts = {}) {
|
|
|
1362
1401
|
}
|
|
1363
1402
|
|
|
1364
1403
|
/**
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
* All three events are routed through gsd-context-monitor.js — the same hook
|
|
1374
|
-
* used for PostToolUse in the Claude Code baseline — so context-headroom
|
|
1375
|
-
* warnings surface at these key Codex session lifecycle moments.
|
|
1376
|
-
*
|
|
1377
|
-
* On Windows (#3426): writes a gsd-context-monitor.cmd shim alongside the .js
|
|
1378
|
-
* file and uses the .cmd path as the hook command — exactly the same fix as
|
|
1379
|
-
* SessionStart uses for gsd-check-update — to avoid the bash.exe POSIX-exec
|
|
1380
|
-
* failure when Codex's hook dispatcher tries to run node.exe through Git Bash.
|
|
1381
|
-
*
|
|
1382
|
-
* @param {string} targetDir
|
|
1383
|
-
* @param {string} eventName - One of 'SubagentStart', 'Stop', 'PostToolUse'.
|
|
1384
|
-
* @param {{ absoluteRunner: string|null, platform?: NodeJS.Platform }} opts
|
|
1385
|
-
* @returns {{ changed: boolean, wrote: boolean, path: string }}
|
|
1386
|
-
*/
|
|
1387
|
-
function ensureCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
|
|
1388
|
-
return hooksSurface.ensureCodexHooksJsonEvent(targetDir, eventName, opts);
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
/**
|
|
1392
|
-
* Remove a GSD-managed event entry from hooks.json. Called during uninstall.
|
|
1404
|
+
* Remove a GSD-managed event entry from hooks.json. Called during uninstall,
|
|
1405
|
+
* and (#2586) unconditionally during install/reinstall to clean up a
|
|
1406
|
+
* pre-#2586 install's stale gsd-context-monitor.js registrations — GSD no
|
|
1407
|
+
* longer ADDS entries for these events (see CODEX_HOOKS_TO_COPY /
|
|
1408
|
+
* cleanupOrphanedCodexContextMonitorScript in bin/install.js's Codex branch),
|
|
1409
|
+
* only removes recognized ones, so the `ensureCodexHooksJsonEvent` wrapper
|
|
1410
|
+
* that used to add them was removed as dead code.
|
|
1393
1411
|
*
|
|
1394
1412
|
* @param {string} targetDir
|
|
1395
1413
|
* @param {string} eventName
|
|
@@ -1638,19 +1656,20 @@ const claudeToOpencodeTools = {
|
|
|
1638
1656
|
WebSearch: 'websearch', // Plugin/MCP - keep for compatibility
|
|
1639
1657
|
};
|
|
1640
1658
|
|
|
1641
|
-
// Tool name mapping from Claude Code to
|
|
1642
|
-
//
|
|
1643
|
-
const
|
|
1644
|
-
|
|
1659
|
+
// Tool name mapping from Claude Code to Antigravity
|
|
1660
|
+
// Antigravity uses Gemini's snake_case built-in tool names
|
|
1661
|
+
const claudeToAntigravityTools = {
|
|
1662
|
+
// #4705: Antigravity-NATIVE tool names (see src/runtime-artifact-conversion.cts)
|
|
1663
|
+
Read: 'view_file',
|
|
1645
1664
|
Write: 'write_file',
|
|
1646
|
-
Edit: '
|
|
1647
|
-
Bash: '
|
|
1665
|
+
Edit: 'replace_file_content',
|
|
1666
|
+
Bash: 'run_command',
|
|
1648
1667
|
Glob: 'glob',
|
|
1649
|
-
Grep: '
|
|
1668
|
+
Grep: 'grep_search',
|
|
1650
1669
|
WebSearch: 'google_web_search',
|
|
1651
1670
|
WebFetch: 'web_fetch',
|
|
1652
1671
|
TodoWrite: 'write_todos',
|
|
1653
|
-
}
|
|
1672
|
+
}
|
|
1654
1673
|
|
|
1655
1674
|
// Tool name mapping from Claude/GSD agents to Kimi CLI module paths.
|
|
1656
1675
|
// Kimi custom agent YAML requires fully-qualified module paths.
|
|
@@ -1700,24 +1719,24 @@ function convertToolName(claudeTool) {
|
|
|
1700
1719
|
}
|
|
1701
1720
|
|
|
1702
1721
|
/**
|
|
1703
|
-
* Convert a Claude Code tool name to
|
|
1704
|
-
* - Applies Claude→
|
|
1705
|
-
* - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in
|
|
1706
|
-
* - Filters out Task/Agent — agents are auto-registered as tools in
|
|
1707
|
-
* @returns {string|null}
|
|
1722
|
+
* Convert a Claude Code tool name to Antigravity format
|
|
1723
|
+
* - Applies Claude→Antigravity mapping (Read→read_file, Bash→run_shell_command, etc.)
|
|
1724
|
+
* - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in Antigravity
|
|
1725
|
+
* - Filters out Task/Agent — agents are auto-registered as tools in Antigravity
|
|
1726
|
+
* @returns {string|null} Antigravity tool name, or null if tool should be excluded
|
|
1708
1727
|
*/
|
|
1709
|
-
function
|
|
1728
|
+
function convertAntigravityToolName(claudeTool) {
|
|
1710
1729
|
// MCP tools: exclude — auto-discovered from mcpServers config at runtime
|
|
1711
1730
|
if (claudeTool.startsWith('mcp__')) {
|
|
1712
1731
|
return null;
|
|
1713
1732
|
}
|
|
1714
1733
|
// Task/Agent: exclude — agents are auto-registered as callable tools.
|
|
1715
|
-
// AskUserQuestion: exclude — Gemini
|
|
1716
|
-
// emitting it causes frontmatter validation errors (#3362).
|
|
1717
|
-
// Skill/SlashCommand: exclude — Gemini
|
|
1718
|
-
// the lowercase fallback would emit an invalid
|
|
1719
|
-
// that fails frontmatter validation
|
|
1720
|
-
// the entire agent load (#1394).
|
|
1734
|
+
// AskUserQuestion: exclude — Antigravity (Gemini tool dialect) does not expose
|
|
1735
|
+
// an ask_user tool; emitting it causes frontmatter validation errors (#3362).
|
|
1736
|
+
// Skill/SlashCommand: exclude — Antigravity (Gemini tool dialect) has no 'skill'
|
|
1737
|
+
// built-in tool; the lowercase fallback would emit an invalid
|
|
1738
|
+
// 'skill'/'slashcommand' name that fails frontmatter validation
|
|
1739
|
+
// (tools.N: Invalid tool name) and aborts the entire agent load (#1394).
|
|
1721
1740
|
if (
|
|
1722
1741
|
claudeTool === 'Task' ||
|
|
1723
1742
|
claudeTool === 'Agent' ||
|
|
@@ -1729,8 +1748,8 @@ function convertGeminiToolName(claudeTool) {
|
|
|
1729
1748
|
return null;
|
|
1730
1749
|
}
|
|
1731
1750
|
// Check for explicit mapping
|
|
1732
|
-
if (
|
|
1733
|
-
return
|
|
1751
|
+
if (claudeToAntigravityTools[claudeTool]) {
|
|
1752
|
+
return claudeToAntigravityTools[claudeTool];
|
|
1734
1753
|
}
|
|
1735
1754
|
// Default: lowercase
|
|
1736
1755
|
return claudeTool.toLowerCase();
|
|
@@ -2036,7 +2055,12 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
|
|
|
2036
2055
|
const names = cmdNames || readGsdCommandNames();
|
|
2037
2056
|
const normalizedBody = transformContentToHyphen(body, names);
|
|
2038
2057
|
|
|
2039
|
-
|
|
2058
|
+
// #4324: the description is the text the host's skill picker renders, so it
|
|
2059
|
+
// needs the same hyphen normalisation the body gets — otherwise a `/gsd:<cmd>`
|
|
2060
|
+
// mention in a command description ships the retired colon form to the user.
|
|
2061
|
+
const description = transformContentToHyphen(
|
|
2062
|
+
extractFrontmatterField(frontmatter, 'description') || '', names,
|
|
2063
|
+
);
|
|
2040
2064
|
const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
|
|
2041
2065
|
const agent = extractFrontmatterField(frontmatter, 'agent');
|
|
2042
2066
|
// #769: preserve context: from source command files so it is emitted into
|
|
@@ -2429,7 +2453,7 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) {
|
|
|
2429
2453
|
* @param {boolean} [isGlobal=false] - Whether this is a global install
|
|
2430
2454
|
*/
|
|
2431
2455
|
function convertClaudeToAntigravityContent(content, isGlobal = false) {
|
|
2432
|
-
let c = content;
|
|
2456
|
+
let c = filterRuntimeNotesForTarget(content, 'antigravity');
|
|
2433
2457
|
if (isGlobal) {
|
|
2434
2458
|
// #3738: global skills install under ~/.gemini/config/skills (the dir AGY
|
|
2435
2459
|
// scans for global discovery), so skills-path references must divert there
|
|
@@ -2499,12 +2523,16 @@ function convertClaudeAgentToAntigravityAgent(content, isGlobal = false) {
|
|
|
2499
2523
|
const color = extractFrontmatterField(frontmatter, 'color');
|
|
2500
2524
|
const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || '';
|
|
2501
2525
|
|
|
2502
|
-
// Map tools to
|
|
2526
|
+
// Map tools to Antigravity equivalents (reuse existing convertAntigravityToolName)
|
|
2503
2527
|
const claudeTools = toolsRaw.split(',').map(t => t.trim()).filter(Boolean);
|
|
2504
|
-
const mappedTools = claudeTools.map(t =>
|
|
2528
|
+
const mappedTools = claudeTools.map(t => convertAntigravityToolName(t)).filter(Boolean);
|
|
2505
2529
|
|
|
2506
2530
|
// #2876: quote description for the same reason as the skill variant.
|
|
2507
|
-
|
|
2531
|
+
// #4705: tools is a YAML SEQUENCE of native names (see the src twin).
|
|
2532
|
+
const toolsBlock = mappedTools.length > 0
|
|
2533
|
+
? `tools:\n${mappedTools.map((t) => `- ${t}`).join('\n')}\n`
|
|
2534
|
+
: 'tools: []\n';
|
|
2535
|
+
let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n${toolsBlock}`;
|
|
2508
2536
|
if (color) fm += `color: ${color}\n`;
|
|
2509
2537
|
fm += '---';
|
|
2510
2538
|
|
|
@@ -2565,7 +2593,7 @@ function convertSlashCommandsToCursorSkillMentions(content) {
|
|
|
2565
2593
|
}
|
|
2566
2594
|
|
|
2567
2595
|
function convertClaudeToCursorMarkdown(content) {
|
|
2568
|
-
let converted = convertSlashCommandsToCursorSkillMentions(content);
|
|
2596
|
+
let converted = convertSlashCommandsToCursorSkillMentions(filterRuntimeNotesForTarget(content, 'cursor'));
|
|
2569
2597
|
// Replace tool name references in body text
|
|
2570
2598
|
converted = converted.replace(/\bBash\(/g, 'Shell(');
|
|
2571
2599
|
converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
|
|
@@ -2686,7 +2714,7 @@ function convertSlashCommandsToTraeSkillMentions(content) {
|
|
|
2686
2714
|
}
|
|
2687
2715
|
|
|
2688
2716
|
function convertClaudeToTraeMarkdown(content) {
|
|
2689
|
-
let converted = convertSlashCommandsToTraeSkillMentions(content);
|
|
2717
|
+
let converted = convertSlashCommandsToTraeSkillMentions(filterRuntimeNotesForTarget(content, 'trae'));
|
|
2690
2718
|
converted = converted.replace(/\bBash\(/g, 'Shell(');
|
|
2691
2719
|
converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
|
|
2692
2720
|
// Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
|
|
@@ -2812,7 +2840,7 @@ function convertSlashCommandsToCodebuddySkillMentions(content) {
|
|
|
2812
2840
|
}
|
|
2813
2841
|
|
|
2814
2842
|
function convertClaudeToCodebuddyMarkdown(content) {
|
|
2815
|
-
let converted = convertSlashCommandsToCodebuddySkillMentions(content);
|
|
2843
|
+
let converted = convertSlashCommandsToCodebuddySkillMentions(filterRuntimeNotesForTarget(content, 'codebuddy'));
|
|
2816
2844
|
// CodeBuddy uses the same tool names as Claude Code (Bash, Edit, Read, Write, etc.)
|
|
2817
2845
|
// No tool name conversion needed
|
|
2818
2846
|
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
|
@@ -2904,7 +2932,7 @@ function convertClaudeAgentToCodebuddyAgent(content) {
|
|
|
2904
2932
|
// ── Cline converters ────────────────────────────────────────────────────────
|
|
2905
2933
|
|
|
2906
2934
|
function convertClaudeToCliineMarkdown(content) {
|
|
2907
|
-
let converted = content;
|
|
2935
|
+
let converted = filterRuntimeNotesForTarget(content, 'cline');
|
|
2908
2936
|
// Cline uses the same tool names as Claude Code — no tool name conversion needed
|
|
2909
2937
|
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.clinerules`');
|
|
2910
2938
|
converted = converted.replace(/\.\/CLAUDE\.md/g, '.clinerules');
|
|
@@ -2960,6 +2988,8 @@ function convertClaudeCommandToClineSkill(content, skillName, runtime = null, cm
|
|
|
2960
2988
|
let description = extractFrontmatterField(frontmatter, 'description');
|
|
2961
2989
|
if (!description) description = `Run GSD workflow ${skillName}.`;
|
|
2962
2990
|
description = toSingleLine(description);
|
|
2991
|
+
// #4324: same reason as the Claude skill converter above.
|
|
2992
|
+
description = transformContentToHyphen(description, names);
|
|
2963
2993
|
// Cline documented max is 1024 code points (not UTF-16 code units).
|
|
2964
2994
|
// Use Array.from to iterate by code point so that multibyte characters
|
|
2965
2995
|
// (e.g. emoji, astral-plane chars) are never split, which would produce
|
|
@@ -3827,7 +3857,7 @@ function rewriteBareGsdToolsCommandsForCodex(content) {
|
|
|
3827
3857
|
}
|
|
3828
3858
|
|
|
3829
3859
|
function convertClaudeToCodexMarkdown(content) {
|
|
3830
|
-
let converted = convertSlashCommandsToCodexSkillMentions(content);
|
|
3860
|
+
let converted = convertSlashCommandsToCodexSkillMentions(filterRuntimeNotesForTarget(content, 'codex'));
|
|
3831
3861
|
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
|
3832
3862
|
// Remove /clear references — Codex has no equivalent command
|
|
3833
3863
|
// Handle backtick-wrapped: `\/clear` then: → (removed)
|
|
@@ -3934,6 +3964,22 @@ Typed mapping (agent_type-capable schema only):
|
|
|
3934
3964
|
never fabricate a manual worktree protocol — route through the negotiated
|
|
3935
3965
|
isolation adapter, which still fails closed for hosts declaring \`none\` (#3360).
|
|
3936
3966
|
|
|
3967
|
+
Foreground handoffs:
|
|
3968
|
+
- spawn_agent is asynchronous. When the source Agent(...) or Task(...) declares
|
|
3969
|
+
run_in_background=false, call collaboration.wait_agent(timeout_ms=...) immediately after
|
|
3970
|
+
spawn and keep the parent turn active until that child returns a terminal result.
|
|
3971
|
+
- collaboration.wait_agent is a mailbox wakeup, NOT a completion oracle: "Wait completed"
|
|
3972
|
+
can mean only that a child sent an interim MESSAGE or status update. After every wakeup,
|
|
3973
|
+
inspect the named child's update/status. Only a FINAL_ANSWER or a terminal agent status
|
|
3974
|
+
(completed, failed, or cancelled) ends the foreground handoff.
|
|
3975
|
+
- On an interim MESSAGE or any non-terminal status, do not report an outcome, send a
|
|
3976
|
+
continuation, start parent work, or end the parent turn. Call collaboration.wait_agent
|
|
3977
|
+
again for the same child. If a terminal response is absent after an abnormal end, reconcile
|
|
3978
|
+
the workflow's durable artifacts before classifying the child.
|
|
3979
|
+
- This applies to one foreground child as well as fan-out. The child retains its workflow's
|
|
3980
|
+
own checkpoint loop; do not report an outcome or start any further parent work before its
|
|
3981
|
+
terminal result is available.
|
|
3982
|
+
|
|
3937
3983
|
Generic-agent workaround (multi_agent_v1 schema — NO agent_type field):
|
|
3938
3984
|
When only the generic \`multi_agent_v1\` schema is available, typed GSD agent dispatch
|
|
3939
3985
|
(\`gsd-planner\`, \`gsd-executor\`, etc.) is NOT possible. This is a known Codex limitation
|
|
@@ -3961,6 +4007,9 @@ Spawn restriction:
|
|
|
3961
4007
|
defaulting to inline execution.
|
|
3962
4008
|
|
|
3963
4009
|
Parallel fan-out:
|
|
4010
|
+
- For each child, loop on collaboration.wait_agent(timeout_ms=...) until its own terminal
|
|
4011
|
+
result is observed. A mailbox update from one child never completes another child, and an
|
|
4012
|
+
interim MESSAGE never completes its sender.
|
|
3964
4013
|
- Spawn multiple agents → collect agent IDs → \`collaboration.wait_agent(timeout_ms=...)\` for each to complete
|
|
3965
4014
|
- Do NOT use \`functions.wait(cell_id=...)\` — that is an unrelated exec-cell tool, not the collaboration wait
|
|
3966
4015
|
|
|
@@ -7208,7 +7257,7 @@ function neutralizeAgentReferences(content, instructionFile) {
|
|
|
7208
7257
|
|
|
7209
7258
|
function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOverride = null } = {}) {
|
|
7210
7259
|
// Replace tool name references in content (applies to all files)
|
|
7211
|
-
let convertedContent = content;
|
|
7260
|
+
let convertedContent = filterRuntimeNotesForTarget(content, 'opencode');
|
|
7212
7261
|
convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
|
|
7213
7262
|
convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
|
|
7214
7263
|
convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
|
|
@@ -7370,7 +7419,7 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
|
|
|
7370
7419
|
// tests/runtime-converters.test.cjs (#2093).
|
|
7371
7420
|
function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverride = null } = {}) {
|
|
7372
7421
|
// Replace tool name references in content (applies to all files)
|
|
7373
|
-
let convertedContent = content;
|
|
7422
|
+
let convertedContent = filterRuntimeNotesForTarget(content, 'kilo');
|
|
7374
7423
|
convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
|
|
7375
7424
|
convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
|
|
7376
7425
|
convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
|
|
@@ -7924,45 +7973,66 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7924
7973
|
content = composeWorkflow(content, { sourcePath: srcPath });
|
|
7925
7974
|
}
|
|
7926
7975
|
|
|
7976
|
+
content = filterRuntimeNotesForTarget(content, runtime);
|
|
7977
|
+
|
|
7927
7978
|
if (!dispatch.mdSkipGenericRewrite) {
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
//
|
|
7935
|
-
//
|
|
7936
|
-
// (
|
|
7937
|
-
//
|
|
7938
|
-
//
|
|
7939
|
-
//
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
content =
|
|
7965
|
-
|
|
7979
|
+
// #4377: with a project-relative prefix, mask `${VAR:-default}` shell
|
|
7980
|
+
// defaults out of the substitutions below and restore them after. The
|
|
7981
|
+
// runtime launcher snippet probes gsd-tools through a chain of those
|
|
7982
|
+
// (`${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/...`, one per
|
|
7983
|
+
// runtime); they are shell word expansions, not markdown @ includes,
|
|
7984
|
+
// and a relative value there resolves against the shell's cwd instead
|
|
7985
|
+
// of the project. Swapping an include that points at the wrong
|
|
7986
|
+
// checkout for a path that points at nothing is not a fix, and the
|
|
7987
|
+
// launcher already probes `$(git rev-parse --show-toplevel)/.claude`
|
|
7988
|
+
// first, so the multi-worktree case is handled before these defaults
|
|
7989
|
+
// are ever reached. The shared helper is the single owner of the
|
|
7990
|
+
// balanced masking grammar used by this path and the rewrite engine.
|
|
7991
|
+
const rewriteGenericPaths = (body) => {
|
|
7992
|
+
content = body;
|
|
7993
|
+
const globalClaudeRegex = /~\/\.claude\//g;
|
|
7994
|
+
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
7995
|
+
const localClaudeRegex = /\.\/\.claude\//g;
|
|
7996
|
+
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
7997
|
+
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
7998
|
+
content = content.replace(localClaudeRegex, `./${dirName}/`);
|
|
7999
|
+
// #3544 review (Finding 1 fallout): guarded with the SAME
|
|
8000
|
+
// negative-lookahead convention already used at ~:2859-2860 below
|
|
8001
|
+
// ("preserve .claude-plugin and .claudeignore"). A naive `\b` here
|
|
8002
|
+
// is satisfied by ANY non-word character, including '-' — so for a
|
|
8003
|
+
// --config-dir whose name EXTENDS '.claude' (e.g. '.claude-work',
|
|
8004
|
+
// pathPrefix '$HOME/.claude-work/'), this pass re-matched the
|
|
8005
|
+
// '$HOME/.claude' PREFIX of its own slash-form output (lines above)
|
|
8006
|
+
// and re-appended the full prefix, corrupting every emitted path to
|
|
8007
|
+
// '$HOME/.claude-work-work/...'. Harmless no-op for the literal
|
|
8008
|
+
// default '.claude' (self-replace with an identical string), which
|
|
8009
|
+
// is why this went undetected until a non-default config-dir name
|
|
8010
|
+
// was exercised.
|
|
8011
|
+
content = content.replace(/~\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
|
|
8012
|
+
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
|
|
8013
|
+
content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
|
|
8014
|
+
content = content.replace(/~\/\.qwen\//g, pathPrefix);
|
|
8015
|
+
content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
|
|
8016
|
+
content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
|
|
8017
|
+
content = content.replace(/~\/\.hermes\//g, pathPrefix);
|
|
8018
|
+
content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
|
|
8019
|
+
content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
|
|
8020
|
+
// #3544: restore @-file-reference lines to the tilde form Claude Code
|
|
8021
|
+
// actually expands — the SAME correction #3133 already applies to
|
|
8022
|
+
// skill/command bodies via _applyRuntimeRewrites's 'claude' case (see
|
|
8023
|
+
// restoreClaudeGlobalAtRefTilde's doc comment in
|
|
8024
|
+
// runtime-artifact-conversion.cts). This is the gsd-core/ spec-tree
|
|
8025
|
+
// emit path, which never had it: every @~/.claude/gsd-core/… include
|
|
8026
|
+
// in a global install's workflows/references tree silently resolved
|
|
8027
|
+
// to nothing (54 includes across 22 files on a live install).
|
|
8028
|
+
if (runtime === 'claude') {
|
|
8029
|
+
content = runtimeArtifactConversion._restoreClaudeGlobalAtRefTilde(content, pathPrefix);
|
|
8030
|
+
}
|
|
8031
|
+
return content;
|
|
8032
|
+
};
|
|
8033
|
+
content = runtimeArtifactConversion._isRelativePathPrefix(pathPrefix)
|
|
8034
|
+
? runtimeArtifactConversion._withShellDefaultsPreserved(content, rewriteGenericPaths)
|
|
8035
|
+
: rewriteGenericPaths(content);
|
|
7966
8036
|
}
|
|
7967
8037
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
7968
8038
|
|
|
@@ -8478,6 +8548,16 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8478
8548
|
console.log(` ${green}✓${reset} Removed managed Codex ${eventName} hook from hooks.json`);
|
|
8479
8549
|
}
|
|
8480
8550
|
}
|
|
8551
|
+
// #2586: uninstall's own symmetric half of the orphaned-script cleanup —
|
|
8552
|
+
// same GSD-owned + unreferenced gate as the install-time call.
|
|
8553
|
+
const uninstallMonitorCleanup = hooksSurface.cleanupOrphanedCodexContextMonitorScript(targetDir);
|
|
8554
|
+
for (const deletedPath of uninstallMonitorCleanup.deleted) {
|
|
8555
|
+
removedCount++;
|
|
8556
|
+
console.log(` ${green}✓${reset} Removed orphaned Codex hook script (${path.basename(deletedPath)})`);
|
|
8557
|
+
}
|
|
8558
|
+
for (const warning of uninstallMonitorCleanup.warnings) {
|
|
8559
|
+
console.warn(` ${yellow}⚠${reset} Could not remove orphaned Codex hook script ${warning.path}: ${warning.reason}`);
|
|
8560
|
+
}
|
|
8481
8561
|
}
|
|
8482
8562
|
|
|
8483
8563
|
// 1a-kimi. Non-layout Kimi side-effect (#2095 EoS/kimi Upgrade 1): kimi's
|
|
@@ -9891,6 +9971,11 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9891
9971
|
// it from the directory it happened to be found in (#2872).
|
|
9892
9972
|
runtime,
|
|
9893
9973
|
scope: resolvedScope,
|
|
9974
|
+
// #4377: a surface re-apply is a separate process and cannot rely on the
|
|
9975
|
+
// installer's environment. Persist only a safe project-relative prefix.
|
|
9976
|
+
relativeIncludePrefix: resolvedScope === 'local' && hasRelativeIncludes
|
|
9977
|
+
? runtimeArtifactConversion._projectRelativePrefixFromProjectRoot(process.cwd(), configDir)
|
|
9978
|
+
: undefined,
|
|
9894
9979
|
files: {},
|
|
9895
9980
|
};
|
|
9896
9981
|
|
|
@@ -10153,6 +10238,86 @@ function populatePristineDir({ packageSrc, pristineDir, modified, runtime, pathP
|
|
|
10153
10238
|
return written;
|
|
10154
10239
|
}
|
|
10155
10240
|
|
|
10241
|
+
/**
|
|
10242
|
+
* #4145: recover a pristine baseline from a hash-matching orphan stored at an
|
|
10243
|
+
* unexpected path under gsd-pristine/ (e.g. without the gsd-core/ prefix an
|
|
10244
|
+
* earlier release's writer dropped).
|
|
10245
|
+
*
|
|
10246
|
+
* The preserve-check's strict join (pristineDir + manifest-keyed relPath)
|
|
10247
|
+
* misses such snapshots, so they were pushed into regeneration from the
|
|
10248
|
+
* incoming release — and when the file changed upstream, the candidate's hash
|
|
10249
|
+
* could never satisfy the recorded outgoing hash, leaving the correct
|
|
10250
|
+
* baseline permanently unconsumed and unpruned (the self-perpetuating state
|
|
10251
|
+
* #4145 reports). Hash equality with pristine_hashes is the same authority
|
|
10252
|
+
* the #3657 drift guard trusts, so an exact match cannot be the wrong
|
|
10253
|
+
* baseline no matter where under gsd-pristine/ it lives.
|
|
10254
|
+
*
|
|
10255
|
+
* Recovery = relocation: copy the orphan to the canonical manifest-keyed path
|
|
10256
|
+
* (hash-verified after the copy) and remove the orphan only once the
|
|
10257
|
+
* canonical copy is verified in place. Returns true when the canonical path
|
|
10258
|
+
* ended up holding recorded-hash bytes. Never deletes anything it cannot
|
|
10259
|
+
* vouch for by hash, and never consumes a path that is the canonical path of
|
|
10260
|
+
* ANY manifest file (see canonicalSkip below) — only genuine orphans, which
|
|
10261
|
+
* no strict-join reader ever consults, are eligible for removal.
|
|
10262
|
+
*/
|
|
10263
|
+
function recoverOrphanedPristine(pristineDir, relPath, recordedHash, canonicalSkip) {
|
|
10264
|
+
if (!recordedHash) return false;
|
|
10265
|
+
let orphanRel;
|
|
10266
|
+
try {
|
|
10267
|
+
// canonicalSkip = the normalized manifest keys: a file already sitting at
|
|
10268
|
+
// any canonical path can never be (re-)adopted through the scan. Without
|
|
10269
|
+
// this, two modified files sharing byte-identical outgoing content would
|
|
10270
|
+
// repeatedly "rescue" each other's canonical away (relocate + delete at
|
|
10271
|
+
// its home path) in alternating updates — bytes identical, state unstable.
|
|
10272
|
+
// It also keeps drift (#3657) / stale (#3407) territory with the caller.
|
|
10273
|
+
orphanRel = gsdFindPristineByHash(pristineDir, recordedHash, canonicalSkip);
|
|
10274
|
+
} catch {
|
|
10275
|
+
return false;
|
|
10276
|
+
}
|
|
10277
|
+
if (!orphanRel) return false;
|
|
10278
|
+
const outRef = resolveInstallRelativePath(pristineDir, relPath);
|
|
10279
|
+
if (!outRef) return false;
|
|
10280
|
+
try {
|
|
10281
|
+
fs.mkdirSync(path.dirname(outRef.fullPath), { recursive: true });
|
|
10282
|
+
fs.copyFileSync(path.join(pristineDir, orphanRel), outRef.fullPath);
|
|
10283
|
+
// Verify the relocated copy before removing the orphan — only a
|
|
10284
|
+
// hash-matching canonical counts as recovered.
|
|
10285
|
+
if (fileHash(outRef.fullPath) !== recordedHash) {
|
|
10286
|
+
try { fs.rmSync(outRef.fullPath, { force: true }); } catch { /* best-effort */ }
|
|
10287
|
+
return false;
|
|
10288
|
+
}
|
|
10289
|
+
// Orphan removal is best-effort: the canonical copy is already verified,
|
|
10290
|
+
// so a failed unlink leaves a harmless duplicate, never data loss.
|
|
10291
|
+
try { fs.rmSync(path.join(pristineDir, orphanRel), { force: true }); } catch { /* best-effort */ }
|
|
10292
|
+
return true;
|
|
10293
|
+
} catch {
|
|
10294
|
+
return false;
|
|
10295
|
+
}
|
|
10296
|
+
}
|
|
10297
|
+
|
|
10298
|
+
/**
|
|
10299
|
+
* #4135: honest N-of-M accounting for gsd-pristine/ baselines after an
|
|
10300
|
+
* update. The #3407 promotion rule keeps only hash-validated candidates
|
|
10301
|
+
* (byte-identical across the version span), so a multi-version update
|
|
10302
|
+
* legitimately ends with near-zero baselines — the collapse itself is NOT a
|
|
10303
|
+
* bug to hide; hiding it is. This renders the covered-of-total line the
|
|
10304
|
+
* update output prints either way, so "1 of 13" can never present like a
|
|
10305
|
+
* fully-covered run. Pure function (typed return) so tests lock the exact
|
|
10306
|
+
* contract without matching console prose.
|
|
10307
|
+
*/
|
|
10308
|
+
function describeBaselineCoverage(totalModified, covered) {
|
|
10309
|
+
const total = Math.max(0, totalModified);
|
|
10310
|
+
const have = Math.min(Math.max(0, covered), total);
|
|
10311
|
+
const uncovered = total - have;
|
|
10312
|
+
return {
|
|
10313
|
+
complete: uncovered === 0,
|
|
10314
|
+
uncovered,
|
|
10315
|
+
text: uncovered === 0
|
|
10316
|
+
? `gsd-pristine/ baselines cover ${have} of ${total} modified file(s)`
|
|
10317
|
+
: `gsd-pristine/ baselines cover ${have} of ${total} modified file(s) — ${uncovered} will be reported no_baseline by the reapply verifier`,
|
|
10318
|
+
};
|
|
10319
|
+
}
|
|
10320
|
+
|
|
10156
10321
|
/**
|
|
10157
10322
|
* Detect user-modified GSD files by comparing against install manifest.
|
|
10158
10323
|
* Backs up modified files to gsd-local-patches/ for reapply after update.
|
|
@@ -10299,6 +10464,15 @@ function saveLocalPatches(configDir, pristineCtx) {
|
|
|
10299
10464
|
const stalePaths = new Set();
|
|
10300
10465
|
// Track which relPaths were successfully regenerated (from either missing or stale).
|
|
10301
10466
|
const regeneratedPaths = new Set();
|
|
10467
|
+
// #4145: track which relPaths were recovered by relocating a hash-matching
|
|
10468
|
+
// orphan (stored at an unexpected path, e.g. without the gsd-core/ prefix).
|
|
10469
|
+
const rescuedPaths = new Set();
|
|
10470
|
+
// #4145: the set of paths that are SOME file's canonical pristine path
|
|
10471
|
+
// (every normalized manifest key). The orphan scan must never consume
|
|
10472
|
+
// these — see recoverOrphanedPristine.
|
|
10473
|
+
const canonicalSkip = new Set(
|
|
10474
|
+
Object.keys(manifest.files || {}).map((k) => normalizeInstallRelativePath(k)).filter(Boolean),
|
|
10475
|
+
);
|
|
10302
10476
|
const missingPaths = [];
|
|
10303
10477
|
for (const relPath of modified) {
|
|
10304
10478
|
const outRef = resolveInstallRelativePath(pristineDir, relPath);
|
|
@@ -10320,6 +10494,17 @@ function saveLocalPatches(configDir, pristineCtx) {
|
|
|
10320
10494
|
stalePaths.add(relPath);
|
|
10321
10495
|
}
|
|
10322
10496
|
}
|
|
10497
|
+
// #4145: canonical absent (or just removed as stale) — before falling
|
|
10498
|
+
// into regeneration, try to recover the baseline from a hash-matching
|
|
10499
|
+
// orphan elsewhere under gsd-pristine/ and relocate it to the canonical
|
|
10500
|
+
// path. This is the self-heal for snapshots an earlier release stored
|
|
10501
|
+
// without the gsd-core/ prefix: without it the state repeats forever
|
|
10502
|
+
// (regeneration candidates from the incoming release can never satisfy
|
|
10503
|
+
// the recorded outgoing hash when upstream changed the file).
|
|
10504
|
+
if (recoverOrphanedPristine(pristineDir, relPath, pristineHashes[relPath], canonicalSkip)) {
|
|
10505
|
+
rescuedPaths.add(relPath);
|
|
10506
|
+
continue;
|
|
10507
|
+
}
|
|
10323
10508
|
// File absent from gsd-pristine/ (or just removed above as stale):
|
|
10324
10509
|
// attempt hash-validated regeneration from new-release source.
|
|
10325
10510
|
missingPaths.push(relPath);
|
|
@@ -10362,19 +10547,38 @@ function saveLocalPatches(configDir, pristineCtx) {
|
|
|
10362
10547
|
}
|
|
10363
10548
|
// `regenerated` = total files successfully regenerated (from missing OR stale).
|
|
10364
10549
|
const regenerated = regeneratedPaths.size;
|
|
10550
|
+
// `rescued` = files recovered by relocating a hash-matching orphan to its
|
|
10551
|
+
// canonical path (#4145) — distinct from preservation (canonical already
|
|
10552
|
+
// correct) and regeneration (bytes re-derived from new-release source).
|
|
10553
|
+
const rescued = rescuedPaths.size;
|
|
10365
10554
|
// `removed` = stale entries that were deleted and NOT subsequently regenerated.
|
|
10366
10555
|
// Entries that were stale-deleted but then successfully regenerated are counted
|
|
10367
|
-
// only in `regenerated
|
|
10368
|
-
|
|
10556
|
+
// only in `regenerated`; stale-deleted-then-orphan-rescued entries are counted
|
|
10557
|
+
// only in `rescued` — the counts are non-overlapping.
|
|
10558
|
+
const removed = [...stalePaths].filter(p => !regeneratedPaths.has(p) && !rescuedPaths.has(p)).length;
|
|
10369
10559
|
if (preserved > 0) {
|
|
10370
10560
|
console.log(' ' + green + '✓' + reset + ' Preserved ' + cyan + 'gsd-pristine/' + reset + ' (' + preserved + ' file(s)) for three-way merge');
|
|
10371
10561
|
}
|
|
10562
|
+
if (rescued > 0) {
|
|
10563
|
+
console.log(' ' + green + '✓' + reset + ' Recovered ' + cyan + 'gsd-pristine/' + reset + ' (' + rescued + ' file(s)) by recorded hash from a legacy-path snapshot and relocated them (#4145)');
|
|
10564
|
+
}
|
|
10372
10565
|
if (regenerated > 0) {
|
|
10373
10566
|
console.log(' ' + green + '✓' + reset + ' Regenerated ' + cyan + 'gsd-pristine/' + reset + ' (' + regenerated + ' file(s)) via hash-validated new-release source');
|
|
10374
10567
|
}
|
|
10375
10568
|
if (removed > 0) {
|
|
10376
10569
|
console.log(' ' + yellow + 'i' + reset + ' Removed ' + removed + ' stale gsd-pristine/ snapshot(s); regenerated ' + regenerated + ' of those — falls back to over-broad verify heuristic for the rest');
|
|
10377
10570
|
}
|
|
10571
|
+
// #4135: the honest N-of-M coverage line. Preserved/rescued/regenerated
|
|
10572
|
+
// are disjoint buckets (see their accounting comments above), so their
|
|
10573
|
+
// sum is exactly the files that ended this update with a hash-valid
|
|
10574
|
+
// baseline. A partial result renders as an info line, not an error:
|
|
10575
|
+
// the collapse is legitimate (#3407), hiding it was the bug.
|
|
10576
|
+
const coverage = describeBaselineCoverage(modified.length, preserved + rescued + regenerated);
|
|
10577
|
+
if (coverage.complete) {
|
|
10578
|
+
console.log(' ' + green + '✓' + reset + ' ' + coverage.text);
|
|
10579
|
+
} else {
|
|
10580
|
+
console.log(' ' + yellow + 'i' + reset + ' ' + coverage.text);
|
|
10581
|
+
}
|
|
10378
10582
|
}
|
|
10379
10583
|
}
|
|
10380
10584
|
return modified;
|
|
@@ -10655,6 +10859,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10655
10859
|
isWindowsHost,
|
|
10656
10860
|
resolvedTarget,
|
|
10657
10861
|
homeDir,
|
|
10862
|
+
// #4377: the runtime's own localConfigDir. This is the prefix that reaches
|
|
10863
|
+
// copyWithPathReplacement, i.e. the one actually written into every
|
|
10864
|
+
// emitted command/skill/workflow body — the rewrite-engine seams below
|
|
10865
|
+
// handle re-applied surfaces, not the first install.
|
|
10866
|
+
localDirName: _hostBehaviors(runtime).localTargetIsProjectRoot === true ? undefined : getDirName(runtime),
|
|
10658
10867
|
});
|
|
10659
10868
|
|
|
10660
10869
|
// runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239
|
|
@@ -10665,6 +10874,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10665
10874
|
|
|
10666
10875
|
// Track installation failures
|
|
10667
10876
|
const failures = [];
|
|
10877
|
+
const configuredEntrypoints = [];
|
|
10668
10878
|
let installerMigrationResult = null;
|
|
10669
10879
|
const rollbackInstallerMigrations = () => {
|
|
10670
10880
|
if (!installerMigrationResult || typeof installerMigrationResult.rollback !== 'function') return;
|
|
@@ -10717,7 +10927,48 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10717
10927
|
// Map<filename, Buffer> — content snapshot of each pre-existing gsd-* agent file.
|
|
10718
10928
|
const codexPreInstallAgentContents = new Map();
|
|
10719
10929
|
let codexPreInstallVersionBytes = null;
|
|
10720
|
-
|
|
10930
|
+
// #4544 — manifest-driven snapshot state (captured in the block below):
|
|
10931
|
+
// codexPreInstallManagedFiles — Map<normalizedRelPath, Buffer|null>; one
|
|
10932
|
+
// entry per path the PRIOR install's gsd-file-manifest.json recorded.
|
|
10933
|
+
// null means the path did not exist pre-install, so rollback re-deletes
|
|
10934
|
+
// whatever this install put there instead of resurrecting it.
|
|
10935
|
+
// codexPreInstallManifestBytes — Buffer (or null) of the prior manifest file
|
|
10936
|
+
// itself, which a reinstall rewrites.
|
|
10937
|
+
// codexPreInstallHooksTree — Map<relPath, Buffer>, a full recursive
|
|
10938
|
+
// snapshot of <targetDir>/hooks/. The Codex manifest deliberately omits
|
|
10939
|
+
// hooks/ (the !isCodex gate on shared-hooks tracking), and hooks/ is
|
|
10940
|
+
// shared space, so the restore is wholesale: user files that predate
|
|
10941
|
+
// the install are in the snapshot and come back; anything the failed
|
|
10942
|
+
// install staged does not.
|
|
10943
|
+
const codexPreInstallManagedFiles = new Map();
|
|
10944
|
+
let codexPreInstallManifestBytes = null;
|
|
10945
|
+
const codexPreInstallHooksTree = new Map();
|
|
10946
|
+
// #4544 (review) — capture-state flags the restore must consult:
|
|
10947
|
+
// codexManagedSnapshotCaptured — the capture gate ran at all. When
|
|
10948
|
+
// false (non-Codex runtimes, minimal mode) NO pre-install state was
|
|
10949
|
+
// recorded, and the only safe restore is no restore: an empty
|
|
10950
|
+
// snapshot must never be read as "hooks/ was absent".
|
|
10951
|
+
// codexPreInstallHooksDirPreExisted — hooks/ existed as a DIRECTORY
|
|
10952
|
+
// pre-install. A pre-existing hooks FILE is left alone on rollback
|
|
10953
|
+
// rather than deleted.
|
|
10954
|
+
// codexPreInstallHooksCaptureIncomplete — some part of the hooks/ tree
|
|
10955
|
+
// could not be read (permissions, special files). The restore
|
|
10956
|
+
// downgrades to per-file so an uncapturable user file is never
|
|
10957
|
+
// destroyed by a wholesale delete whose snapshot lacked it.
|
|
10958
|
+
let codexManagedSnapshotCaptured = false;
|
|
10959
|
+
// null = the gate never ran; true/false = the gate ran and hooks/ (did|did
|
|
10960
|
+
// not) exist as a directory pre-install. Two states are load-bearing: a
|
|
10961
|
+
// clean first install records false, so its rollback removes the staged
|
|
10962
|
+
// hooks/ tree entirely; a non-Codex runtime records null, so rollback does
|
|
10963
|
+
// nothing.
|
|
10964
|
+
let codexPreInstallHooksDirPreExisted = null;
|
|
10965
|
+
let codexPreInstallHooksCaptureIncomplete = false;
|
|
10966
|
+
// #4249 CR: not gated on install mode. restoreCodexSnapshot is reachable for
|
|
10967
|
+
// a core/--minimal install too (#2695), and its pass-2 sweeps remove every
|
|
10968
|
+
// gsd-* skill dir / agent file the snapshot does not claim — so an empty
|
|
10969
|
+
// minimal-mode snapshot deleted the whole surface with nothing to restore.
|
|
10970
|
+
if (_hostBehaviors(runtime).tomlConfigInstall) {
|
|
10971
|
+
codexManagedSnapshotCaptured = true;
|
|
10721
10972
|
const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
|
|
10722
10973
|
if (fs.existsSync(_preSkillsDir)) {
|
|
10723
10974
|
for (const entry of fs.readdirSync(_preSkillsDir, { withFileTypes: true })) {
|
|
@@ -10759,18 +11010,212 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10759
11010
|
if (fs.existsSync(_preVersionPath)) {
|
|
10760
11011
|
try { codexPreInstallVersionBytes = fs.readFileSync(_preVersionPath); } catch (_) { /* best-effort */ }
|
|
10761
11012
|
}
|
|
10762
|
-
|
|
11013
|
+
// #4544 — capture the manifest-driven surfaces, same best-effort
|
|
11014
|
+
// conventions as the skills/ snapshot above. readInstallManifest is the
|
|
11015
|
+
// same hardened reader installer-migrations uses (array/ garbage shapes
|
|
11016
|
+
// degrade to an empty file set — rollback then simply covers less, never
|
|
11017
|
+
// crashes), and resolveInstallRelativePath keeps a hostile manifest key
|
|
11018
|
+
// from turning into a write outside the install root.
|
|
11019
|
+
const _priorManifest = readInstallManifest(targetDir);
|
|
11020
|
+
for (const rel of Object.keys(_priorManifest.files)) {
|
|
11021
|
+
const resolved = resolveInstallRelativePath(targetDir, rel);
|
|
11022
|
+
if (!resolved) continue;
|
|
11023
|
+
try {
|
|
11024
|
+
codexPreInstallManagedFiles.set(resolved.relPath, fs.readFileSync(resolved.fullPath));
|
|
11025
|
+
} catch (_) {
|
|
11026
|
+
// Listed but absent/unreadable pre-install: snapshot absence, so
|
|
11027
|
+
// rollback re-deletes instead of resurrecting.
|
|
11028
|
+
codexPreInstallManagedFiles.set(resolved.relPath, null);
|
|
11029
|
+
}
|
|
11030
|
+
}
|
|
11031
|
+
const _preManifestPath = path.join(targetDir, MANIFEST_NAME);
|
|
11032
|
+
if (fs.existsSync(_preManifestPath)) {
|
|
11033
|
+
try { codexPreInstallManifestBytes = fs.readFileSync(_preManifestPath); } catch (_) { /* best-effort */ }
|
|
11034
|
+
}
|
|
11035
|
+
// #4544 (review) — a clean FIRST install has no prior manifest, so nothing
|
|
11036
|
+
// above records the payload this install is about to write, and a failed
|
|
11037
|
+
// clean install would roll back to a half-written tree. Enumerate the SAME
|
|
11038
|
+
// source directories the installer copies (a directory walk tracks the
|
|
11039
|
+
// source tree automatically — no second file list to keep in parity) and
|
|
11040
|
+
// record every path as absent-pre-install. On a reinstall most of these
|
|
11041
|
+
// already carry entries from the prior manifest; any that do not (files
|
|
11042
|
+
// new in this version) snapshot their pre-install bytes or absence exactly
|
|
11043
|
+
// like the rest, which also closes the new-version-file residual.
|
|
11044
|
+
const _recordWritePlanTree = (srcDir, relPrefix) => {
|
|
11045
|
+
let children;
|
|
11046
|
+
try { children = fs.readdirSync(srcDir, { withFileTypes: true }); } catch (_) { return; }
|
|
11047
|
+
for (const child of children) {
|
|
11048
|
+
const rel = relPrefix ? `${relPrefix}/${child.name}` : child.name;
|
|
11049
|
+
if (child.isDirectory()) {
|
|
11050
|
+
_recordWritePlanTree(path.join(srcDir, child.name), rel);
|
|
11051
|
+
} else if (child.isFile()) {
|
|
11052
|
+
if (codexPreInstallManagedFiles.has(rel)) continue;
|
|
11053
|
+
// USER_OWNED_ARTIFACTS are manifest-relative to gsd-core/ (#2771):
|
|
11054
|
+
// they are durably staged across reinstalls and must never enter a
|
|
11055
|
+
// rollback delete-set.
|
|
11056
|
+
const manifestRel = rel.startsWith('gsd-core/') ? rel.slice('gsd-core/'.length) : rel;
|
|
11057
|
+
if (USER_OWNED_ARTIFACTS.includes(manifestRel)) continue;
|
|
11058
|
+
const resolved = resolveInstallRelativePath(targetDir, rel);
|
|
11059
|
+
if (!resolved) continue;
|
|
11060
|
+
try {
|
|
11061
|
+
codexPreInstallManagedFiles.set(rel, fs.existsSync(resolved.fullPath) ? fs.readFileSync(resolved.fullPath) : null);
|
|
11062
|
+
} catch (_) {
|
|
11063
|
+
codexPreInstallManagedFiles.set(rel, null);
|
|
11064
|
+
}
|
|
11065
|
+
}
|
|
11066
|
+
}
|
|
11067
|
+
};
|
|
11068
|
+
_recordWritePlanTree(path.join(src, 'gsd-core'), 'gsd-core');
|
|
11069
|
+
_recordWritePlanTree(path.join(src, 'scripts', 'changeset'), 'scripts/changeset');
|
|
11070
|
+
_recordWritePlanTree(path.join(src, 'scripts', 'lib'), 'scripts/lib');
|
|
11071
|
+
// gsd-core/CHANGELOG.md is sourced from the repo root (not src/gsd-core)
|
|
11072
|
+
// and gsd-core/.gsd-runtime is generated at install time — neither appears
|
|
11073
|
+
// in the directory walks, so record them explicitly.
|
|
11074
|
+
for (const standalone of ['gsd-core/CHANGELOG.md', 'gsd-core/.gsd-runtime', 'scripts/fix-slash-commands.cjs', 'scripts/gen-capability-registry.cjs', 'scripts/gen-loop-host-contract.cjs']) {
|
|
11075
|
+
if (codexPreInstallManagedFiles.has(standalone)) continue;
|
|
11076
|
+
const resolved = resolveInstallRelativePath(targetDir, standalone);
|
|
11077
|
+
if (!resolved) continue;
|
|
11078
|
+
try {
|
|
11079
|
+
codexPreInstallManagedFiles.set(standalone, fs.existsSync(resolved.fullPath) ? fs.readFileSync(resolved.fullPath) : null);
|
|
11080
|
+
} catch (_) {
|
|
11081
|
+
codexPreInstallManagedFiles.set(standalone, null);
|
|
11082
|
+
}
|
|
11083
|
+
}
|
|
11084
|
+
// hooks/ — full recursive snapshot, but never blind: lstat every entry so
|
|
11085
|
+
// a symlink under hooks/ is neither followed (a link to a FIFO would hang
|
|
11086
|
+
// the installer, /dev/zero would exhaust memory, and a link to private
|
|
11087
|
+
// data would copy that data into the snapshot — hooks/ is user-writable
|
|
11088
|
+
// shared space and, for local installs, repo-controllable) nor restored
|
|
11089
|
+
// as a link. Anything unreadable or special marks the capture INCOMPLETE
|
|
11090
|
+
// so the restore downgrades to per-file instead of wholesale-deleting a
|
|
11091
|
+
// tree it never fully saw. A pre-existing hooks FILE (not directory) is
|
|
11092
|
+
// recorded as such and left alone on rollback.
|
|
11093
|
+
const _preHooksPath = path.join(targetDir, 'hooks');
|
|
11094
|
+
let _preHooksStat = null;
|
|
11095
|
+
try { _preHooksStat = fs.lstatSync(_preHooksPath); } catch (_) { /* absent */ }
|
|
11096
|
+
codexPreInstallHooksDirPreExisted = Boolean(_preHooksStat && _preHooksStat.isDirectory());
|
|
11097
|
+
if (codexPreInstallHooksDirPreExisted) {
|
|
11098
|
+
const _snapshotHooksDir = (dir, relBase) => {
|
|
11099
|
+
let children;
|
|
11100
|
+
try { children = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) {
|
|
11101
|
+
codexPreInstallHooksCaptureIncomplete = true;
|
|
11102
|
+
return;
|
|
11103
|
+
}
|
|
11104
|
+
for (const child of children) {
|
|
11105
|
+
const relPath = relBase ? `${relBase}/${child.name}` : child.name;
|
|
11106
|
+
const fullPath = path.join(dir, child.name);
|
|
11107
|
+
let st = null;
|
|
11108
|
+
try { st = fs.lstatSync(fullPath); } catch (_) {
|
|
11109
|
+
codexPreInstallHooksCaptureIncomplete = true;
|
|
11110
|
+
continue;
|
|
11111
|
+
}
|
|
11112
|
+
if (st.isDirectory()) {
|
|
11113
|
+
_snapshotHooksDir(fullPath, relPath);
|
|
11114
|
+
} else if (st.isFile()) {
|
|
11115
|
+
try { codexPreInstallHooksTree.set(relPath, fs.readFileSync(fullPath)); } catch (_) {
|
|
11116
|
+
codexPreInstallHooksCaptureIncomplete = true;
|
|
11117
|
+
}
|
|
11118
|
+
} else {
|
|
11119
|
+
codexPreInstallHooksCaptureIncomplete = true;
|
|
11120
|
+
}
|
|
11121
|
+
}
|
|
11122
|
+
};
|
|
11123
|
+
_snapshotHooksDir(_preHooksPath, '');
|
|
11124
|
+
}
|
|
11125
|
+
}
|
|
11126
|
+
|
|
11127
|
+
// #4544 — shared restore for the manifest-driven surfaces. Called by BOTH
|
|
11128
|
+
// rollback paths: _codexPreConfigRollback (CHANGELOG.md, scripts/, the
|
|
11129
|
+
// initial manifest write AND — via installer migrations' stale-hook removal
|
|
11130
|
+
// — hooks/ itself are all mutated BEFORE config.toml is touched, so the
|
|
11131
|
+
// early path must cover them) and the full restoreCodexSnapshot() below.
|
|
11132
|
+
// Best-effort throughout, matching the #3245 convention: restore failures
|
|
11133
|
+
// never mask the original install error.
|
|
11134
|
+
const restoreCodexManagedSnapshot = () => {
|
|
11135
|
+
// #4544 (review) — if the capture never ran (non-Codex runtimes, minimal
|
|
11136
|
+
// mode), no pre-install state was recorded. The only safe action is NONE:
|
|
11137
|
+
// an empty snapshot must never be read as "hooks/ was absent", or a
|
|
11138
|
+
// minimal-mode rollback would delete the user's entire hooks/ tree.
|
|
11139
|
+
if (!codexManagedSnapshotCaptured) return;
|
|
11140
|
+
// hooks/ — the pre-install tree is restored wholesale: a user file that
|
|
11141
|
+
// predated the install is IN the snapshot and comes back; anything the
|
|
11142
|
+
// failed install staged is not, and goes away with the tree. When the
|
|
11143
|
+
// capture was INCOMPLETE, wholesale deletion would permanently destroy a
|
|
11144
|
+
// file whose bytes were never captured, so the restore downgrades to
|
|
11145
|
+
// per-file: put back what was captured and remove only the names GSD
|
|
11146
|
+
// itself stages (the hoisted CODEX_HOOKS_TO_COPY set plus the CommonJS
|
|
11147
|
+
// marker). hooks/lib/ is left untouched in that mode — its contents are
|
|
11148
|
+
// transitive and cannot be enumerated safely without the capture.
|
|
11149
|
+
if (codexPreInstallHooksDirPreExisted !== null) {
|
|
11150
|
+
const _hooksRestoreDir = path.join(targetDir, 'hooks');
|
|
11151
|
+
if (!codexPreInstallHooksDirPreExisted) {
|
|
11152
|
+
// Clean first install: nothing pre-existed under hooks/, so nothing
|
|
11153
|
+
// the failed install staged may survive either.
|
|
11154
|
+
try { fs.rmSync(_hooksRestoreDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
|
|
11155
|
+
} else if (!codexPreInstallHooksCaptureIncomplete) {
|
|
11156
|
+
try { fs.rmSync(_hooksRestoreDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
|
|
11157
|
+
for (const [relPath, buf] of codexPreInstallHooksTree) {
|
|
11158
|
+
const destFile = path.join(_hooksRestoreDir, relPath);
|
|
11159
|
+
try {
|
|
11160
|
+
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
11161
|
+
fs.writeFileSync(destFile, buf);
|
|
11162
|
+
} catch (_) { /* best-effort */ }
|
|
11163
|
+
}
|
|
11164
|
+
} else {
|
|
11165
|
+
// GSD-owned names are removed FIRST: several of them are also
|
|
11166
|
+
// legitimate pre-install files the snapshot just restored, and a
|
|
11167
|
+
// removal pass after the restore would delete the restored bytes.
|
|
11168
|
+
for (const hookName of CODEX_HOOKS_TO_COPY) {
|
|
11169
|
+
try { fs.rmSync(path.join(_hooksRestoreDir, hookName), { force: true }); } catch (_) { /* best-effort */ }
|
|
11170
|
+
}
|
|
11171
|
+
try { fs.rmSync(path.join(_hooksRestoreDir, 'package.json'), { force: true }); } catch (_) { /* best-effort */ }
|
|
11172
|
+
for (const [relPath, buf] of codexPreInstallHooksTree) {
|
|
11173
|
+
const destFile = path.join(_hooksRestoreDir, relPath);
|
|
11174
|
+
try {
|
|
11175
|
+
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
11176
|
+
fs.writeFileSync(destFile, buf);
|
|
11177
|
+
} catch (_) { /* best-effort */ }
|
|
11178
|
+
}
|
|
11179
|
+
}
|
|
11180
|
+
}
|
|
11181
|
+
// Every GSD-owned path the prior manifest recorded (plus the clean-install
|
|
11182
|
+
// write plan): restore bytes, or re-delete a path that was absent
|
|
11183
|
+
// pre-install.
|
|
11184
|
+
for (const [relPath, buf] of codexPreInstallManagedFiles) {
|
|
11185
|
+
const resolved = resolveInstallRelativePath(targetDir, relPath);
|
|
11186
|
+
if (!resolved) continue;
|
|
11187
|
+
try {
|
|
11188
|
+
if (buf !== null) {
|
|
11189
|
+
fs.mkdirSync(path.dirname(resolved.fullPath), { recursive: true });
|
|
11190
|
+
fs.writeFileSync(resolved.fullPath, buf);
|
|
11191
|
+
} else if (fs.existsSync(resolved.fullPath)) {
|
|
11192
|
+
fs.rmSync(resolved.fullPath, { force: true });
|
|
11193
|
+
}
|
|
11194
|
+
} catch (_) { /* best-effort */ }
|
|
11195
|
+
}
|
|
11196
|
+
// The prior manifest file itself: reinstall rewrites it; rollback returns
|
|
11197
|
+
// the previous install's manifest (or removes it on a clean first install).
|
|
11198
|
+
const _manifestRestorePath = path.join(targetDir, MANIFEST_NAME);
|
|
11199
|
+
if (codexPreInstallManifestBytes !== null) {
|
|
11200
|
+
try { fs.writeFileSync(_manifestRestorePath, codexPreInstallManifestBytes); } catch (_) { /* best-effort */ }
|
|
11201
|
+
} else if (fs.existsSync(_manifestRestorePath)) {
|
|
11202
|
+
try { fs.unlinkSync(_manifestRestorePath); } catch (_) { /* best-effort */ }
|
|
11203
|
+
}
|
|
11204
|
+
};
|
|
10763
11205
|
|
|
10764
11206
|
// #3245 CR finding 2 — Rollback coverage extends to ALL post-snapshot operations,
|
|
10765
11207
|
// not just the Codex config/hook error paths. Any throw between snapshot capture and
|
|
10766
11208
|
// the Codex config block (skills copy, agents copy, VERSION write, manifest write, etc.)
|
|
10767
11209
|
// must also trigger rollback so the caller is never left in a partially-installed state.
|
|
10768
11210
|
//
|
|
10769
|
-
// _codexPreConfigRollback covers the
|
|
10770
|
-
// config.toml is touched: skills/, agents/, gsd-core/VERSION,
|
|
11211
|
+
// _codexPreConfigRollback covers the surfaces that can be mutated before
|
|
11212
|
+
// config.toml is touched: skills/, agents/, gsd-core/VERSION, the manifest-
|
|
11213
|
+
// driven surfaces (#4544 — CHANGELOG.md, scripts/, .gsd-runtime and the
|
|
11214
|
+
// manifest itself are all rewritten in this window), and orphaned
|
|
10771
11215
|
// atomic-write temp files. It is safe to call before any writes have happened.
|
|
10772
11216
|
// The full restoreCodexSnapshot() (defined inside the config block) additionally
|
|
10773
|
-
// handles config.toml, which
|
|
11217
|
+
// handles config.toml and the staged hooks/ tree, which are not yet touched
|
|
11218
|
+
// at this point in the pipeline.
|
|
10774
11219
|
const _codexPreConfigRollback = !_hostBehaviors(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
|
|
10775
11220
|
rollbackInstallerMigrations();
|
|
10776
11221
|
// skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
|
|
@@ -10831,6 +11276,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10831
11276
|
} else if (fs.existsSync(_earlyVersionPath)) {
|
|
10832
11277
|
try { fs.unlinkSync(_earlyVersionPath); } catch (_) { /* best-effort */ }
|
|
10833
11278
|
}
|
|
11279
|
+
// #4544 — manifest-driven surfaces (CHANGELOG.md, scripts/, the initial
|
|
11280
|
+
// manifest write, and — via installer migrations' stale-hook removal —
|
|
11281
|
+
// hooks/ itself are all mutated in this window). The shared restore is
|
|
11282
|
+
// also idempotent against an untouched tree.
|
|
11283
|
+
restoreCodexManagedSnapshot();
|
|
10834
11284
|
// Orphaned atomic-write temp files.
|
|
10835
11285
|
const _earlyTmpPattern = /\.tmp-\d+-\d+$/;
|
|
10836
11286
|
function _earlyCleanTmpFiles(dir) {
|
|
@@ -11691,7 +12141,14 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11691
12141
|
console.log(` ${green}✓${reset} Wrote ${sharedHooksDirName}/package.json (CommonJS mode)`);
|
|
11692
12142
|
break;
|
|
11693
12143
|
case 'preserved-foreign':
|
|
11694
|
-
|
|
12144
|
+
// #4759: the foreign file usually DOES declare "type": "commonjs" —
|
|
12145
|
+
// any hand-written or formatter-touched package.json does — and Node
|
|
12146
|
+
// then loads the staged .js hooks as CommonJS, so the old
|
|
12147
|
+
// unconditional "may not resolve" claim was usually false. The
|
|
12148
|
+
// sibling plugin path (src/install-engine.cts) words this same
|
|
12149
|
+
// outcome conditionally; match it and keep will-not-load conditional
|
|
12150
|
+
// on "type": "module", the only case where it is true.
|
|
12151
|
+
console.warn(` ${yellow}⚠${reset} Left existing ${sharedHooksDirName}/package.json untouched (not GSD's marker). If it declares "type": "module", the staged hooks will not load.`);
|
|
11695
12152
|
break;
|
|
11696
12153
|
case 'failed':
|
|
11697
12154
|
// Best-effort: a read-only or full config dir must not abort the
|
|
@@ -11923,6 +12380,42 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11923
12380
|
manifestFiles = null;
|
|
11924
12381
|
}
|
|
11925
12382
|
if (manifestFiles !== null) {
|
|
12383
|
+
// #4667: codex-installed artifacts must not keep `@~/.claude/gsd-core/…`
|
|
12384
|
+
// include references — the `@` form resolves into the CLAUDE install
|
|
12385
|
+
// (wrong copy on dual-runtime machines at divergent versions, nothing at
|
|
12386
|
+
// all on codex-only ones; #570 cause 2 residue). Every target ships in
|
|
12387
|
+
// the codex install, so rewriting the `@~/` include form to the codex
|
|
12388
|
+
// root is mechanical and correct. This runs after all .md emitters
|
|
12389
|
+
// (several bypass the per-runtime converters — that is how the leak
|
|
12390
|
+
// survived the per-emitter fixes; the agent .tomls are generated later
|
|
12391
|
+
// and prefix themselves), and before the scan below, which stays as the
|
|
12392
|
+
// verification backstop. The `_GSD_RUNTIME_ROOT`/`$PREFERRED_CONFIG_DIR`
|
|
12393
|
+
// fallback chains and prose `.claude` mentions carry no `@~/` prefix and
|
|
12394
|
+
// are deliberately untouched, as is CHANGELOG.md.
|
|
12395
|
+
if (runtime === 'codex') {
|
|
12396
|
+
for (const relPath of manifestFiles) {
|
|
12397
|
+
const fileName = path.basename(relPath);
|
|
12398
|
+
if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
|
|
12399
|
+
if (fileName === 'CHANGELOG.md') continue;
|
|
12400
|
+
const rewritePath = path.join(targetDir, relPath);
|
|
12401
|
+
let rewriteContent;
|
|
12402
|
+
try {
|
|
12403
|
+
rewriteContent = fs.readFileSync(rewritePath, 'utf8');
|
|
12404
|
+
} catch (rewriteErr) {
|
|
12405
|
+
continue; // inaccessible or missing — the scan below reports or skips it
|
|
12406
|
+
}
|
|
12407
|
+
const rewritten = rewriteContent
|
|
12408
|
+
.split('@~/.claude/gsd-core/').join('@~/.codex/gsd-core/')
|
|
12409
|
+
.split('@$HOME/.claude/gsd-core/').join('@$HOME/.codex/gsd-core/');
|
|
12410
|
+
if (rewritten !== rewriteContent) {
|
|
12411
|
+
try {
|
|
12412
|
+
fs.writeFileSync(rewritePath, rewritten);
|
|
12413
|
+
} catch (writeErr) {
|
|
12414
|
+
continue; // never fail the install over the rewrite; the scan still warns
|
|
12415
|
+
}
|
|
12416
|
+
}
|
|
12417
|
+
}
|
|
12418
|
+
}
|
|
11926
12419
|
for (const relPath of manifestFiles) {
|
|
11927
12420
|
const fileName = path.basename(relPath);
|
|
11928
12421
|
if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
|
|
@@ -12131,6 +12624,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12131
12624
|
try { fs.unlinkSync(_rollbackVersionPath); } catch (_) { /* best-effort */ }
|
|
12132
12625
|
}
|
|
12133
12626
|
|
|
12627
|
+
// 4b. #4544 — manifest-driven surfaces: the staged hooks/ tree, every
|
|
12628
|
+
// GSD-owned path the prior manifest recorded (scripts/, gsd-core/
|
|
12629
|
+
// payload), and the prior manifest file itself.
|
|
12630
|
+
restoreCodexManagedSnapshot();
|
|
12631
|
+
|
|
12134
12632
|
// 5. Orphaned atomic-write temp files (<file>.tmp-<pid>-<n>) in targetDir.
|
|
12135
12633
|
// These can accumulate if an atomic write fails mid-rename. Best-effort scan.
|
|
12136
12634
|
//
|
|
@@ -12197,12 +12695,15 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12197
12695
|
// stageTransitiveHookLibs call after the copy loop. The #3579 boundary is
|
|
12198
12696
|
// preserved: helpers no staged Codex hook requires (graphify tooling among
|
|
12199
12697
|
// them) are still not shipped.
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
|
|
12698
|
+
// #2586: gsd-context-monitor.js is deliberately NOT copied for Codex.
|
|
12699
|
+
// It reads the statusline bridge file (${TMPDIR}/claude-ctx-{session_id}.json)
|
|
12700
|
+
// written only by hooks/gsd-statusline.js, which Codex never installs — so
|
|
12701
|
+
// every registered event was a guaranteed silent no-op (readSentinel throws
|
|
12702
|
+
// ENOENT -> allow(undefined), every invocation, every event, no exceptions).
|
|
12703
|
+
// A pre-#2586 install's stale copy + hooks.json registrations are cleaned
|
|
12704
|
+
// up below (see the CODEX_EXTENDED_HOOK_EVENTS loop), not re-added here.
|
|
12705
|
+
// CODEX_HOOKS_TO_COPY itself lives at module scope (#4544) — the rollback's
|
|
12706
|
+
// incomplete-capture path must name the same set without a second literal.
|
|
12206
12707
|
const codexHooksSrc = path.join(src, 'hooks', 'dist');
|
|
12207
12708
|
if (fs.existsSync(codexHooksSrc)) {
|
|
12208
12709
|
const codexHooksDest = path.join(targetDir, 'hooks');
|
|
@@ -12396,6 +12897,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12396
12897
|
absoluteRunner: codexNodeRunner,
|
|
12397
12898
|
platform: process.platform,
|
|
12398
12899
|
});
|
|
12900
|
+
configuredEntrypoints.push(...(hookWrite.configuredEntrypoints || []));
|
|
12399
12901
|
if (hookWrite.wrote) {
|
|
12400
12902
|
console.log(` ${green}✓${reset} Configured Codex hooks (SessionStart via hooks.json)`);
|
|
12401
12903
|
} else {
|
|
@@ -12403,35 +12905,48 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12403
12905
|
}
|
|
12404
12906
|
}
|
|
12405
12907
|
|
|
12406
|
-
//
|
|
12407
|
-
//
|
|
12408
|
-
//
|
|
12409
|
-
//
|
|
12410
|
-
//
|
|
12411
|
-
//
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
//
|
|
12415
|
-
// context-monitor
|
|
12416
|
-
//
|
|
12417
|
-
//
|
|
12418
|
-
//
|
|
12419
|
-
//
|
|
12420
|
-
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
|
|
12424
|
-
|
|
12425
|
-
|
|
12426
|
-
|
|
12427
|
-
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
}
|
|
12908
|
+
// #2586: Codex's hook payload carries no context/token-usage field
|
|
12909
|
+
// (confirmed against codex-rs/hooks/src/schema.rs), so agent-facing
|
|
12910
|
+
// context warnings and GSD phase/lifecycle display cannot be
|
|
12911
|
+
// supported on this runtime — state that plainly during install
|
|
12912
|
+
// rather than silently omitting the capability. Matches
|
|
12913
|
+
// capabilities/codex/capability.json's hostBehaviors.unsupportedFeatures.
|
|
12914
|
+
console.log(` ${dim}↳${reset} Codex: agent-facing context warnings and GSD phase/lifecycle display are unsupported (Codex's hook payload has no context-usage metric)`);
|
|
12915
|
+
|
|
12916
|
+
// ── Codex extended hook events (#772, #2088) — REMOVED by #2586 ──────
|
|
12917
|
+
// gsd-context-monitor.js is no longer copied or registered for Codex
|
|
12918
|
+
// (see the CODEX_HOOKS_TO_COPY comment above): every one of these
|
|
12919
|
+
// events was a guaranteed silent no-op, since the metrics bridge file
|
|
12920
|
+
// it reads is only ever written by Claude's own statusline hook.
|
|
12921
|
+
// Every event in CODEX_EXTENDED_HOOK_EVENTS is unconditionally
|
|
12922
|
+
// reconciled here — not gated on the script existing — so a
|
|
12923
|
+
// pre-#2586 install's stale registrations (exact current shape, or a
|
|
12924
|
+
// recognized legacy shape via isManagedHookCommand's
|
|
12925
|
+
// includeLegacyAliases) are stripped on reinstall. Mirrors the
|
|
12926
|
+
// unconditional uninstall-time loop over the same constant. A
|
|
12927
|
+
// registration whose command does not match the managed shape (a
|
|
12928
|
+
// hand-customized entry) survives untouched — see
|
|
12929
|
+
// reconcileCodexHooksJsonEvent's isManagedHookCommand filter.
|
|
12930
|
+
for (const codexEvent of CODEX_EXTENDED_HOOK_EVENTS) {
|
|
12931
|
+
const eventCleanup = removeCodexHooksJsonEvent(targetDir, codexEvent);
|
|
12932
|
+
if (eventCleanup.changed) {
|
|
12933
|
+
console.log(` ${green}✓${reset} Removed stale Codex ${codexEvent} context-monitor hook from hooks.json`);
|
|
12432
12934
|
}
|
|
12433
|
-
}
|
|
12434
|
-
|
|
12935
|
+
}
|
|
12936
|
+
// Delete the orphaned script (+ Windows .cmd shim) left by a
|
|
12937
|
+
// pre-#2586 install, but ONLY once no surviving hooks.json
|
|
12938
|
+
// registration under any event still references it, and only when
|
|
12939
|
+
// the on-disk file is GSD's own (see design doc's Ownership check —
|
|
12940
|
+
// a content-signature check, not manifest membership, so this works
|
|
12941
|
+
// on the very first reinstall after upgrading, with no bootstrap
|
|
12942
|
+
// gap). A deletion failure never reverts the (already safe,
|
|
12943
|
+
// already-written) hooks.json cleanup above — must-have #8.
|
|
12944
|
+
const monitorCleanup = hooksSurface.cleanupOrphanedCodexContextMonitorScript(targetDir);
|
|
12945
|
+
for (const deletedPath of monitorCleanup.deleted) {
|
|
12946
|
+
console.log(` ${green}✓${reset} Removed orphaned Codex hook script (${path.basename(deletedPath)})`);
|
|
12947
|
+
}
|
|
12948
|
+
for (const warning of monitorCleanup.warnings) {
|
|
12949
|
+
console.warn(` ${yellow}⚠${reset} Could not remove orphaned Codex hook script ${warning.path}: ${warning.reason}`);
|
|
12435
12950
|
}
|
|
12436
12951
|
// ── end Codex extended hook events ────────────────────────────────────
|
|
12437
12952
|
}
|
|
@@ -12462,7 +12977,21 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12462
12977
|
}
|
|
12463
12978
|
|
|
12464
12979
|
persistActiveProfileMarker();
|
|
12465
|
-
|
|
12980
|
+
// #4249: expose restoreCodexSnapshot (#3245) as a SECOND, separately named
|
|
12981
|
+
// rollback rather than rebinding `rollbackInstallerMigrations` to it. A
|
|
12982
|
+
// configured-entrypoint validation failure discovered later (outside this
|
|
12983
|
+
// function, after Codex's own hooks.json/config.toml write already
|
|
12984
|
+
// succeeded) previously had only the installer-migrations closure to call,
|
|
12985
|
+
// leaving the just-written config.toml/hooks.json broken on disk despite
|
|
12986
|
+
// Codex already owning a full pre-install snapshot/restore for exactly this.
|
|
12987
|
+
//
|
|
12988
|
+
// Every runtime's `rollbackInstallerMigrations` therefore still means what
|
|
12989
|
+
// it says — the installer-migrations-only closure, which is what a
|
|
12990
|
+
// finalize-stage failure that is NOT an entrypoint-validation failure gets
|
|
12991
|
+
// (the Phase 4 contract). `rollbackPreInstallSnapshot` is Codex-only and is
|
|
12992
|
+
// chosen only for entrypoint-validation failures. See the selection in
|
|
12993
|
+
// installAllRuntimes' rollbackFinalizedInstallerMigrations.
|
|
12994
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints, rollbackInstallerMigrations, rollbackPreInstallSnapshot: restoreCodexSnapshot };
|
|
12466
12995
|
}
|
|
12467
12996
|
|
|
12468
12997
|
if (plan.installSurface === 'copilot-instructions') {
|
|
@@ -12492,7 +13021,18 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12492
13021
|
writeCopilotHookConfig(targetDir);
|
|
12493
13022
|
console.log(` ${green}✓${reset} Configured Copilot lifecycle hook (sessionStart)`);
|
|
12494
13023
|
persistActiveProfileMarker();
|
|
12495
|
-
|
|
13024
|
+
// #4249: `[]`, not omitted — every Copilot hook is an inline `printf`
|
|
13025
|
+
// one-liner (GSD_COPILOT_*_HOOK_BASH/PWSH in src/runtime-hooks-surface.cts),
|
|
13026
|
+
// so this runtime genuinely launches no GSD-managed script and has no
|
|
13027
|
+
// interpreter to resolve. Stated explicitly like every other branch rather
|
|
13028
|
+
// than leaning on installAllRuntimes' `|| []` defence.
|
|
13029
|
+
// #4249 (antigravity review): `rollbackInstallerMigrations` was missing here
|
|
13030
|
+
// — every other branch returns it. This PR's own aggregate entrypoint gate
|
|
13031
|
+
// is what makes the gap reachable: an unrelated runtime's invalid entrypoint
|
|
13032
|
+
// now triggers rollbackFinalizedInstallerMigrations for every result in the
|
|
13033
|
+
// batch, and a Copilot result with no rollback function silently skips
|
|
13034
|
+
// reverting Copilot's own installer migrations.
|
|
13035
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: [], rollbackInstallerMigrations };
|
|
12496
13036
|
}
|
|
12497
13037
|
|
|
12498
13038
|
if (plan.installSurface === 'cursor-hooks-json') {
|
|
@@ -12515,7 +13055,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12515
13055
|
// The re-run is retained for parity with the settings.json install path.
|
|
12516
13056
|
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
12517
13057
|
persistActiveProfileMarker();
|
|
12518
|
-
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
13058
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: cursorHookResult.configuredEntrypoints, rollbackInstallerMigrations };
|
|
12519
13059
|
}
|
|
12520
13060
|
|
|
12521
13061
|
if (plan.installSurface === 'profile-marker-only') {
|
|
@@ -12571,6 +13111,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12571
13111
|
const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
|
|
12572
13112
|
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
12573
13113
|
const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
|
|
13114
|
+
configuredEntrypoints.push(...kimiHooksResult.configuredEntrypoints);
|
|
12574
13115
|
if (kimiHooksResult.changed) {
|
|
12575
13116
|
console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
|
|
12576
13117
|
}
|
|
@@ -12627,6 +13168,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12627
13168
|
const windsurfHookResult = writeWindsurfHooksJson(targetDir, src, {
|
|
12628
13169
|
platform: process.platform,
|
|
12629
13170
|
});
|
|
13171
|
+
configuredEntrypoints.push(...windsurfHookResult.configuredEntrypoints);
|
|
12630
13172
|
if (windsurfHookResult.changed) {
|
|
12631
13173
|
console.log(` ${green}✓${reset} Configured Windsurf lifecycle hooks (pre_write_code, pre_run_command)`);
|
|
12632
13174
|
} else {
|
|
@@ -12642,19 +13184,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12642
13184
|
}
|
|
12643
13185
|
|
|
12644
13186
|
persistActiveProfileMarker();
|
|
12645
|
-
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
13187
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints, rollbackInstallerMigrations };
|
|
12646
13188
|
}
|
|
12647
13189
|
|
|
12648
13190
|
if (plan.installSurface === 'cline-rules') {
|
|
12649
13191
|
// Cline uses the `.clinerules/` directory form (issue #787): GSD rules live
|
|
12650
13192
|
// at .clinerules/gsd.md and a PreToolUse lifecycle hook at
|
|
12651
13193
|
// .clinerules/hooks/PreToolUse. Global installs also get ~/.agents/AGENTS.md.
|
|
12652
|
-
writeClineArtifacts(targetDir, isGlobal);
|
|
13194
|
+
const clineArtifacts = writeClineArtifacts(targetDir, isGlobal);
|
|
12653
13195
|
// Re-run the manifest pass: these artifacts are written *after* the earlier
|
|
12654
13196
|
// writeManifest() call, so a second pass is needed to hash-track them.
|
|
12655
13197
|
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
12656
13198
|
persistActiveProfileMarker();
|
|
12657
|
-
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
13199
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: clineArtifacts.configuredEntrypoints, rollbackInstallerMigrations };
|
|
12658
13200
|
}
|
|
12659
13201
|
|
|
12660
13202
|
// Configure statusline and hooks in settings.json (or settings.local.json for local Claude installs).
|
|
@@ -12779,8 +13321,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12779
13321
|
persistActiveProfileMarker();
|
|
12780
13322
|
// Callers index this result by `runtime` (installAllRuntimes' statusline
|
|
12781
13323
|
// lookup), so every early exit must return the full shape — a bare return
|
|
12782
|
-
// crashes the install rather than skipping one file.
|
|
12783
|
-
|
|
13324
|
+
// crashes the install rather than skipping one file. That includes
|
|
13325
|
+
// configuredEntrypoints/rollbackInstallerMigrations: rollbackFinalizedInstallerMigrations
|
|
13326
|
+
// reads result.rollbackInstallerMigrations unconditionally, and an omitted
|
|
13327
|
+
// field there silently skips this runtime's rollback on a finalize-stage failure.
|
|
13328
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: [], rollbackInstallerMigrations };
|
|
12784
13329
|
}
|
|
12785
13330
|
const settings = validateHookFields(cleanupOrphanedHooks(rawSettings));
|
|
12786
13331
|
// #3002 CR / #3662: rewrite legacy `node .../gsd-*.js` command strings (pre-
|
|
@@ -12802,7 +13347,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12802
13347
|
// runtime's hostBehaviors instead of a hardcoded `runtime === 'antigravity'`
|
|
12803
13348
|
// check inside projectLocalHookPrefix.
|
|
12804
13349
|
const localPrefix = projectLocalHookPrefix({ runtime, dirName, hookPathStyle: _hostBehaviors(runtime).hookPathStyle });
|
|
12805
|
-
const
|
|
13350
|
+
const settingsEntrypoints = [];
|
|
13351
|
+
const hookOpts = {
|
|
13352
|
+
portableHooks: hasPortableHooks,
|
|
13353
|
+
runtime,
|
|
13354
|
+
configPath: settingsPath,
|
|
13355
|
+
// #4249: track unconditionally. Gating on `plan.hooksSurface ===
|
|
13356
|
+
// 'settings-json'` made tracking depend on an unasserted
|
|
13357
|
+
// installSurface/hooksSurface coupling — a descriptor that broke it would
|
|
13358
|
+
// silently drop this runtime out of validation. Everything recorded here
|
|
13359
|
+
// lands in settings.json by construction, and the registered-command
|
|
13360
|
+
// filter below already discards entries no hook actually references.
|
|
13361
|
+
configuredEntrypoints: settingsEntrypoints,
|
|
13362
|
+
};
|
|
12806
13363
|
// #2979: local-install hook commands also use a runner GUI/minimal-PATH
|
|
12807
13364
|
// runtimes can resolve. Bare `node` fails when the host launches the
|
|
12808
13365
|
// runtime with a stripped PATH (Finder/Antigravity/etc) — #3662 replaces
|
|
@@ -12816,19 +13373,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12816
13373
|
// `node` command that recreates the #2979 failure.
|
|
12817
13374
|
const localCmd = (hookFile) => localNodeRunner === null
|
|
12818
13375
|
? null
|
|
12819
|
-
: projectShellCommandText({
|
|
13376
|
+
: hooksSurface.recordConfiguredHookCommand(projectShellCommandText({
|
|
12820
13377
|
runnerToken: localNodeRunner,
|
|
12821
13378
|
argTokens: [`${localPrefix}/hooks/${hookFile}`],
|
|
12822
13379
|
runtime,
|
|
12823
13380
|
platform: process.platform,
|
|
12824
|
-
});
|
|
12825
|
-
const localShellCmd = (hookFile) => buildLocalShellHookCommand({
|
|
13381
|
+
}), targetDir, hookFile, hookOpts);
|
|
13382
|
+
const localShellCmd = (hookFile) => hooksSurface.recordConfiguredHookCommand(buildLocalShellHookCommand({
|
|
12826
13383
|
localPrefix,
|
|
12827
13384
|
hookFile,
|
|
12828
13385
|
bashRunner: localBashRunner,
|
|
12829
13386
|
runtime,
|
|
12830
13387
|
platform: process.platform,
|
|
12831
|
-
});
|
|
13388
|
+
}), targetDir, hookFile, hookOpts);
|
|
12832
13389
|
const statuslineCommand = isGlobal
|
|
12833
13390
|
? buildHookCommand(targetDir, 'gsd-statusline.js', hookOpts)
|
|
12834
13391
|
: localCmd('gsd-statusline.js');
|
|
@@ -12898,6 +13455,31 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12898
13455
|
? buildHookCommand(targetDir, 'gsd-update-banner.js', hookOpts)
|
|
12899
13456
|
: localCmd('gsd-update-banner.js'));
|
|
12900
13457
|
|
|
13458
|
+
const registeredHookCommands = Object.values(settings.hooks || {})
|
|
13459
|
+
.flatMap(groups => Array.isArray(groups) ? groups : [])
|
|
13460
|
+
.flatMap(group => Array.isArray(group && group.hooks) ? group.hooks : [])
|
|
13461
|
+
.map(hook => hook && hook.command)
|
|
13462
|
+
.filter(command => typeof command === 'string');
|
|
13463
|
+
// #4249: match by the managed script's `/hooks/<basename>` path segment, not
|
|
13464
|
+
// by exact command-string equality. The blocking-guard hooks above register
|
|
13465
|
+
// only-if-absent, so a hook already present from a prior install keeps its
|
|
13466
|
+
// OLD command untouched — but `track()` always records the FRESHLY computed
|
|
13467
|
+
// command for it, which never equals what's actually persisted. Matching on
|
|
13468
|
+
// the segment (present in the persisted command either way, since every
|
|
13469
|
+
// entry.scriptPath is <configDir>/hooks/<name> by construction) keeps an
|
|
13470
|
+
// already-registered, still-active hook in the validated set instead of
|
|
13471
|
+
// silently dropping it (#4154 Blocker) — anchored on `/hooks/` rather than a
|
|
13472
|
+
// bare basename so an unrelated user command that merely mentions the same
|
|
13473
|
+
// filename can't false-positive into GSD's validated set.
|
|
13474
|
+
configuredEntrypoints.push(
|
|
13475
|
+
...settingsEntrypoints.filter(entry => {
|
|
13476
|
+
const hooksSegment = '/hooks/' + path.basename(entry.scriptPath);
|
|
13477
|
+
return registeredHookCommands.some(command => command.includes(hooksSegment));
|
|
13478
|
+
}),
|
|
13479
|
+
);
|
|
13480
|
+
const statuslineEntrypoints = settingsEntrypoints.filter(entry => entry.command === statuslineCommand);
|
|
13481
|
+
const updateBannerEntrypoints = settingsEntrypoints.filter(entry => entry.command === updateBannerCommand);
|
|
13482
|
+
|
|
12901
13483
|
// #683: Set worktree.baseRef:"head" in settings.local.json for local Claude installs.
|
|
12902
13484
|
// Both fresh and upgrade paths apply only when worktrees are enabled for the project.
|
|
12903
13485
|
// Never applies to global installs, non-Claude runtimes, or when the user already
|
|
@@ -12966,15 +13548,65 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12966
13548
|
settings,
|
|
12967
13549
|
statuslineCommand,
|
|
12968
13550
|
updateBannerCommand,
|
|
13551
|
+
statuslineEntrypoints,
|
|
13552
|
+
updateBannerEntrypoints,
|
|
12969
13553
|
runtime,
|
|
12970
13554
|
configDir: targetDir,
|
|
12971
13555
|
rollbackInstallerMigrations,
|
|
13556
|
+
configuredEntrypoints,
|
|
12972
13557
|
};
|
|
12973
13558
|
}
|
|
12974
13559
|
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
|
|
13560
|
+
// #4249 (review, Major): rollback consequence differs by runtime surface —
|
|
13561
|
+
// see docs/how-to/update-gsd.md's rollback-matrix paragraph, which this
|
|
13562
|
+
// mirrors. Codex reverts (pre-install snapshot restore); Cursor/Windsurf/
|
|
13563
|
+
// Kimi/Kimi Code/Cline already wrote their config file inside install(),
|
|
13564
|
+
// ahead of this gate, with no revert path, so it is left on disk broken;
|
|
13565
|
+
// every other (settings.json-based) runtime writes strictly after this gate,
|
|
13566
|
+
// so a failure here means nothing new was persisted for it.
|
|
13567
|
+
const ENTRYPOINT_LEFT_UNREVERTED_RUNTIMES = new Set(['cursor', 'windsurf', 'kimi', 'kimi-code', 'cline']);
|
|
13568
|
+
function describeEntrypointConsequence(invalidRuntime) {
|
|
13569
|
+
if (invalidRuntime === 'codex') return 'reverted: its pre-install snapshot was restored';
|
|
13570
|
+
if (ENTRYPOINT_LEFT_UNREVERTED_RUNTIMES.has(invalidRuntime)) return 'NOT reverted: its config file is already written and was left on disk — fix the reported path and rerun install';
|
|
13571
|
+
return 'not persisted: this runtime writes its config after this check';
|
|
13572
|
+
}
|
|
13573
|
+
|
|
13574
|
+
function assertConfiguredEntrypoints(entries) {
|
|
13575
|
+
// #4249: some writers push the same (configPath, scriptPath) pair more than
|
|
13576
|
+
// once (e.g. Kimi's context-monitor hook registered under several events,
|
|
13577
|
+
// or the portable resolver script shared by every portable JS hook) — keep
|
|
13578
|
+
// one so a broken entry is reported once, not once per duplicate.
|
|
13579
|
+
const seen = new Set();
|
|
13580
|
+
const deduped = (entries || []).filter((entry) => {
|
|
13581
|
+
const key = JSON.stringify([entry.configPath, entry.scriptPath]);
|
|
13582
|
+
if (seen.has(key)) return false;
|
|
13583
|
+
seen.add(key);
|
|
13584
|
+
return true;
|
|
13585
|
+
});
|
|
13586
|
+
const validation = hooksSurface.validateConfiguredEntrypoints(deduped);
|
|
13587
|
+
if (validation.ok) return;
|
|
13588
|
+
|
|
13589
|
+
const error = new Error(
|
|
13590
|
+
// #4249: lead each entry with its runtime, and name the actual consequence
|
|
13591
|
+
// for that runtime (review, Major) — the aggregate gate is all-or-nothing
|
|
13592
|
+
// across every runtime being installed, and a failure here can revert a
|
|
13593
|
+
// runtime whose own entrypoints were fine (see
|
|
13594
|
+
// rollbackFinalizedInstallerMigrations) while leaving another runtime's
|
|
13595
|
+
// already-written config broken on disk with no revert at all, so an
|
|
13596
|
+
// operator reading only this message must be able to tell WHOSE
|
|
13597
|
+
// entrypoint broke and WHAT that means for their config, not just that
|
|
13598
|
+
// something did.
|
|
13599
|
+
`Configured entrypoint validation failed: ${validation.invalid.map(({ runtime: invalidRuntime, role, path: invalidPath, reason }) => `${invalidRuntime} ${role} ${invalidPath} (${reason}) [${describeEntrypointConsequence(invalidRuntime)}]`).join(', ')}`,
|
|
13600
|
+
);
|
|
13601
|
+
error.configuredEntrypointValidation = validation;
|
|
13602
|
+
throw error;
|
|
13603
|
+
}
|
|
13604
|
+
|
|
13605
|
+
// #4249: `bannerOpts.configuredEntrypoints` is the ONLY source assertConfiguredEntrypoints
|
|
13606
|
+
// checks below — a caller that omits it (or calls finishInstall directly instead of
|
|
13607
|
+
// through installAllRuntimes) gets zero entrypoint validation, silently. installAllRuntimes
|
|
13608
|
+
// always passes the full set (per-runtime entries plus statusline/updateBanner); any other
|
|
13609
|
+
// caller must do the same for this gate to mean anything.
|
|
12978
13610
|
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = DEFAULT_RUNTIME, isGlobal = true, configDir = null, bannerOpts = {}) {
|
|
12979
13611
|
// #2093: isKilo dropped — the Kilo permissions-writer call below is gated
|
|
12980
13612
|
// on plan.finishPermissionWriter === 'kilo' (descriptor-driven), not this flag.
|
|
@@ -12988,6 +13620,19 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
12988
13620
|
const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
|
12989
13621
|
const plan = resolveInstallPlan(runtime);
|
|
12990
13622
|
|
|
13623
|
+
// #4249 Major: validate BEFORE this function's own settings.json write (and
|
|
13624
|
+
// before writeNonClaudeDefaults) instead of after. Cursor/Windsurf/Kimi/Cline
|
|
13625
|
+
// already persisted their config inside install() by this point, with no
|
|
13626
|
+
// rollback path covering those writes; Codex also persists inside install()
|
|
13627
|
+
// but its rollback binds to a full pre-install snapshot restore, so it IS
|
|
13628
|
+
// covered (see docs/how-to/update-gsd.md). For the settings-json surface
|
|
13629
|
+
// this ordering means a failing validation never reaches this function's
|
|
13630
|
+
// own write at all. On the production path this is a redundant backstop —
|
|
13631
|
+
// installAllRuntimes's own aggregate assertConfiguredEntrypoints call
|
|
13632
|
+
// already validates the superset before finishInstall runs for any
|
|
13633
|
+
// runtime — kept for a caller that invokes finishInstall directly.
|
|
13634
|
+
assertConfiguredEntrypoints(bannerOpts.configuredEntrypoints);
|
|
13635
|
+
|
|
12991
13636
|
if (shouldInstallStatusline && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
|
|
12992
13637
|
if (!isGlobal && !forceStatusline) {
|
|
12993
13638
|
// Local installs skip statusLine by default: repo settings.json takes precedence over
|
|
@@ -13885,10 +14530,32 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
13885
14530
|
|
|
13886
14531
|
const rollbackFinalizedInstallerMigrations = (error) => {
|
|
13887
14532
|
const rollbackFailures = [];
|
|
14533
|
+
// #4249: this discriminates on the error's KIND, never on which runtime
|
|
14534
|
+
// owns the failing entrypoint. `wide` is true for ANY entrypoint-validation
|
|
14535
|
+
// failure from ANY runtime, by design: the aggregate gate exists so a
|
|
14536
|
+
// multi-runtime install cannot report success while one of its entrypoints
|
|
14537
|
+
// is broken, so an invalid Cline entrypoint reverts Codex's pre-install
|
|
14538
|
+
// snapshot too — even though Codex itself was fine and its own "Done!"
|
|
14539
|
+
// summary already printed. tests/configured-entrypoint-validation.test.cjs
|
|
14540
|
+
// ('an aggregate entrypoint validation failure rolls the Codex install
|
|
14541
|
+
// back') exercises exactly that, and it is the all-or-nothing behaviour
|
|
14542
|
+
// docs/how-to/update-gsd.md documents.
|
|
14543
|
+
//
|
|
14544
|
+
// What this narrows is the OTHER axis: a finalize-stage exception that is
|
|
14545
|
+
// not an entrypoint-validation failure at all — e.g. a sibling runtime's
|
|
14546
|
+
// permission-config write dying with EACCES — gets only the
|
|
14547
|
+
// installer-migrations-only rollback that Phase 4 specifies
|
|
14548
|
+
// (docs/installer-migrations.md#phase-4-installupdate-integration).
|
|
14549
|
+
// Un-installing (and, on update, downgrading) an already-"Done!" Codex over
|
|
14550
|
+
// an unrelated error is not an outcome any doc promises, while the sibling
|
|
14551
|
+
// surfaces that write config inside install() would keep theirs regardless.
|
|
14552
|
+
const wide = !!(error && error.configuredEntrypointValidation);
|
|
13888
14553
|
for (const result of [...results].reverse()) {
|
|
13889
|
-
if (!result
|
|
14554
|
+
if (!result) continue;
|
|
14555
|
+
const rollback = (wide && result.rollbackPreInstallSnapshot) || result.rollbackInstallerMigrations;
|
|
14556
|
+
if (typeof rollback !== 'function') continue;
|
|
13890
14557
|
try {
|
|
13891
|
-
|
|
14558
|
+
rollback();
|
|
13892
14559
|
} catch (rollbackError) {
|
|
13893
14560
|
rollbackFailures.push({
|
|
13894
14561
|
runtime: result.runtime,
|
|
@@ -13916,6 +14583,19 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
13916
14583
|
|
|
13917
14584
|
const finalize = (shouldInstallStatusline, shouldInstallBanner) => {
|
|
13918
14585
|
try {
|
|
14586
|
+
const selectedConfiguredEntrypoints = (result) => {
|
|
14587
|
+
if (!result || result.skipped) return [];
|
|
14588
|
+
const useStatusline = statuslineRuntimes.includes(result.runtime)
|
|
14589
|
+
&& shouldInstallStatusline
|
|
14590
|
+
&& (isGlobal || forceStatusline);
|
|
14591
|
+
return [
|
|
14592
|
+
...(result.configuredEntrypoints || []),
|
|
14593
|
+
...(useStatusline ? (result.statuslineEntrypoints || []) : []),
|
|
14594
|
+
...(shouldInstallBanner ? (result.updateBannerEntrypoints || []) : []),
|
|
14595
|
+
];
|
|
14596
|
+
};
|
|
14597
|
+
assertConfiguredEntrypoints(results.flatMap(selectedConfiguredEntrypoints));
|
|
14598
|
+
|
|
13919
14599
|
const printSummaries = () => {
|
|
13920
14600
|
for (const result of results) {
|
|
13921
14601
|
if (result && result.skipped) continue;
|
|
@@ -13929,7 +14609,11 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
13929
14609
|
result.runtime,
|
|
13930
14610
|
isGlobal,
|
|
13931
14611
|
result.configDir,
|
|
13932
|
-
{
|
|
14612
|
+
{
|
|
14613
|
+
shouldInstallBanner: !!shouldInstallBanner,
|
|
14614
|
+
bannerCommand: result.updateBannerCommand,
|
|
14615
|
+
configuredEntrypoints: selectedConfiguredEntrypoints(result),
|
|
14616
|
+
}
|
|
13933
14617
|
);
|
|
13934
14618
|
}
|
|
13935
14619
|
};
|
|
@@ -14117,6 +14801,7 @@ module.exports = {
|
|
|
14117
14801
|
reportLocalPatches,
|
|
14118
14802
|
validateHookFields,
|
|
14119
14803
|
populatePristineDir,
|
|
14804
|
+
describeBaselineCoverage,
|
|
14120
14805
|
_resolveUserArtifactStagingRoot,
|
|
14121
14806
|
_tryResolveUserArtifactStagingRoot,
|
|
14122
14807
|
finishInstall,
|