@opengsd/gsd-core 1.11.0 → 1.13.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/.opencode/plugins/gsd-core.js +12 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +78 -42
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +0 -1
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +3 -1
- package/agents/gsd-plan-checker.md +91 -112
- package/agents/gsd-planner.md +20 -4
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +82 -7
- package/agents/gsd-ui-researcher.md +70 -3
- package/agents/gsd-verifier.md +24 -2
- package/bin/install.js +847 -200
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/quick.md +8 -4
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +761 -100
- package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
- package/gsd-core/bin/lib/api-coverage.cjs +30 -9
- package/gsd-core/bin/lib/artifacts.cjs +2 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
- package/gsd-core/bin/lib/audit.cjs +163 -41
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +785 -144
- package/gsd-core/bin/lib/capability-state.cjs +25 -4
- package/gsd-core/bin/lib/capability-validator.cjs +321 -18
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +229 -6
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +877 -54
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +121 -29
- package/gsd-core/bin/lib/config.cjs +92 -2
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +118 -14
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +23 -2
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +975 -326
- package/gsd-core/bin/lib/gap-checker.cjs +41 -8
- package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
- package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
- package/gsd-core/bin/lib/host-integration.cjs +96 -11
- package/gsd-core/bin/lib/init-command-router.cjs +132 -21
- package/gsd-core/bin/lib/init.cjs +252 -56
- package/gsd-core/bin/lib/install-engine.cjs +252 -15
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
- package/gsd-core/bin/lib/install-profiles.cjs +100 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +195 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +175 -4
- package/gsd-core/bin/lib/milestone.cjs +112 -7
- package/gsd-core/bin/lib/model-catalog.cjs +177 -19
- package/gsd-core/bin/lib/model-resolver.cjs +10 -28
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +321 -13
- package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
- package/gsd-core/bin/lib/phase-locator.cjs +138 -17
- package/gsd-core/bin/lib/phase.cjs +1175 -115
- package/gsd-core/bin/lib/plan-document.cjs +273 -0
- package/gsd-core/bin/lib/plan-scan.cjs +13 -2
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +165 -34
- package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
- package/gsd-core/bin/lib/probe-core.cjs +4 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
- package/gsd-core/bin/lib/roadmap.cjs +248 -64
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -31
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +47 -18
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +216 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
- package/gsd-core/bin/lib/state-transition.cjs +850 -145
- package/gsd-core/bin/lib/state.cjs +1629 -287
- package/gsd-core/bin/lib/surface.cjs +33 -10
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +2542 -387
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
- package/gsd-core/bin/lib/unusable-input.cjs +13 -0
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/vendor/README.md +43 -5
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -13
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +441 -56
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +6 -5
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +37 -19
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/edge-probe.md +17 -5
- package/gsd-core/references/execute-mvp-tdd.md +18 -18
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +3 -3
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +22 -0
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/mvp-concepts.md +2 -2
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +76 -3
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +39 -9
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +15 -12
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +2 -2
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +11 -11
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/phase-prompt.md +7 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +3 -1
- package/gsd-core/workflows/add-phase.md +5 -3
- package/gsd-core/workflows/add-tests.md +4 -9
- package/gsd-core/workflows/add-todo.md +2 -2
- package/gsd-core/workflows/ai-integration-phase.md +5 -10
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +14 -3
- package/gsd-core/workflows/audit-milestone.md +11 -9
- package/gsd-core/workflows/audit-uat.md +19 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +12 -26
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +3 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
- package/gsd-core/workflows/code-review-fix.md +3 -1
- package/gsd-core/workflows/code-review.md +192 -69
- package/gsd-core/workflows/complete-milestone.md +28 -14
- package/gsd-core/workflows/debug.md +6 -4
- package/gsd-core/workflows/diagnose-issues.md +17 -7
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +46 -19
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +3 -1
- package/gsd-core/workflows/eval-review.md +5 -10
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
- 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 +29 -5
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +4 -2
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +68 -66
- package/gsd-core/workflows/execute-plan.md +25 -20
- package/gsd-core/workflows/explore.md +3 -1
- package/gsd-core/workflows/extract-learnings.md +3 -1
- package/gsd-core/workflows/fast.md +8 -2
- package/gsd-core/workflows/forensics.md +3 -1
- package/gsd-core/workflows/graduation.md +6 -6
- package/gsd-core/workflows/health.md +4 -7
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +17 -14
- package/gsd-core/workflows/inbox.md +5 -6
- package/gsd-core/workflows/ingest-docs.md +45 -12
- package/gsd-core/workflows/insert-phase.md +7 -5
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +7 -3
- package/gsd-core/workflows/list-workspaces.md +3 -1
- package/gsd-core/workflows/manager.md +15 -26
- package/gsd-core/workflows/map-codebase.md +3 -1
- package/gsd-core/workflows/milestone-summary.md +3 -1
- package/gsd-core/workflows/mvp-phase.md +3 -3
- package/gsd-core/workflows/new-milestone.md +10 -22
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +17 -29
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +4 -2
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +20 -5
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
- package/gsd-core/workflows/plan-phase.md +251 -54
- package/gsd-core/workflows/plan-review-convergence.md +148 -19
- package/gsd-core/workflows/plant-seed.md +3 -3
- package/gsd-core/workflows/pr-branch.md +195 -51
- package/gsd-core/workflows/profile-user.md +17 -15
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +52 -15
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +33 -32
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +6 -4
- package/gsd-core/workflows/remove-workspace.md +3 -3
- package/gsd-core/workflows/resume-project.md +14 -14
- package/gsd-core/workflows/review.md +404 -21
- package/gsd-core/workflows/scan.md +3 -1
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +9 -9
- package/gsd-core/workflows/settings-integrations.md +66 -32
- package/gsd-core/workflows/settings.md +4 -6
- package/gsd-core/workflows/ship.md +22 -16
- package/gsd-core/workflows/sketch-wrap-up.md +13 -17
- package/gsd-core/workflows/sketch.md +13 -19
- package/gsd-core/workflows/smart-entry.md +4 -6
- package/gsd-core/workflows/spec-phase.md +31 -4
- package/gsd-core/workflows/spike-wrap-up.md +9 -11
- package/gsd-core/workflows/spike.md +21 -32
- package/gsd-core/workflows/stats.md +4 -2
- package/gsd-core/workflows/sync-skills.md +13 -5
- package/gsd-core/workflows/thread.md +13 -7
- package/gsd-core/workflows/transition.md +7 -5
- package/gsd-core/workflows/ui-phase.md +36 -21
- package/gsd-core/workflows/ui-review.md +7 -11
- package/gsd-core/workflows/ultraplan-phase.md +7 -13
- package/gsd-core/workflows/undo.md +9 -17
- package/gsd-core/workflows/update.md +47 -48
- package/gsd-core/workflows/validate-phase.md +3 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +106 -21
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +302 -22
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +77 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +46 -12
- package/hooks/dist/gsd-read-guard.js +18 -7
- package/hooks/dist/gsd-read-injection-scanner.js +22 -13
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +222 -29
- package/hooks/dist/gsd-validate-commit.sh +523 -12
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +36 -17
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +4 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +302 -22
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +3 -1
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +28 -23
- package/hooks/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +77 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +46 -12
- package/hooks/gsd-read-guard.js +18 -7
- package/hooks/gsd-read-injection-scanner.js +22 -13
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +222 -29
- package/hooks/gsd-validate-commit.sh +523 -12
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +36 -17
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/hooks.json +6 -0
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +4 -0
- package/package.json +14 -9
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +12 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +52 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +406 -0
- package/scripts/gen-capability-registry.cjs +8 -6
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-loop-host-contract.cjs +189 -4
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +32 -2
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +45 -14
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-response-language-coverage.cjs +524 -0
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +124 -4
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +51 -64
- package/scripts/lint-vendored-deps.cjs +208 -35
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +83 -14
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +22 -3
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -109
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/state-write-path-drift-baseline.json +0 -19
package/bin/install.js
CHANGED
|
@@ -41,6 +41,7 @@ const {
|
|
|
41
41
|
// consentRequired, hostPrecedenceRank) instead of the id being re-derived
|
|
42
42
|
// and re-interpreted at each call site. See src/install-scope.cts.
|
|
43
43
|
const { resolveScope } = require('../gsd-core/bin/lib/install-scope.cjs');
|
|
44
|
+
const { isTestHomeGuardRefusal } = require('../gsd-core/bin/lib/real-home-guard.cjs');
|
|
44
45
|
// getDirName (runtime -> local config dir name) is relocated out of this
|
|
45
46
|
// installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the
|
|
46
47
|
// conversion module's rewrite engine can consume it without importing
|
|
@@ -182,10 +183,11 @@ function isCodexHooksFeatureKey(key) {
|
|
|
182
183
|
return CODEX_HOOKS_FEATURE_ALL_KEYS.includes(key);
|
|
183
184
|
}
|
|
184
185
|
|
|
185
|
-
// #768 \u2014 Claude Code permissions.allow
|
|
186
|
+
// #768 \u2014 Claude Code permissions.allow entries.
|
|
186
187
|
// Pre-populated during Claude installs to eliminate first-run approval friction
|
|
187
|
-
// for gsd-core's own known-safe tool calls
|
|
188
|
-
//
|
|
188
|
+
// for gsd-core's own known-safe tool calls. (The defense-in-depth deny entries
|
|
189
|
+
// for credential files that #768 also wrote are retired \u2014 see
|
|
190
|
+
// GSD_CLAUDE_LEGACY_DENY_PERMISSIONS below.)
|
|
189
191
|
//
|
|
190
192
|
// Format: each string uses Claude Code's documented permission rule syntax \u2014
|
|
191
193
|
// "Tool(pattern)" e.g. "Bash(npx gsd-core *)", "Read(.planning/*)"
|
|
@@ -203,7 +205,20 @@ const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([
|
|
|
203
205
|
'Read(STATE.md)',
|
|
204
206
|
'Edit(STATE.md)',
|
|
205
207
|
]);
|
|
206
|
-
|
|
208
|
+
// #4221 \u2014 Retired deny rules. #768 wrote these three `Read()` deny rules
|
|
209
|
+
// into settings.json; Claude Code 2.1.259 hardened the Bash-side enforcement
|
|
210
|
+
// of Read() deny rules so that ANY such rule makes every
|
|
211
|
+
// `cd DIR && grep/cat relative-path` compound prompt for approval, even in
|
|
212
|
+
// `auto` permission mode \u2014 and GSD subagents emit hundreds of those per
|
|
213
|
+
// session. The same protection now ships as the managed PreToolUse hook
|
|
214
|
+
// hooks/gsd-secret-read-guard.js (a hook denial is not a permission rule and
|
|
215
|
+
// never arms that check). Unlike the #2278 allow-side migration, there is no
|
|
216
|
+
// surviving "current" deny list: the constant is RENAMED to its legacy role
|
|
217
|
+
// and only ever filtered, never added. Byte-equal strings only \u2014 a user's
|
|
218
|
+
// own hand-written identical rule is indistinguishable and is removed too
|
|
219
|
+
// (the install manifest never recorded permission strings, so a
|
|
220
|
+
// manifest-gated cleanup is not possible).
|
|
221
|
+
const GSD_CLAUDE_LEGACY_DENY_PERMISSIONS = Object.freeze([
|
|
207
222
|
'Read(.env)',
|
|
208
223
|
'Read(.env.*)',
|
|
209
224
|
'Read(.secrets)',
|
|
@@ -235,6 +250,13 @@ const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
|
|
|
235
250
|
* so existing installs end up with the working `Edit(...)` forms instead of
|
|
236
251
|
* both the dead legacy entry and its replacement sitting side by side.
|
|
237
252
|
*
|
|
253
|
+
* Migration (#4221): the retired GSD_CLAUDE_LEGACY_DENY_PERMISSIONS entries
|
|
254
|
+
* are removed from permissions.deny (byte-equal only). Nothing is added to
|
|
255
|
+
* deny any more: an absent `deny` key is left absent (never created as an
|
|
256
|
+
* empty array), and a `deny` array emptied BY THIS FILTER is deleted so the
|
|
257
|
+
* retirement leaves no `"deny": []` residue; a user's pre-existing empty
|
|
258
|
+
* `deny: []` is untouched.
|
|
259
|
+
*
|
|
238
260
|
* Defensive: if settings is not a plain object, returns immediately without
|
|
239
261
|
* throwing. If permissions.allow / permissions.deny exist but are not arrays
|
|
240
262
|
* (malformed settings), they are replaced with valid arrays.
|
|
@@ -251,7 +273,7 @@ function mergeClaudePermissions(settings) {
|
|
|
251
273
|
if (!Array.isArray(settings.permissions.allow)) {
|
|
252
274
|
settings.permissions.allow = [];
|
|
253
275
|
}
|
|
254
|
-
if (!Array.isArray(settings.permissions.deny)) {
|
|
276
|
+
if (settings.permissions.deny !== undefined && !Array.isArray(settings.permissions.deny)) {
|
|
255
277
|
settings.permissions.deny = [];
|
|
256
278
|
}
|
|
257
279
|
|
|
@@ -264,9 +286,13 @@ function mergeClaudePermissions(settings) {
|
|
|
264
286
|
settings.permissions.allow.push(entry);
|
|
265
287
|
}
|
|
266
288
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
289
|
+
if (Array.isArray(settings.permissions.deny)) {
|
|
290
|
+
const before = settings.permissions.deny.length;
|
|
291
|
+
settings.permissions.deny = settings.permissions.deny.filter(
|
|
292
|
+
(e) => !GSD_CLAUDE_LEGACY_DENY_PERMISSIONS.includes(e)
|
|
293
|
+
);
|
|
294
|
+
if (settings.permissions.deny.length === 0 && before > 0) {
|
|
295
|
+
delete settings.permissions.deny;
|
|
270
296
|
}
|
|
271
297
|
}
|
|
272
298
|
}
|
|
@@ -410,7 +436,7 @@ const GSD_CHANGESET_FILES = [
|
|
|
410
436
|
'github-release-notes.cjs', 'lint.cjs', 'new.cjs',
|
|
411
437
|
'README.md', // documentation only — not user-authored
|
|
412
438
|
];
|
|
413
|
-
const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-scan.cjs', 'alias-drift-families.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'];
|
|
414
440
|
|
|
415
441
|
/**
|
|
416
442
|
* Resolve a runtime's shared-hooks directory name from its descriptor.
|
|
@@ -459,19 +485,32 @@ function resolveSharedHooksDirName(runtime) {
|
|
|
459
485
|
return name;
|
|
460
486
|
}
|
|
461
487
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
488
|
+
// #3897 rung 3 — sandbox_mode derivation, the hold list, and the hold-roster
|
|
489
|
+
// validator now live in `src/codex-agent-toml.cts` (compiled to
|
|
490
|
+
// `gsd-core/bin/lib/codex-agent-toml.cjs`), NOT here. This module used to be
|
|
491
|
+
// the sole owner, and `agent-install-check.cts`'s `checkCodexSandboxPosture`
|
|
492
|
+
// lazily `require()`d THIS FILE to reach `deriveCodexSandboxMode` — but
|
|
493
|
+
// requiring `bin/install.js` runs its whole top-level script, including the
|
|
494
|
+
// CLI's ASCII banner print to stdout, which corrupted every stdout-JSON
|
|
495
|
+
// caller downstream of that posture check (`gsd-tools validate agents`).
|
|
496
|
+
// `codex-agent-toml.cjs` is a genuine leaf (no top-level side effects), so
|
|
497
|
+
// both this file and `agent-install-check.cts` import the derivation from
|
|
498
|
+
// there — ONE owner, no second predicate. See that module's header for the
|
|
499
|
+
// full rationale, and CAUSE B (below, `installCodexConfig`) for the removal
|
|
500
|
+
// of `validateCodexSandboxHolds`'s call from the install runtime path.
|
|
501
|
+
const {
|
|
502
|
+
CODEX_SANDBOX_HOLDS,
|
|
503
|
+
deriveCodexSandboxMode,
|
|
504
|
+
validateCodexSandboxHolds,
|
|
505
|
+
// #3897 list-form parse fix, Fix 3 (generative-fix-divergence): this
|
|
506
|
+
// file's own `generateCodexAgentToml` used to pull `tools:` via its
|
|
507
|
+
// private `extractFrontmatterField` (single-line only) instead of this
|
|
508
|
+
// shared reader — the two sandbox-feeding paths (this emitter and
|
|
509
|
+
// `agent-install-check.cts`'s `checkCodexSandboxPosture`) silently
|
|
510
|
+
// disagreed on YAML block-list `tools:` form. Both now route through this
|
|
511
|
+
// ONE extractor.
|
|
512
|
+
extractToolsValue,
|
|
513
|
+
} = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'codex-agent-toml.cjs'));
|
|
475
514
|
|
|
476
515
|
// Copilot tool name mapping — Claude Code tools to GitHub Copilot tools
|
|
477
516
|
// Tool mapping applies ONLY to agents, NOT to skills (per CONTEXT.md decision)
|
|
@@ -668,6 +707,68 @@ function _resolveScopeSafe(id, runtime) {
|
|
|
668
707
|
}
|
|
669
708
|
}
|
|
670
709
|
|
|
710
|
+
/**
|
|
711
|
+
* Resolve a layout kind's on-disk destination directory. Shared by the
|
|
712
|
+
* skills-root resolution above and the #3664 warning below so the
|
|
713
|
+
* home-override + destSubpath join exists once (#3659-class re-encoding guard).
|
|
714
|
+
*/
|
|
715
|
+
function _kindDestDir(layout, kindName, targetDir) {
|
|
716
|
+
const kind = layout.kinds.find((k) => k.kind === kindName);
|
|
717
|
+
if (!kind) return null;
|
|
718
|
+
return path.join(kind.home || targetDir, kind.destSubpath);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* #3738: scope-aware, layout-resolving wrapper over _kindDestDir for callers
|
|
723
|
+
* that have (runtime, configDir, scope) rather than a resolved Layout — the
|
|
724
|
+
* writeManifest agents surface being the first. Never throws: a runtime whose
|
|
725
|
+
* layout cannot be resolved (unknown id, descriptor error) keeps the caller's
|
|
726
|
+
* own fallback rather than losing the manifest.
|
|
727
|
+
*/
|
|
728
|
+
function _kindDestDirSafe(runtime, configDir, scope, kindName) {
|
|
729
|
+
try {
|
|
730
|
+
return _kindDestDir(resolveRuntimeArtifactLayout(runtime, configDir, scope), kindName, configDir);
|
|
731
|
+
} catch {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* #3664 — warn (never refuse) when `--config-dir` points the install at a
|
|
738
|
+
* directory whose agent destination already holds FOREIGN (non-GSD) agent
|
|
739
|
+
* files — the fingerprint of another harness's config home (e.g. ~/.junie,
|
|
740
|
+
* ~/.factory) or a hand-curated agents dir. GSD emits the selected runtime's
|
|
741
|
+
* artifacts verbatim: tool IDs (`Skill`) and MCP grants (`mcp__server__tool`)
|
|
742
|
+
* that are inert or invalid in a foreign harness surface only at dispatch
|
|
743
|
+
* time, months later. Warn-and-proceed is the issue-sanctioned option (b):
|
|
744
|
+
* a fresh custom dir (the documented brand-specific-dir use), a gsd-only dir
|
|
745
|
+
* (updates, the --all shared dir — including kimi's root `gsd.md`, which is
|
|
746
|
+
* GSD-owned despite the bare `gsd` stem), and the no-flag default-home path
|
|
747
|
+
* (users keep personal agents in ~/.claude/agents) all stay silent. Degrades
|
|
748
|
+
* silently on any resolution failure — the warn path never blocks install.
|
|
749
|
+
*/
|
|
750
|
+
function warnIfForeignAgentDest(runtime, targetDir, scope, explicitConfigDir) {
|
|
751
|
+
if (explicitConfigDir !== true) return;
|
|
752
|
+
try {
|
|
753
|
+
const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope);
|
|
754
|
+
// kimi's global agents kind is `kimi-agents` (#2095 EoS), every other
|
|
755
|
+
// runtime's is `agents`.
|
|
756
|
+
const agentsDir = _kindDestDir(layout, 'agents', targetDir)
|
|
757
|
+
|| _kindDestDir(layout, 'kimi-agents', targetDir);
|
|
758
|
+
if (!agentsDir) return;
|
|
759
|
+
if (!fs.existsSync(agentsDir) || !fs.statSync(agentsDir).isDirectory()) return;
|
|
760
|
+
const foreign = fs
|
|
761
|
+
.readdirSync(agentsDir)
|
|
762
|
+
.filter((f) => f.endsWith('.md') && !f.startsWith('gsd-') && f !== 'gsd.md');
|
|
763
|
+
if (foreign.length === 0) return;
|
|
764
|
+
console.log(
|
|
765
|
+
` ${yellow}⚠${reset} ${bold}${targetDir}${reset} already contains ${foreign.length} non-GSD agent file(s) — this may be another harness's config home. GSD emits artifacts shaped for ${runtime}: tool IDs and MCP grants may be inert or invalid for whatever harness reads this directory (#3664).`
|
|
766
|
+
);
|
|
767
|
+
} catch (_) {
|
|
768
|
+
/* never block install on the warning path */
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
671
772
|
/**
|
|
672
773
|
* Resolve the ACTUAL on-disk skills-install directory for a runtime, honoring a
|
|
673
774
|
* skills-kind `home` override (ADR-1239 upgrade 3 / #2088: e.g. Codex skills ->
|
|
@@ -678,8 +779,8 @@ function _resolveScopeSafe(id, runtime) {
|
|
|
678
779
|
function _resolveSkillsRootDir(runtime, targetDir, scope) {
|
|
679
780
|
try {
|
|
680
781
|
const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope);
|
|
681
|
-
const
|
|
682
|
-
if (
|
|
782
|
+
const skillsDir = _kindDestDir(layout, 'skills', targetDir);
|
|
783
|
+
if (skillsDir) return skillsDir;
|
|
683
784
|
} catch (_e) { /* fall through to the configDir default */ }
|
|
684
785
|
return path.join(targetDir, 'skills');
|
|
685
786
|
}
|
|
@@ -700,6 +801,7 @@ function _runtimeAdapter(runtime) {
|
|
|
700
801
|
}
|
|
701
802
|
}
|
|
702
803
|
const {
|
|
804
|
+
acquireInstallMigrationLock,
|
|
703
805
|
applyInstallerMigrationPlan,
|
|
704
806
|
discoverInstallerMigrations,
|
|
705
807
|
MANIFEST_SCHEMA_VERSION,
|
|
@@ -713,6 +815,10 @@ const {
|
|
|
713
815
|
const {
|
|
714
816
|
resolveRuntimeArtifactLayout,
|
|
715
817
|
} = require(path.join(_gsdLibDir, 'runtime-artifact-layout.cjs'));
|
|
818
|
+
const {
|
|
819
|
+
readSurface,
|
|
820
|
+
resolveSurface,
|
|
821
|
+
} = require(path.join(_gsdLibDir, 'surface.cjs'));
|
|
716
822
|
const {
|
|
717
823
|
assertDestWithinConfigHome,
|
|
718
824
|
createRuntimeArtifactInstallPlan,
|
|
@@ -812,6 +918,17 @@ const hasSkillsRoot = args.includes('--skills-root');
|
|
|
812
918
|
const hasPortableHooks = args.includes('--portable-hooks') || process.env.GSD_PORTABLE_HOOKS === '1';
|
|
813
919
|
const hasMinimal = args.includes('--minimal') || args.includes('--core-only');
|
|
814
920
|
const hasDryRun = args.includes('--dry-run');
|
|
921
|
+
// #3031: opt-in reclaim of the GSD artifacts a PRE-#2755 `--kimi-code` install
|
|
922
|
+
// orphaned in Kimi CLI's `~/.kimi`. Opt-in and not automatic because the stale
|
|
923
|
+
// block is BYTE-IDENTICAL to a legitimate Kimi CLI one — both runtimes render
|
|
924
|
+
// the same bytes for the same root, since the command paths derive from the
|
|
925
|
+
// hooks root and not from the runtime — so no inspection can tell "litter GSD
|
|
926
|
+
// wrote for kimi-code" from "Kimi CLI's working hooks". Cleaning unasked would
|
|
927
|
+
// break #2755's own acceptance criterion ("Uninstalling GSD hooks for one
|
|
928
|
+
// runtime does not touch or remove the other runtime's hooks") for anyone with
|
|
929
|
+
// both products installed. The user, who knows which products they run, is the
|
|
930
|
+
// only party that can decide — so they ask for it explicitly.
|
|
931
|
+
const hasReclaimKimiLegacy = args.includes('--reclaim-kimi-legacy');
|
|
815
932
|
// --profile=<name> or --profile=<n1>,<n2> (composable); mutually exclusive with --minimal
|
|
816
933
|
const _profileArgRaw = (() => {
|
|
817
934
|
for (const arg of args) {
|
|
@@ -911,6 +1028,21 @@ function disambiguateKimiVariant(runtimes) {
|
|
|
911
1028
|
return notices;
|
|
912
1029
|
}
|
|
913
1030
|
|
|
1031
|
+
// #3031: `--reclaim-kimi-legacy` only ever acts inside the kimi-code GLOBAL
|
|
1032
|
+
// install branch. Say so when it cannot act, rather than exiting 0 having
|
|
1033
|
+
// silently done nothing: the user asked for a cleanup, and silence is
|
|
1034
|
+
// indistinguishable from "it ran and found nothing". Not a hard error — it
|
|
1035
|
+
// stays composable with `--all`, where it is legitimately inert for the other
|
|
1036
|
+
// seventeen runtimes.
|
|
1037
|
+
if (hasReclaimKimiLegacy && !selectedRuntimes.includes('kimi-code')) {
|
|
1038
|
+
console.error(`${yellow}⚠ --reclaim-kimi-legacy ignored${reset} — it applies only to a --kimi-code install; nothing in ~/.kimi was touched.`);
|
|
1039
|
+
} else if (hasReclaimKimiLegacy && hasLocal) {
|
|
1040
|
+
// Scope, checked HERE rather than inside install(): kimi-code declares
|
|
1041
|
+
// hostBehaviors.localInstallDeferred, so install() returns early long before
|
|
1042
|
+
// the kimi-hooks-toml branch — a warning placed there would be unreachable.
|
|
1043
|
+
console.error(`${yellow}⚠ --reclaim-kimi-legacy ignored${reset} — the legacy root is a global location; re-run with --global to reclaim it.`);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
914
1046
|
if (selectedRuntimes.includes('kimi') || selectedRuntimes.includes('kimi-code')) {
|
|
915
1047
|
const kimiNotices = disambiguateKimiVariant(selectedRuntimes);
|
|
916
1048
|
for (const notice of kimiNotices) {
|
|
@@ -1075,6 +1207,14 @@ function parseConfigDirFromArgs(argsArray) {
|
|
|
1075
1207
|
return null;
|
|
1076
1208
|
}
|
|
1077
1209
|
|
|
1210
|
+
// Parse --no-legacy-cleanup (#3799) — skip the legacy get-shit-done-cc scan
|
|
1211
|
+
// entirely. Some users run gsd-core alongside a live legacy install on
|
|
1212
|
+
// purpose; the scan is best-effbelt cleanup, never load-bearing for the
|
|
1213
|
+
// install itself.
|
|
1214
|
+
function parseNoLegacyCleanupArg(args = process.argv) {
|
|
1215
|
+
return args.includes('--no-legacy-cleanup');
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1078
1218
|
// Parse --config-dir argument
|
|
1079
1219
|
function parseConfigDirArg() {
|
|
1080
1220
|
const result = parseConfigDirFromArgs(args);
|
|
@@ -1105,7 +1245,7 @@ if (hasUninstall) {
|
|
|
1105
1245
|
|
|
1106
1246
|
// Show help if requested
|
|
1107
1247
|
if (hasHelp) {
|
|
1108
|
-
console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--zcode${reset} Install for ZCode only\n ${cyan}--pi${reset} Install for Pi only\n ${cyan}--gemini${reset} Install for Gemini CLI only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (
|
|
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`);
|
|
1109
1249
|
process.exit(0);
|
|
1110
1250
|
}
|
|
1111
1251
|
|
|
@@ -1125,6 +1265,10 @@ if (hasHelp) {
|
|
|
1125
1265
|
// had no internal caller and no export consumer for it — hooksSurface owns
|
|
1126
1266
|
// the single implementation now, used internally by resolveNodeRunner there.)
|
|
1127
1267
|
const resolveNodeRunner = hooksSurface.resolveNodeRunner;
|
|
1268
|
+
// #3662: the runtime-resolving runner token for managed JS hooks — the baked
|
|
1269
|
+
// absolute node path tried FIRST, then `command -v node`, then well-known
|
|
1270
|
+
// layouts, resolved by the shell at hook-fire time instead of bake time.
|
|
1271
|
+
const buildNodeRunnerChainToken = hooksSurface.buildNodeRunnerChainToken;
|
|
1128
1272
|
const resolveBashRunner = hooksSurface.resolveBashRunner;
|
|
1129
1273
|
// referencesHook: pure predicate over hook entry objects, shared between
|
|
1130
1274
|
// install() and finishInstall() (ADR-857 phase 5f-1b).
|
|
@@ -1377,10 +1521,18 @@ function readSettings(settingsPath) {
|
|
|
1377
1521
|
}
|
|
1378
1522
|
|
|
1379
1523
|
/**
|
|
1380
|
-
* Write settings.json with proper formatting
|
|
1524
|
+
* Write settings.json with proper formatting.
|
|
1525
|
+
*
|
|
1526
|
+
* Atomic (temp+rename) because hosts discard the ENTIRE settings file on any
|
|
1527
|
+
* parse failure, so a truncated write costs the user every hook, permission,
|
|
1528
|
+
* and statusline they have — not just GSD's entries. This is the sole writer
|
|
1529
|
+
* of that surface for six runtimes.
|
|
1530
|
+
*
|
|
1531
|
+
* `atomicWriteFileSync` is declared further down this file; it is dereferenced
|
|
1532
|
+
* at call time, after module evaluation, so the ordering is safe.
|
|
1381
1533
|
*/
|
|
1382
1534
|
function writeSettings(settingsPath, settings) {
|
|
1383
|
-
|
|
1535
|
+
atomicWriteFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
1384
1536
|
}
|
|
1385
1537
|
|
|
1386
1538
|
// #2875 Part 2 (J8): model-override resolution (readGsdGlobalModelOverrides /
|
|
@@ -1673,8 +1825,20 @@ const kiloAgentPermissionOrder = [
|
|
|
1673
1825
|
'lsp',
|
|
1674
1826
|
];
|
|
1675
1827
|
|
|
1828
|
+
const kiloMcpPermissionPattern = /^mcp__([A-Za-z0-9_-]+)__((?:[A-Za-z0-9_-]+)|\*)$/;
|
|
1829
|
+
|
|
1830
|
+
// Derives Kilo's native `{server}_{tool}` MCP permission key (kilo.ai/docs —
|
|
1831
|
+
// external, fixed format). Not injective: both capture groups allow `_`, so
|
|
1832
|
+
// e.g. `mcp__a_b__c` and `mcp__a__b_c` derive the same key. The `Set` below
|
|
1833
|
+
// resolves any such collision deterministically to first-seen-wins — see
|
|
1834
|
+
// src/runtime-artifact-conversion.cts's regression test. Mirrors that file's
|
|
1835
|
+
// convertClaudeToKiloPermissionTool exactly (#4032).
|
|
1676
1836
|
function convertClaudeToKiloPermissionTool(claudeTool) {
|
|
1677
|
-
|
|
1837
|
+
const builtinPermission = claudeToKiloAgentPermissions[claudeTool];
|
|
1838
|
+
if (builtinPermission) return builtinPermission;
|
|
1839
|
+
|
|
1840
|
+
const mcpPermission = kiloMcpPermissionPattern.exec(claudeTool);
|
|
1841
|
+
return mcpPermission ? `${mcpPermission[1]}_${mcpPermission[2]}` : null;
|
|
1678
1842
|
}
|
|
1679
1843
|
|
|
1680
1844
|
function buildKiloAgentPermissionBlock(claudeTools) {
|
|
@@ -1691,6 +1855,10 @@ function buildKiloAgentPermissionBlock(claudeTools) {
|
|
|
1691
1855
|
for (const permission of kiloAgentPermissionOrder) {
|
|
1692
1856
|
lines.push(` ${permission}: ${allowedPermissions.has(permission) ? 'allow' : 'deny'}`);
|
|
1693
1857
|
}
|
|
1858
|
+
for (const permission of allowedPermissions) {
|
|
1859
|
+
if (kiloAgentPermissionOrder.includes(permission)) continue;
|
|
1860
|
+
lines.push(` ${permission}: allow`);
|
|
1861
|
+
}
|
|
1694
1862
|
|
|
1695
1863
|
return lines;
|
|
1696
1864
|
}
|
|
@@ -2253,7 +2421,8 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) {
|
|
|
2253
2421
|
/**
|
|
2254
2422
|
* Apply Antigravity-specific content conversion — path replacement + command name conversion.
|
|
2255
2423
|
* Path mappings depend on install mode:
|
|
2256
|
-
* Global: ~/.claude/ → ~/.gemini/
|
|
2424
|
+
* Global: ~/.claude/skills/ → ~/.gemini/config/skills/ (#3738),
|
|
2425
|
+
* ~/.claude/ → ~/.gemini/antigravity/, ./.claude/ → ./.agents/
|
|
2257
2426
|
* Local: ~/.claude/ → .agents/, ./.claude/ → ./.agents/
|
|
2258
2427
|
* Applied to ALL Antigravity content (skills, agents, engine files).
|
|
2259
2428
|
* @param {string} content - Source content to convert
|
|
@@ -2262,6 +2431,19 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) {
|
|
|
2262
2431
|
function convertClaudeToAntigravityContent(content, isGlobal = false) {
|
|
2263
2432
|
let c = content;
|
|
2264
2433
|
if (isGlobal) {
|
|
2434
|
+
// #3738: global skills install under ~/.gemini/config/skills (the dir AGY
|
|
2435
|
+
// scans for global discovery), so skills-path references must divert there
|
|
2436
|
+
// — BEFORE the configHome rewrite below, which is correct for gsd-core
|
|
2437
|
+
// runtime-file references (settings, workflows, VERSION) but wrong for the
|
|
2438
|
+
// skills dir itself. Mirrors src/runtime-artifact-conversion.cts (ADR-1508
|
|
2439
|
+
// keeps bin/install.js hand-authored; the two copies must stay in sync).
|
|
2440
|
+
c = c.replace(/\$HOME\/\.claude\/skills\//g, '$HOME/.gemini/config/skills/');
|
|
2441
|
+
c = c.replace(/~\/\.claude\/skills\//g, '~/.gemini/config/skills/');
|
|
2442
|
+
// Bare skills form (no trailing slash) — must also precede the generic
|
|
2443
|
+
// slash rule, which would otherwise divert it to the retired configHome
|
|
2444
|
+
// path ($HOME/.gemini/antigravity/skills).
|
|
2445
|
+
c = c.replace(/\$HOME\/\.claude\/skills\b/g, '$HOME/.gemini/config/skills');
|
|
2446
|
+
c = c.replace(/~\/\.claude\/skills\b/g, '~/.gemini/config/skills');
|
|
2265
2447
|
c = c.replace(/\$HOME\/\.claude\//g, '$HOME/.gemini/antigravity/');
|
|
2266
2448
|
c = c.replace(/~\/\.claude\//g, '~/.gemini/antigravity/');
|
|
2267
2449
|
// Bare form (no trailing slash) — must come after slash form to avoid double-replace
|
|
@@ -3712,26 +3894,37 @@ Execute mode fallback:
|
|
|
3712
3894
|
## C. Task() → spawn_agent Mapping
|
|
3713
3895
|
GSD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools:
|
|
3714
3896
|
|
|
3715
|
-
**Schema detection (required first step):**
|
|
3716
|
-
|
|
3717
|
-
|
|
3897
|
+
**Schema detection (required first step):** Before spawning, inspect the \`spawn_agent\`
|
|
3898
|
+
tool's visible parameter schema (via \`tool_search\` or the tool list). Use the presence
|
|
3899
|
+
of \`agent_type\` only to choose typed dispatch versus the generic-agent workaround.
|
|
3900
|
+
Detect optional fields independently: \`model\`, \`reasoning_effort\`, \`task_name\`,
|
|
3901
|
+
\`fork_turns\`, and \`fork_context\` may be added or removed without \`agent_type\` changing.
|
|
3902
|
+
Never infer one field from a schema/version label or from the presence of another field.
|
|
3718
3903
|
|
|
3719
|
-
|
|
3904
|
+
- **agent_type-capable schema:** \`spawn_agent\` advertises \`agent_type\` — typed GSD agent dispatch is available.
|
|
3905
|
+
- **Generic schema:** \`spawn_agent\` does not advertise \`agent_type\` — typed GSD agent dispatch is unavailable in this session, even if other optional fields are present.
|
|
3720
3906
|
|
|
3721
3907
|
Typed mapping (agent_type-capable schema only):
|
|
3722
3908
|
- \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
|
|
3723
3909
|
- \`Agent(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
|
|
3724
|
-
- \`Task(model="
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3910
|
+
- \`Task(model="{resolved_model}")\` → pass \`model="{resolved_model}"\` when the
|
|
3911
|
+
visible \`spawn_agent\` schema advertises \`model\` and the resolved value is explicit.
|
|
3912
|
+
This is how \`model_profile\` tier routing, including \`adaptive\`, reaches the child agent.
|
|
3913
|
+
Omit \`model\` only when the schema does not advertise \`model\`, or when the value is
|
|
3914
|
+
missing, empty, or \`"inherit"\`; omission deliberately inherits the session/static agent
|
|
3915
|
+
configuration. Explicit \`model_overrides\` may also be embedded in agent \`.toml\` files,
|
|
3916
|
+
but ordinary profile-resolved models are not, so a TOML file is not a reason to discard
|
|
3917
|
+
an available inline value.
|
|
3918
|
+
- Before each typed spawn, obtain the paired effort for its role with
|
|
3919
|
+
\`gsd_run query resolve-model <subagent_type> --pick effort\` when the workflow has not
|
|
3920
|
+
already exposed it. The resolver's unified \`effort\` field maps to the Codex spawn argument
|
|
3921
|
+
\`reasoning_effort\`; do not look for a resolver field named \`reasoning_effort\`.
|
|
3922
|
+
Pass it when the visible \`spawn_agent\` schema advertises \`reasoning_effort\`. Omit the
|
|
3923
|
+
field when it is not advertised, or when the value is missing, empty, \`"inherit"\`, or
|
|
3924
|
+
unsupported; do not invent one-off effort literals in workflow prose.
|
|
3732
3925
|
- \`fork_context: false\` by default — GSD agents load their own context via \`<required_reading>\` blocks
|
|
3733
|
-
- \`task_name\` —
|
|
3734
|
-
- \`fork_turns\` —
|
|
3926
|
+
- \`task_name\` — when advertised, provide a descriptive name for each spawned task
|
|
3927
|
+
- \`fork_turns\` — when advertised, controls turn-forking depth; coexists with \`fork_context\` (not a replacement)
|
|
3735
3928
|
- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
|
|
3736
3929
|
but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex
|
|
3737
3930
|
\`spawn_agent\` still does not create or bind a git worktree; instead GSD itself
|
|
@@ -3903,10 +4096,39 @@ function _resetCodexWarningDedupeForTests() {
|
|
|
3903
4096
|
* @param {object|null} effortCfg — #443: merged effort config from readGsdEffectiveEffortConfig
|
|
3904
4097
|
*/
|
|
3905
4098
|
function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, runtimeResolver = null, effortCfg = null, sandboxTier = 'codex-agent-sandbox') {
|
|
3906
|
-
const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only';
|
|
3907
4099
|
const { frontmatter, body } = extractFrontmatterAndBody(agentContent);
|
|
3908
4100
|
const frontmatterText = frontmatter || '';
|
|
4101
|
+
// #3897 list-form parse fix, Fix 3: `toolsRaw` MUST come from the same
|
|
4102
|
+
// shared `extractToolsValue` reader `checkCodexSandboxPosture` uses, not
|
|
4103
|
+
// this file's own `extractFrontmatterField` — the two used to disagree on
|
|
4104
|
+
// YAML block-list `tools:` form (`extractFrontmatterField`'s single-line
|
|
4105
|
+
// regex read only the first list item), which is exactly the generative-
|
|
4106
|
+
// fix-divergence shape CLAUDE.md warns about for two paths feeding one
|
|
4107
|
+
// derivation. `extractToolsValue` does its own `---`-delimited frontmatter
|
|
4108
|
+
// scan of the full `agentContent`, so it is not re-derived from
|
|
4109
|
+
// `frontmatterText` here.
|
|
4110
|
+
const toolsRaw = extractToolsValue(agentContent) ?? '';
|
|
3909
4111
|
const resolvedName = extractFrontmatterField(frontmatterText, 'name') || agentName;
|
|
4112
|
+
// #3897 rung 3 — derived from the role's own tool contract (HALT.md option
|
|
4113
|
+
// 2). The former hand-maintained CODEX_AGENT_SANDBOX map is deleted (ADR-3473
|
|
4114
|
+
// §8.3): it was fully redundant with this derivation, zero disagreements
|
|
4115
|
+
// across all 11 entries. Never a silent `|| 'read-only'` fallback either.
|
|
4116
|
+
// Derivation itself lives in codex-agent-toml.cjs, which does no frontmatter
|
|
4117
|
+
// parsing of its own (no third copy of that extraction) — it takes the
|
|
4118
|
+
// already-resolved `tools:` value, extracted via the shared reader above.
|
|
4119
|
+
//
|
|
4120
|
+
// #3897 security review F1 (blocker): pass BOTH candidate identities —
|
|
4121
|
+
// `agentName` (the caller's filename-stem identity) AND `resolvedName`
|
|
4122
|
+
// (the frontmatter `name:` this function's OWN emitted `name = ...` line
|
|
4123
|
+
// uses, and what `installCodexConfig`'s caller keys the output PATH on) —
|
|
4124
|
+
// never just one. Deciding the sandbox for `agentName` alone and applying
|
|
4125
|
+
// it to an artifact that a DIFFERENT identity (`resolvedName`) names is
|
|
4126
|
+
// exactly how a held role's own `.toml` could end up `workspace-write`
|
|
4127
|
+
// (rename the source file, or plant a sibling whose `name:` collides with
|
|
4128
|
+
// a held role). `deriveCodexSandboxMode` takes the most restrictive result
|
|
4129
|
+
// across every candidate — see its doc and `isSandboxHeld` in
|
|
4130
|
+
// codex-agent-toml.cjs.
|
|
4131
|
+
const sandboxMode = deriveCodexSandboxMode([agentName, resolvedName], toolsRaw);
|
|
3910
4132
|
const resolvedDescription = toSingleLine(
|
|
3911
4133
|
extractFrontmatterField(frontmatterText, 'description') || `GSD agent ${resolvedName}`
|
|
3912
4134
|
);
|
|
@@ -3986,8 +4208,10 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
|
|
|
3986
4208
|
// #443 — Unified effort for Codex .toml. Uses the same config-driven precedence chain
|
|
3987
4209
|
// as the Claude .md effort injection (resolveInstallTimeEffort), so both runtimes read
|
|
3988
4210
|
// from the same effort.agent_overrides / effort.routing_tier_defaults / effort.default
|
|
3989
|
-
// config source.
|
|
3990
|
-
//
|
|
4211
|
+
// config source. #3007 — Codex advertises supported_reasoning_levels per model, so the
|
|
4212
|
+
// pinned model id is passed through and the value is resolved against that model's own
|
|
4213
|
+
// set: 'max' now passes, 'minimal' clamps up to 'low', and 'ultra' is refused (no key
|
|
4214
|
+
// emitted) rather than clamped to a fabricated level.
|
|
3991
4215
|
// #838 — Do not pin effort when Codex is intentionally inheriting the parent
|
|
3992
4216
|
// chat model. A TOML with no `model` but a static `model_reasoning_effort`
|
|
3993
4217
|
// creates confusing partial routing: model follows the Codex UI while effort
|
|
@@ -3997,8 +4221,12 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
|
|
|
3997
4221
|
// #3533 (10d): 'inherit' means OMIT the pin — the agent follows the host's
|
|
3998
4222
|
// own effort default. Never write the literal.
|
|
3999
4223
|
if (_universalEffortCodex !== 'inherit') {
|
|
4000
|
-
const _renderedEffortCodex = _getGsdEffortCatalog().renderEffortForRuntime('codex', _universalEffortCodex).value;
|
|
4001
|
-
|
|
4224
|
+
const _renderedEffortCodex = _getGsdEffortCatalog().renderEffortForRuntime('codex', _universalEffortCodex, pinnedModel).value;
|
|
4225
|
+
// #3007 — 'ultra' is rejected by the model's supported_reasoning_levels and
|
|
4226
|
+
// renders as null. Omit the key entirely rather than write a literal `null`.
|
|
4227
|
+
if (_renderedEffortCodex !== null) {
|
|
4228
|
+
lines.push(`model_reasoning_effort = ${JSON.stringify(_renderedEffortCodex)}`);
|
|
4229
|
+
}
|
|
4002
4230
|
}
|
|
4003
4231
|
}
|
|
4004
4232
|
|
|
@@ -6206,6 +6434,31 @@ const __atomicWrittenTmps = hooksSurface.__atomicWrittenTmps;
|
|
|
6206
6434
|
* All writes go through atomicWriteFileSync so a mid-write failure leaves
|
|
6207
6435
|
* the original config.toml untouched (#2760 fix 4).
|
|
6208
6436
|
*/
|
|
6437
|
+
/**
|
|
6438
|
+
* Split TOML content into its leading TOP-LEVEL key lines and everything from
|
|
6439
|
+
* the first table header onward (#3610).
|
|
6440
|
+
*
|
|
6441
|
+
* Top-level keys were file-scoped before a merge. The regenerated GSD block
|
|
6442
|
+
* opens with a table header (`[agents]`, #2088/ADR-1239 upgrade 2), so placing
|
|
6443
|
+
* that block ABOVE surviving top-level keys re-scopes them into `[agents]` —
|
|
6444
|
+
* `validateCodexConfigSchema` then correctly rejects the merged file and the
|
|
6445
|
+
* install aborts. Hoisting the keys above the block preserves their scope.
|
|
6446
|
+
*
|
|
6447
|
+
* Table headers inside multiline strings do not start the "rest" region (the
|
|
6448
|
+
* record parser already excludes them via startsInMultilineString).
|
|
6449
|
+
*/
|
|
6450
|
+
function splitTopLevelKeys(content) {
|
|
6451
|
+
for (const record of getTomlLineRecords(content)) {
|
|
6452
|
+
if (record.tableHeader && !record.startsInMultilineString) {
|
|
6453
|
+
return {
|
|
6454
|
+
topLevel: content.slice(0, record.start).trim(),
|
|
6455
|
+
rest: content.slice(record.start).trim(),
|
|
6456
|
+
};
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
return { topLevel: content.trim(), rest: '' };
|
|
6460
|
+
}
|
|
6461
|
+
|
|
6209
6462
|
function mergeCodexConfig(configPath, gsdBlock) {
|
|
6210
6463
|
// Case 1: No config.toml — create fresh
|
|
6211
6464
|
if (!fs.existsSync(configPath)) {
|
|
@@ -6253,10 +6506,25 @@ function mergeCodexConfig(configPath, gsdBlock) {
|
|
|
6253
6506
|
.replace(/^\r?\n# GSD codex_hooks ownership: (?:section|root_dotted)\r?\n/, '');
|
|
6254
6507
|
const afterUser = stripLeakedGsdCodexSections(markerStripped).trim();
|
|
6255
6508
|
|
|
6509
|
+
// #3610: top-level keys that survived BELOW the marker were file-scoped
|
|
6510
|
+
// before this merge; the regenerated block opens with the `[agents]` table
|
|
6511
|
+
// header, so they must be hoisted to FILE scope or TOML re-scopes them
|
|
6512
|
+
// into a table. File scope means BEFORE the first table header of the
|
|
6513
|
+
// pre-marker region too — appending them after a pre-marker table (the
|
|
6514
|
+
// default real-world layout: user tables precede the marker) would merely
|
|
6515
|
+
// capture them into THAT table instead of [agents], and the schema
|
|
6516
|
+
// validator is blind to non-agents tables.
|
|
6517
|
+
const beforeSplit = before ? splitTopLevelKeys(before) : { topLevel: '', rest: '' };
|
|
6518
|
+
const { topLevel: afterTopLevel, rest: afterTables } = splitTopLevelKeys(afterUser);
|
|
6519
|
+
|
|
6256
6520
|
const parts = [];
|
|
6257
|
-
|
|
6521
|
+
const topParts = [];
|
|
6522
|
+
if (beforeSplit.topLevel) topParts.push(beforeSplit.topLevel);
|
|
6523
|
+
if (afterTopLevel) topParts.push(afterTopLevel);
|
|
6524
|
+
if (topParts.length > 0) parts.push(topParts.join(eol + eol));
|
|
6525
|
+
if (beforeSplit.rest) parts.push(beforeSplit.rest);
|
|
6258
6526
|
parts.push(normalizedGsdBlock);
|
|
6259
|
-
if (
|
|
6527
|
+
if (afterTables) parts.push(afterTables);
|
|
6260
6528
|
atomicWriteFileSync(configPath, parts.join(eol + eol) + eol);
|
|
6261
6529
|
return;
|
|
6262
6530
|
}
|
|
@@ -6726,29 +6994,50 @@ function writeNonClaudeDefaults(runtime) {
|
|
|
6726
6994
|
if (_hostBehaviors(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return;
|
|
6727
6995
|
const gsdDir = path.join(os.homedir(), '.gsd');
|
|
6728
6996
|
const defaultsPath = path.join(gsdDir, 'defaults.json');
|
|
6997
|
+
let releaseLock = null;
|
|
6729
6998
|
try {
|
|
6730
6999
|
fs.mkdirSync(gsdDir, { recursive: true });
|
|
7000
|
+
// defaults.json is machine-global — every runtime and project on the box
|
|
7001
|
+
// reads it. Serialize the read-modify-write so two concurrent installs
|
|
7002
|
+
// cannot lose each other's key, and apply both mutations in ONE atomic
|
|
7003
|
+
// write so a crash cannot leave the file truncated (the read path swallows
|
|
7004
|
+
// parse errors and treats a corrupt file as absent, which would silently
|
|
7005
|
+
// degrade model resolution everywhere until repaired by hand).
|
|
7006
|
+
releaseLock = acquireInstallMigrationLock(gsdDir);
|
|
6731
7007
|
let defaults = {};
|
|
6732
7008
|
try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* new file */ }
|
|
6733
7009
|
if (defaults === null || typeof defaults !== 'object' || Array.isArray(defaults)) {
|
|
6734
7010
|
defaults = {};
|
|
6735
7011
|
}
|
|
7012
|
+
const applied = [];
|
|
6736
7013
|
// Three-valued domain: false/absent → aliases; true → full IDs; "omit" → ''.
|
|
6737
7014
|
const existing = defaults.resolve_model_ids;
|
|
6738
7015
|
const shouldDefaultToOmit = existing !== true && existing !== 'omit';
|
|
6739
7016
|
if (shouldDefaultToOmit) {
|
|
6740
7017
|
defaults.resolve_model_ids = 'omit';
|
|
6741
|
-
|
|
6742
|
-
console.log(` ${green}✓${reset} Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
|
|
7018
|
+
applied.push(`Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
|
|
6743
7019
|
}
|
|
6744
7020
|
// #2395: persist runtime for non-Claude runtimes.
|
|
6745
7021
|
if (defaults.runtime === undefined || defaults.runtime === null || defaults.runtime === '') {
|
|
6746
7022
|
defaults.runtime = runtime;
|
|
6747
|
-
|
|
6748
|
-
|
|
7023
|
+
applied.push(`Set runtime: "${runtime}" in ~/.gsd/defaults.json`);
|
|
7024
|
+
}
|
|
7025
|
+
if (applied.length > 0) {
|
|
7026
|
+
atomicWriteFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n', 'utf8');
|
|
7027
|
+
for (const message of applied) console.log(` ${green}✓${reset} ${message}`);
|
|
6749
7028
|
}
|
|
6750
7029
|
} catch (e) {
|
|
6751
7030
|
console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
|
|
7031
|
+
} finally {
|
|
7032
|
+
if (releaseLock) {
|
|
7033
|
+
try {
|
|
7034
|
+
releaseLock();
|
|
7035
|
+
} catch (releaseError) {
|
|
7036
|
+
// A leaked lock blocks the next install, so surface it rather than
|
|
7037
|
+
// swallowing; the stale-lock reaper clears it once this pid exits.
|
|
7038
|
+
console.log(` ${yellow}⚠${reset} Could not release the ~/.gsd install lock: ${releaseError.message}`);
|
|
7039
|
+
}
|
|
7040
|
+
}
|
|
6752
7041
|
}
|
|
6753
7042
|
}
|
|
6754
7043
|
|
|
@@ -6772,6 +7061,36 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
6772
7061
|
}
|
|
6773
7062
|
fs.mkdirSync(agentsTomlDir, { recursive: true });
|
|
6774
7063
|
|
|
7064
|
+
// #3897 rung 3 (CAUSE B fix) — validateCodexSandboxHolds is deliberately
|
|
7065
|
+
// NOT called here. The "no stale holds" invariant is a REPO invariant about
|
|
7066
|
+
// the canonical roster in `agents/`, not a property of whatever directory
|
|
7067
|
+
// an install happens to read from: a partial or synthetic `agentsSrc` (a
|
|
7068
|
+
// test fixture, a `--config-dir` subset) legitimately contains only a few
|
|
7069
|
+
// agents, and a held role simply absent from THIS source dir must be
|
|
7070
|
+
// inert, not fatal. Throwing here also masked unrelated failures further
|
|
7071
|
+
// down this same loop (e.g. the name-injection/path-escape guard on
|
|
7072
|
+
// `agentTomlPath` below), since this check ran first and unconditionally.
|
|
7073
|
+
// The invariant is still enforced — as a test over the real `agents/`
|
|
7074
|
+
// roster (tests/codex-config.test.cjs T24/T25) — just never on this
|
|
7075
|
+
// runtime path. See src/codex-agent-toml.cts's `validateCodexSandboxHolds`
|
|
7076
|
+
// docblock for the full rationale.
|
|
7077
|
+
//
|
|
7078
|
+
// #3897 security review F2 (assessed post-F1-fix, verified by execution —
|
|
7079
|
+
// not asserted): before F1's fix, the "runtime detector removed" gap here
|
|
7080
|
+
// was real — a rename (case a) or a sibling `name:` clobber (case b) could
|
|
7081
|
+
// reach this loop and land a held role's `.toml` at `workspace-write` with
|
|
7082
|
+
// nothing here to catch it. After F1 (`deriveCodexSandboxMode` now decides
|
|
7083
|
+
// over BOTH the filename stem and the resolved frontmatter `name:`, most
|
|
7084
|
+
// restrictive wins), both cases were re-run end-to-end through this exact
|
|
7085
|
+
// function and the EMITTED ARTIFACT for both is `read-only` — the
|
|
7086
|
+
// dangerous condition no longer produces a wrong artifact, it produces the
|
|
7087
|
+
// SAFE one. A detector guarding a now-fail-safe condition is not
|
|
7088
|
+
// load-bearing, and restoring a throw here would re-break the legitimate
|
|
7089
|
+
// partial-source-dir case CAUSE B removed it for (see above). Regression
|
|
7090
|
+
// coverage for both cases lives in `tests/codex-config.test.cjs` (F1(a)
|
|
7091
|
+
// rename / F1(b) sibling-clobber rows), asserted on the emitted `.toml`'s
|
|
7092
|
+
// `sandbox_mode`, not on the derivation's return value.
|
|
7093
|
+
|
|
6775
7094
|
const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
|
|
6776
7095
|
const agents = [];
|
|
6777
7096
|
|
|
@@ -6800,7 +7119,20 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
6800
7119
|
// CLAUDE.md neutralization via neutralizeAgentReferences(..., 'AGENTS.md').
|
|
6801
7120
|
content = convertClaudeToCodexMarkdown(content);
|
|
6802
7121
|
const { frontmatter } = extractFrontmatterAndBody(content);
|
|
6803
|
-
|
|
7122
|
+
// #3897 security review F1 (blocker, post-merge): this loop used to key
|
|
7123
|
+
// the sandbox/hold decision off ONLY the filename stem while the emitted
|
|
7124
|
+
// `.toml`'s OUTPUT PATH below is keyed off `name` (frontmatter-derived,
|
|
7125
|
+
// attacker-editable) — so a renamed source file, or a sibling file whose
|
|
7126
|
+
// `name:` collides with a held role, could make the decided identity and
|
|
7127
|
+
// the landed artifact disagree, widening a held role's own file to
|
|
7128
|
+
// `workspace-write`. `generateCodexAgentToml` (below) now derives
|
|
7129
|
+
// `sandbox_mode` over BOTH the filename stem it is given AND the
|
|
7130
|
+
// frontmatter `name:` it resolves internally, taking the most
|
|
7131
|
+
// restrictive result — this loop no longer needs to choose one identity
|
|
7132
|
+
// for that call; see `deriveCodexSandboxMode`'s doc in
|
|
7133
|
+
// `codex-agent-toml.cts` for the resolution.
|
|
7134
|
+
const fileStem = file.replace(/\.md$/, '');
|
|
7135
|
+
const name = extractFrontmatterField(frontmatter, 'name') || fileStem;
|
|
6804
7136
|
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
6805
7137
|
|
|
6806
7138
|
agents.push({ name, description: toSingleLine(description) });
|
|
@@ -6822,7 +7154,10 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
6822
7154
|
// #443 — pass unified effort config so model_reasoning_effort in the .toml
|
|
6823
7155
|
// follows the same config-driven precedence as the Claude .md effort key.
|
|
6824
7156
|
const effortCfg = readGsdEffectiveEffortConfig(targetDir);
|
|
6825
|
-
|
|
7157
|
+
// Pass `fileStem`; `generateCodexAgentToml` itself additionally resolves
|
|
7158
|
+
// and folds in the frontmatter `name:` for the sandbox decision (F1
|
|
7159
|
+
// above) — this call site does not need to pass `name` explicitly.
|
|
7160
|
+
const tomlContent = generateCodexAgentToml(fileStem, content, modelOverrides, runtimeResolver, effortCfg, sandboxTier);
|
|
6826
7161
|
// Confine the per-agent write to the agents/ dir itself: a crafted agent
|
|
6827
7162
|
// `name` containing path separators must not escape agents/ (which would let
|
|
6828
7163
|
// it clobber config.toml or write elsewhere under the configHome).
|
|
@@ -7093,7 +7428,8 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverrid
|
|
|
7093
7428
|
|
|
7094
7429
|
if (isAgent && inAgentTools) {
|
|
7095
7430
|
if (trimmed.startsWith('- ')) {
|
|
7096
|
-
|
|
7431
|
+
const tool = runtimeArtifactConversion._decodeToolScalar(trimmed.substring(2));
|
|
7432
|
+
if (tool !== null) agentTools.push(tool);
|
|
7097
7433
|
continue;
|
|
7098
7434
|
}
|
|
7099
7435
|
if (trimmed && !trimmed.startsWith('-')) {
|
|
@@ -7105,8 +7441,12 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverrid
|
|
|
7105
7441
|
if (trimmed.startsWith('tools:')) {
|
|
7106
7442
|
if (isAgent) {
|
|
7107
7443
|
const toolsValue = trimmed.substring(6).trim();
|
|
7108
|
-
|
|
7109
|
-
|
|
7444
|
+
// A comment-only value (`tools: # note`) is not real inline content —
|
|
7445
|
+
// fall through to the block-list scan instead of decoding the
|
|
7446
|
+
// comment as a bogus tool name and dropping the list (mirrors
|
|
7447
|
+
// src/runtime-artifact-conversion.cts's convertClaudeToKiloFrontmatter, #4032).
|
|
7448
|
+
if (toolsValue && !toolsValue.startsWith('#')) {
|
|
7449
|
+
const tools = runtimeArtifactConversion._splitToolScalars(toolsValue).map(runtimeArtifactConversion._decodeToolScalar).filter(tool => tool !== null);
|
|
7110
7450
|
agentTools.push(...tools);
|
|
7111
7451
|
} else {
|
|
7112
7452
|
inAgentTools = true;
|
|
@@ -7172,7 +7512,8 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverrid
|
|
|
7172
7512
|
if (trimmed.startsWith('- ')) {
|
|
7173
7513
|
const tool = trimmed.substring(2).trim();
|
|
7174
7514
|
if (isAgent) {
|
|
7175
|
-
|
|
7515
|
+
const decoded = runtimeArtifactConversion._decodeToolScalar(tool);
|
|
7516
|
+
if (decoded !== null) agentTools.push(decoded);
|
|
7176
7517
|
} else {
|
|
7177
7518
|
allowedTools.push(tool);
|
|
7178
7519
|
}
|
|
@@ -7822,6 +8163,133 @@ function validateHookFields(settings) {
|
|
|
7822
8163
|
*/
|
|
7823
8164
|
const GSD_UNINSTALL_HOOKS = [..._HOOKS_TO_COPY, 'gsd-check-update.cmd'];
|
|
7824
8165
|
|
|
8166
|
+
/**
|
|
8167
|
+
* Whether two paths denote the SAME directory — used to stop a reclaim from
|
|
8168
|
+
* deleting the very root the current install just wrote (#3031).
|
|
8169
|
+
*
|
|
8170
|
+
* A plain `path.resolve` comparison is not enough here, because both roots come
|
|
8171
|
+
* from user-controlled env vars (`KIMI_SHARE_DIR`, `KIMI_CODE_HOME`) and two
|
|
8172
|
+
* different strings routinely name one directory:
|
|
8173
|
+
* - case-insensitive filesystems (macOS, Windows): `~/Kimi` vs `~/kimi`
|
|
8174
|
+
* - symlinks / bind mounts: `~/link-to-kimi` vs the real target
|
|
8175
|
+
* Getting this wrong is not cosmetic — it is the difference between skipping a
|
|
8176
|
+
* reclaim and deleting a live install's own hooks.
|
|
8177
|
+
*
|
|
8178
|
+
* Strategy, cheapest-first: string equality after `resolve`, then identity by
|
|
8179
|
+
* `dev`+`ino` (definitive when both exist and the platform reports them), then
|
|
8180
|
+
* `realpath` string equality (resolves symlinks AND canonicalizes case). Any
|
|
8181
|
+
* rung answering "same" wins; a path that does not exist cannot be the root we
|
|
8182
|
+
* just wrote, so a failed stat simply falls through.
|
|
8183
|
+
*
|
|
8184
|
+
* @returns {boolean} true only when both paths are proven to be one directory.
|
|
8185
|
+
*/
|
|
8186
|
+
function isSameDirectory(a, b) {
|
|
8187
|
+
if (path.resolve(a) === path.resolve(b)) return true;
|
|
8188
|
+
try {
|
|
8189
|
+
const sa = fs.statSync(a);
|
|
8190
|
+
const sb = fs.statSync(b);
|
|
8191
|
+
// `ino` is 0 on some Windows filesystems; only trust a positive match.
|
|
8192
|
+
if (sa.ino && sb.ino && sa.dev === sb.dev && sa.ino === sb.ino) return true;
|
|
8193
|
+
} catch (_) { /* one side missing — fall through to realpath */ }
|
|
8194
|
+
try {
|
|
8195
|
+
return fs.realpathSync.native(a) === fs.realpathSync.native(b);
|
|
8196
|
+
} catch (_) {
|
|
8197
|
+
return false;
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
8200
|
+
|
|
8201
|
+
/**
|
|
8202
|
+
* Remove every GSD-owned artifact from a Kimi hooks root (`~/.kimi` for kimi,
|
|
8203
|
+
* `~/.kimi-code` for kimi-code — resolveKimiHooksTomlDir, #2755): the managed
|
|
8204
|
+
* `[[hooks]]` block in the native config.toml, the hook scripts, hooks/lib/,
|
|
8205
|
+
* and the CommonJS marker at both its current (hooks/) and pre-#2544 (root)
|
|
8206
|
+
* locations.
|
|
8207
|
+
*
|
|
8208
|
+
* This root is Kimi's own native config home — SHARED space that may hold the
|
|
8209
|
+
* user's real config.toml, providers and their own scripts — so only exact
|
|
8210
|
+
* GSD-owned filenames are removed and directories are pruned only when that
|
|
8211
|
+
* removal leaves them empty.
|
|
8212
|
+
*
|
|
8213
|
+
* TWO callers, deliberately one implementation (#3031). `uninstall()` calls it
|
|
8214
|
+
* for the runtime being uninstalled; the opt-in `--reclaim-kimi-legacy` path in
|
|
8215
|
+
* `install()` calls it for the LEGACY `~/.kimi` root a pre-#2755 `--kimi-code`
|
|
8216
|
+
* install orphaned. Duplicating this sequence for the second caller would be
|
|
8217
|
+
* exactly the generative-divergence hazard the repo bans — the reclaim must
|
|
8218
|
+
* remove precisely what a real uninstall removes, forever, by construction.
|
|
8219
|
+
*
|
|
8220
|
+
* @param {string} kimiHooksRoot - Absolute path to the Kimi hooks root.
|
|
8221
|
+
* @returns {number} count of removal steps performed (0 when nothing matched).
|
|
8222
|
+
*/
|
|
8223
|
+
function reclaimKimiHooksRoot(kimiHooksRoot) {
|
|
8224
|
+
let steps = 0;
|
|
8225
|
+
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
8226
|
+
const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath);
|
|
8227
|
+
if (kimiHooksCleanup.changed) {
|
|
8228
|
+
steps++;
|
|
8229
|
+
console.log(` ${green}✓${reset} Removed GSD hooks from ${kimiHooksTomlPath}`);
|
|
8230
|
+
}
|
|
8231
|
+
|
|
8232
|
+
// Kimi's shared hook scripts + CommonJS package.json marker are installed
|
|
8233
|
+
// into this SAME ~/.kimi root (installSharedHooksBundle, install()'s
|
|
8234
|
+
// kimi-hooks-toml branch) rather than under targetDir — mirror steps "4.
|
|
8235
|
+
// Remove GSD hooks" / "5. Remove GSD package.json" below, but scoped to
|
|
8236
|
+
// kimiHooksRoot. ~/.kimi is Kimi's own native config home (shared space —
|
|
8237
|
+
// may hold the user's real config.toml/providers), so only the exact
|
|
8238
|
+
// GSD-owned filenames are removed, and directories are pruned only if left
|
|
8239
|
+
// empty by that removal.
|
|
8240
|
+
const kimiHooksDir = path.join(kimiHooksRoot, 'hooks');
|
|
8241
|
+
if (fs.existsSync(kimiHooksDir)) {
|
|
8242
|
+
let kimiHookCount = 0;
|
|
8243
|
+
for (const hook of GSD_UNINSTALL_HOOKS) {
|
|
8244
|
+
const hookPath = path.join(kimiHooksDir, hook);
|
|
8245
|
+
if (fs.existsSync(hookPath)) {
|
|
8246
|
+
fs.unlinkSync(hookPath);
|
|
8247
|
+
kimiHookCount++;
|
|
8248
|
+
}
|
|
8249
|
+
}
|
|
8250
|
+
if (kimiHookCount > 0) {
|
|
8251
|
+
steps++;
|
|
8252
|
+
console.log(` ${green}✓${reset} Removed ${kimiHookCount} GSD hooks from ${kimiHooksDir}`);
|
|
8253
|
+
}
|
|
8254
|
+
|
|
8255
|
+
const kimiHooksLibDir = path.join(kimiHooksDir, 'lib');
|
|
8256
|
+
if (fs.existsSync(kimiHooksLibDir)) {
|
|
8257
|
+
let removedKimiLibFiles = 0;
|
|
8258
|
+
for (const file of GSD_HOOK_LIB_FILES) {
|
|
8259
|
+
try {
|
|
8260
|
+
fs.unlinkSync(path.join(kimiHooksLibDir, file));
|
|
8261
|
+
removedKimiLibFiles++;
|
|
8262
|
+
} catch (_) { /* best-effort */ }
|
|
8263
|
+
}
|
|
8264
|
+
try { fs.rmdirSync(kimiHooksLibDir); } catch (_) { /* not empty or other error — leave it */ }
|
|
8265
|
+
if (removedKimiLibFiles > 0) {
|
|
8266
|
+
steps++;
|
|
8267
|
+
console.log(` ${green}✓${reset} Removed ${removedKimiLibFiles} hooks/lib/ helper(s) from ${kimiHooksLibDir}`);
|
|
8268
|
+
}
|
|
8269
|
+
}
|
|
8270
|
+
|
|
8271
|
+
// #2544: the marker now lives inside kimi's hooks/ dir — remove it
|
|
8272
|
+
// before the emptiness check below, or the dir would never prune.
|
|
8273
|
+
if (removeCommonJsMarker(kimiHooksDir)) {
|
|
8274
|
+
steps++;
|
|
8275
|
+
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksDir}`);
|
|
8276
|
+
}
|
|
8277
|
+
|
|
8278
|
+
try {
|
|
8279
|
+
if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir);
|
|
8280
|
+
} catch (_) { /* not empty — leave it */ }
|
|
8281
|
+
}
|
|
8282
|
+
|
|
8283
|
+
// Retire the pre-#2544 marker at kimi's root (~/.kimi), where the bundle
|
|
8284
|
+
// used to write it. Exact content match — a user's own package.json in
|
|
8285
|
+
// kimi's native config home is never touched.
|
|
8286
|
+
if (removeCommonJsMarker(kimiHooksRoot)) {
|
|
8287
|
+
steps++;
|
|
8288
|
+
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot} (pre-#2544 marker)`);
|
|
8289
|
+
}
|
|
8290
|
+
return steps;
|
|
8291
|
+
}
|
|
8292
|
+
|
|
7825
8293
|
/**
|
|
7826
8294
|
* Uninstall GSD from the specified directory for a specific runtime
|
|
7827
8295
|
* Removes only GSD-specific files/directories, preserves user content
|
|
@@ -8019,72 +8487,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8019
8487
|
// cleanup can't be driven by anything under targetDir the way every other
|
|
8020
8488
|
// hook surface above is.
|
|
8021
8489
|
if (resolveInstallPlan(runtime).hooksSurface === 'kimi-hooks-toml') {
|
|
8022
|
-
|
|
8023
|
-
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
8024
|
-
const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath);
|
|
8025
|
-
if (kimiHooksCleanup.changed) {
|
|
8026
|
-
removedCount++;
|
|
8027
|
-
console.log(` ${green}✓${reset} Removed GSD hooks from ${kimiHooksTomlPath}`);
|
|
8028
|
-
}
|
|
8029
|
-
|
|
8030
|
-
// Kimi's shared hook scripts + CommonJS package.json marker are installed
|
|
8031
|
-
// into this SAME ~/.kimi root (installSharedHooksBundle, install()'s
|
|
8032
|
-
// kimi-hooks-toml branch) rather than under targetDir — mirror steps "4.
|
|
8033
|
-
// Remove GSD hooks" / "5. Remove GSD package.json" below, but scoped to
|
|
8034
|
-
// kimiHooksRoot. ~/.kimi is Kimi's own native config home (shared space —
|
|
8035
|
-
// may hold the user's real config.toml/providers), so only the exact
|
|
8036
|
-
// GSD-owned filenames are removed, and directories are pruned only if left
|
|
8037
|
-
// empty by that removal.
|
|
8038
|
-
const kimiHooksDir = path.join(kimiHooksRoot, 'hooks');
|
|
8039
|
-
if (fs.existsSync(kimiHooksDir)) {
|
|
8040
|
-
let kimiHookCount = 0;
|
|
8041
|
-
for (const hook of GSD_UNINSTALL_HOOKS) {
|
|
8042
|
-
const hookPath = path.join(kimiHooksDir, hook);
|
|
8043
|
-
if (fs.existsSync(hookPath)) {
|
|
8044
|
-
fs.unlinkSync(hookPath);
|
|
8045
|
-
kimiHookCount++;
|
|
8046
|
-
}
|
|
8047
|
-
}
|
|
8048
|
-
if (kimiHookCount > 0) {
|
|
8049
|
-
removedCount++;
|
|
8050
|
-
console.log(` ${green}✓${reset} Removed ${kimiHookCount} GSD hooks from ${kimiHooksDir}`);
|
|
8051
|
-
}
|
|
8052
|
-
|
|
8053
|
-
const kimiHooksLibDir = path.join(kimiHooksDir, 'lib');
|
|
8054
|
-
if (fs.existsSync(kimiHooksLibDir)) {
|
|
8055
|
-
let removedKimiLibFiles = 0;
|
|
8056
|
-
for (const file of GSD_HOOK_LIB_FILES) {
|
|
8057
|
-
try {
|
|
8058
|
-
fs.unlinkSync(path.join(kimiHooksLibDir, file));
|
|
8059
|
-
removedKimiLibFiles++;
|
|
8060
|
-
} catch (_) { /* best-effort */ }
|
|
8061
|
-
}
|
|
8062
|
-
try { fs.rmdirSync(kimiHooksLibDir); } catch (_) { /* not empty or other error — leave it */ }
|
|
8063
|
-
if (removedKimiLibFiles > 0) {
|
|
8064
|
-
removedCount++;
|
|
8065
|
-
console.log(` ${green}✓${reset} Removed ${removedKimiLibFiles} hooks/lib/ helper(s) from ${kimiHooksLibDir}`);
|
|
8066
|
-
}
|
|
8067
|
-
}
|
|
8068
|
-
|
|
8069
|
-
// #2544: the marker now lives inside kimi's hooks/ dir — remove it
|
|
8070
|
-
// before the emptiness check below, or the dir would never prune.
|
|
8071
|
-
if (removeCommonJsMarker(kimiHooksDir)) {
|
|
8072
|
-
removedCount++;
|
|
8073
|
-
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksDir}`);
|
|
8074
|
-
}
|
|
8075
|
-
|
|
8076
|
-
try {
|
|
8077
|
-
if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir);
|
|
8078
|
-
} catch (_) { /* not empty — leave it */ }
|
|
8079
|
-
}
|
|
8080
|
-
|
|
8081
|
-
// Retire the pre-#2544 marker at kimi's root (~/.kimi), where the bundle
|
|
8082
|
-
// used to write it. Exact content match — a user's own package.json in
|
|
8083
|
-
// kimi's native config home is never touched.
|
|
8084
|
-
if (removeCommonJsMarker(kimiHooksRoot)) {
|
|
8085
|
-
removedCount++;
|
|
8086
|
-
console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot} (pre-#2544 marker)`);
|
|
8087
|
-
}
|
|
8490
|
+
removedCount += reclaimKimiHooksRoot(resolveKimiHooksTomlDir({ runtime }));
|
|
8088
8491
|
}
|
|
8089
8492
|
|
|
8090
8493
|
// 1b. Non-layout Copilot side-effect: copilot-instructions.md cleanup
|
|
@@ -8698,17 +9101,29 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8698
9101
|
);
|
|
8699
9102
|
if (settings.permissions.allow.length !== before) {
|
|
8700
9103
|
permissionsModified = true;
|
|
9104
|
+
// #4221: an array this filter emptied was GSD-only \u2014 remove the
|
|
9105
|
+
// key rather than leave an empty array behind (Antigravity symmetry).
|
|
9106
|
+
if (settings.permissions.allow.length === 0) {
|
|
9107
|
+
delete settings.permissions.allow;
|
|
9108
|
+
}
|
|
8701
9109
|
}
|
|
8702
9110
|
}
|
|
8703
9111
|
if (Array.isArray(settings.permissions.deny)) {
|
|
8704
9112
|
const before = settings.permissions.deny.length;
|
|
9113
|
+
// #4221: the deny rules are retired, so this is a legacy-only filter.
|
|
8705
9114
|
settings.permissions.deny = settings.permissions.deny.filter(
|
|
8706
|
-
(e) => !
|
|
9115
|
+
(e) => !GSD_CLAUDE_LEGACY_DENY_PERMISSIONS.includes(e)
|
|
8707
9116
|
);
|
|
8708
9117
|
if (settings.permissions.deny.length !== before) {
|
|
8709
9118
|
permissionsModified = true;
|
|
9119
|
+
if (settings.permissions.deny.length === 0) {
|
|
9120
|
+
delete settings.permissions.deny;
|
|
9121
|
+
}
|
|
8710
9122
|
}
|
|
8711
9123
|
}
|
|
9124
|
+
if (permissionsModified && Object.keys(settings.permissions).length === 0) {
|
|
9125
|
+
delete settings.permissions;
|
|
9126
|
+
}
|
|
8712
9127
|
if (permissionsModified) {
|
|
8713
9128
|
settingsModified = true;
|
|
8714
9129
|
console.log(` ${green}✓${reset} Removed GSD permissions from settings.json`);
|
|
@@ -9450,7 +9865,14 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9450
9865
|
const resolvedScope = options.scope === 'local' ? 'local' : 'global';
|
|
9451
9866
|
const codexSkillsDir = _resolveSkillsRootDir(runtime, configDir, resolvedScope);
|
|
9452
9867
|
const codexSkillsManifestPrefix = _hostBehaviors(runtime).skillsManifestPrefix || 'skills/';
|
|
9453
|
-
|
|
9868
|
+
// #3738: resolve the ACTUAL agents-install dir honoring an agents-kind `home`
|
|
9869
|
+
// override (antigravity global → $HOME/.gemini/config/agents), mirroring
|
|
9870
|
+
// _resolveSkillsRootDir for skills. Hardcoding configDir/agents left the
|
|
9871
|
+
// manifest blind to the whole agents surface the moment the override landed —
|
|
9872
|
+
// no drift detection, no patch backup. Falls back to <configDir>/agents.
|
|
9873
|
+
const agentsDir = _kindDestDirSafe(runtime, configDir, resolvedScope, 'agents')
|
|
9874
|
+
|| _kindDestDirSafe(runtime, configDir, resolvedScope, 'kimi-agents')
|
|
9875
|
+
|| path.join(configDir, 'agents');
|
|
9454
9876
|
const manifest = {
|
|
9455
9877
|
// Schema version of this DOCUMENT (#2872) — distinct from `version`
|
|
9456
9878
|
// below, which is the GSD package version. Absent ⇒ a pre-#2872 (v1)
|
|
@@ -9763,19 +10185,57 @@ function saveLocalPatches(configDir, pristineCtx) {
|
|
|
9763
10185
|
const modified = [];
|
|
9764
10186
|
const pristineHashes = {};
|
|
9765
10187
|
|
|
10188
|
+
// #4086: skills/ manifest keys may live OUTSIDE configDir at the runtime's
|
|
10189
|
+
// ACTUAL skills root — codex global installs to $HOME/.agents/skills (the
|
|
10190
|
+
// ADR-1239 skills-kind `home` override), which writeManifest() already
|
|
10191
|
+
// hashes from via _resolveSkillsRootDir (#2088/#3738). Resolving every key
|
|
10192
|
+
// against configDir alone made every skills/ key miss here, so user
|
|
10193
|
+
// modifications to Codex skills were never hash-compared, never backed up,
|
|
10194
|
+
// and were silently overwritten by the next update. configDir stays FIRST
|
|
10195
|
+
// (Postel: runtimes whose skills genuinely live under configDir resolve
|
|
10196
|
+
// byte-identically to before); the skills root is a fallback, only for keys
|
|
10197
|
+
// under the SAME descriptor-driven manifest prefix writeManifest uses, and
|
|
10198
|
+
// only when that root resolves outside configDir. Containment + symlink
|
|
10199
|
+
// guards apply to the alternate root too (resolveInstallRelativePath).
|
|
10200
|
+
const patchRuntime = (pristineCtx && pristineCtx.runtime) || manifest.runtime || null;
|
|
10201
|
+
const patchScope = manifest.scope === 'local' ? 'local' : 'global';
|
|
10202
|
+
let skillsRedirect = null;
|
|
10203
|
+
if (patchRuntime) {
|
|
10204
|
+
const skillsRoot = _resolveSkillsRootDir(patchRuntime, configDir, patchScope);
|
|
10205
|
+
const resolvedConfig = path.resolve(configDir);
|
|
10206
|
+
if (
|
|
10207
|
+
skillsRoot &&
|
|
10208
|
+
skillsRoot !== resolvedConfig &&
|
|
10209
|
+
!skillsRoot.startsWith(resolvedConfig + path.sep)
|
|
10210
|
+
) {
|
|
10211
|
+
const prefix = _hostBehaviors(patchRuntime).skillsManifestPrefix || 'skills/';
|
|
10212
|
+
skillsRedirect = { root: skillsRoot, prefix };
|
|
10213
|
+
}
|
|
10214
|
+
}
|
|
10215
|
+
|
|
9766
10216
|
for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
|
|
9767
10217
|
const safeRef = resolveInstallRelativePath(configDir, relPath);
|
|
9768
10218
|
if (!safeRef) continue;
|
|
9769
10219
|
const { relPath: safeRelPath, fullPath } = safeRef;
|
|
9770
|
-
|
|
9771
|
-
|
|
10220
|
+
let installedPath = fullPath;
|
|
10221
|
+
if (!fs.existsSync(installedPath) && skillsRedirect && safeRelPath.startsWith(skillsRedirect.prefix)) {
|
|
10222
|
+
const altRef = resolveInstallRelativePath(
|
|
10223
|
+
skillsRedirect.root,
|
|
10224
|
+
safeRelPath.slice(skillsRedirect.prefix.length)
|
|
10225
|
+
);
|
|
10226
|
+
if (altRef && fs.existsSync(altRef.fullPath)) {
|
|
10227
|
+
installedPath = altRef.fullPath;
|
|
10228
|
+
}
|
|
10229
|
+
}
|
|
10230
|
+
if (!fs.existsSync(installedPath)) continue;
|
|
10231
|
+
const currentHash = fileHash(installedPath);
|
|
9772
10232
|
if (currentHash !== originalHash) {
|
|
9773
10233
|
// Back up the user's modified version
|
|
9774
10234
|
const backupRef = resolveInstallRelativePath(patchesDir, safeRelPath);
|
|
9775
10235
|
if (!backupRef) continue;
|
|
9776
10236
|
const backupPath = backupRef.fullPath;
|
|
9777
10237
|
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
9778
|
-
fs.copyFileSync(
|
|
10238
|
+
fs.copyFileSync(installedPath, backupPath);
|
|
9779
10239
|
modified.push(safeRelPath);
|
|
9780
10240
|
pristineHashes[safeRelPath] = originalHash;
|
|
9781
10241
|
}
|
|
@@ -10080,6 +10540,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10080
10540
|
? process.cwd()
|
|
10081
10541
|
: path.join(process.cwd(), dirName);
|
|
10082
10542
|
|
|
10543
|
+
// #3664: a --config-dir destination holding foreign agent files gets an
|
|
10544
|
+
// explicit install-time warning — never a silent Claude-shaped emit.
|
|
10545
|
+
if (isGlobal) {
|
|
10546
|
+
warnIfForeignAgentDest(runtime, targetDir, _installScopeId, Boolean(explicitConfigDir));
|
|
10547
|
+
}
|
|
10548
|
+
|
|
10083
10549
|
// #2875 (#1874-F19 anti-inertness, test-matrix C7): recover any user
|
|
10084
10550
|
// artifact orphaned by a PRIOR install run that died between staging and
|
|
10085
10551
|
// its own restore/discard, BEFORE this run's own preserve step stages
|
|
@@ -10158,7 +10624,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10158
10624
|
// one) is honored on every profile, `full` included (#2322 blocker 2).
|
|
10159
10625
|
const _commandsDir = path.join(src, 'commands', 'gsd');
|
|
10160
10626
|
const _skillsManifest = _isCoreProfileAlias ? new Map() : loadSkillsManifest(_commandsDir);
|
|
10161
|
-
|
|
10627
|
+
let _resolvedProfile = resolveProfile({
|
|
10162
10628
|
modes: [_activeProfileName],
|
|
10163
10629
|
manifest: _skillsManifest,
|
|
10164
10630
|
registry: _installedCapabilityRegistry,
|
|
@@ -10509,6 +10975,28 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10509
10975
|
return Array.isArray(scopeLayout) && scopeLayout.length > 0;
|
|
10510
10976
|
})();
|
|
10511
10977
|
|
|
10978
|
+
// Install the distribution-owned gsd-core tree before layout materialization.
|
|
10979
|
+
// installRuntimeArtifacts then provisions the durable Runtime Surface corpus
|
|
10980
|
+
// exactly once into this final tree instead of having that corpus overwritten
|
|
10981
|
+
// by a later whole-tree copy and needing a second provisioning pass.
|
|
10982
|
+
const skillSrc = path.join(src, 'gsd-core');
|
|
10983
|
+
const skillDest = path.join(targetDir, 'gsd-core');
|
|
10984
|
+
const _gsdArtifactsStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
10985
|
+
if (_gsdArtifactsStagingRoot === null) {
|
|
10986
|
+
console.warn(` ${yellow}!${reset} Skipping gsd-core/${USER_OWNED_ARTIFACTS.join(', gsd-core/')} preservation (staging unavailable) — it will be lost if present.`);
|
|
10987
|
+
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
|
|
10988
|
+
} else {
|
|
10989
|
+
const stagedGsdArtifacts = stageUserArtifacts(skillDest, USER_OWNED_ARTIFACTS, _gsdArtifactsStagingRoot);
|
|
10990
|
+
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
|
|
10991
|
+
restoreStagedUserArtifacts(skillDest, stagedGsdArtifacts);
|
|
10992
|
+
discardStagedUserArtifacts(stagedGsdArtifacts);
|
|
10993
|
+
}
|
|
10994
|
+
if (verifyInstalled(skillDest, 'gsd-core')) {
|
|
10995
|
+
console.log(` ${green}✓${reset} Installed workflow assets`);
|
|
10996
|
+
} else {
|
|
10997
|
+
failures.push('gsd-core');
|
|
10998
|
+
}
|
|
10999
|
+
|
|
10512
11000
|
// #2624: write the .gsd-source marker. Extracted from its former late position so it can be
|
|
10513
11001
|
// called BEFORE staging reads the marker (see the call site below). Scoped to the Claude-global
|
|
10514
11002
|
// layout (issue #1477) — the only install path that ships the skills layout without a
|
|
@@ -10525,6 +11013,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10525
11013
|
// ADR-1239 Phase B write-confinement: the descriptor-sourced marker filename
|
|
10526
11014
|
// must resolve under targetDir (parity with the other descriptor-driven writes).
|
|
10527
11015
|
const _markerPath = assertDestWithinConfigHome(targetDir, _hostBehaviors(runtime).sourceMarkerFile);
|
|
11016
|
+
if (hasExistingSymlinkBetween(path.resolve(targetDir), _markerPath, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
11017
|
+
throw new Error(`compatibility marker "${_markerPath}" contains an untrusted symlink`);
|
|
11018
|
+
}
|
|
10528
11019
|
fs.writeFileSync(_markerPath, gsdSourceCommands + '\n', 'utf8');
|
|
10529
11020
|
} catch (err) {
|
|
10530
11021
|
// Non-fatal: install proceeds. But on the Claude-global layout walk-up
|
|
@@ -10551,6 +11042,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10551
11042
|
if (_isSkillsRuntime) {
|
|
10552
11043
|
// Layout-driven install for skills-based runtimes (full and minimal modes)
|
|
10553
11044
|
const scope = _installScopeId;
|
|
11045
|
+
// Preserve an existing Runtime Surface selection during upgrades. The
|
|
11046
|
+
// installer refreshes the source corpus first, then emits exactly the
|
|
11047
|
+
// already-committed selection instead of widening it to the base profile.
|
|
11048
|
+
const _committedSurface = readSurface(targetDir);
|
|
11049
|
+
if (_committedSurface) {
|
|
11050
|
+
_resolvedProfile = resolveSurface(
|
|
11051
|
+
targetDir,
|
|
11052
|
+
loadSkillsManifest(_commandsDir),
|
|
11053
|
+
undefined,
|
|
11054
|
+
_installedCapabilityRegistry,
|
|
11055
|
+
_committedSurface,
|
|
11056
|
+
);
|
|
11057
|
+
}
|
|
10554
11058
|
// ADR-1239 upgrade 3 / #2088: a kind may declare an alternate install `home`
|
|
10555
11059
|
// (e.g. Codex skills -> $HOME/.agents/skills) instead of the runtime's normal
|
|
10556
11060
|
// configDir. Resolve the ACTUAL on-disk skills root here, descriptor-driven
|
|
@@ -10843,36 +11347,6 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10843
11347
|
_installNativePluginIfDeclared(runtime, targetDir, _hostBehaviors(runtime), src);
|
|
10844
11348
|
}
|
|
10845
11349
|
|
|
10846
|
-
// Copy gsd-core skill with path replacement
|
|
10847
|
-
// Stage user-generated files DURABLY to disk before the wipe-and-copy so
|
|
10848
|
-
// they survive re-install even if the process dies mid-copy (#2875 /
|
|
10849
|
-
// #1874-F19) — copyWithPathReplacement wipes and recursively re-copies the
|
|
10850
|
-
// entire gsd-core/ tree, the single longest operation in the install, on
|
|
10851
|
-
// the path every user takes (40-design.md "Site 4 is far worse...").
|
|
10852
|
-
const skillSrc = path.join(src, 'gsd-core');
|
|
10853
|
-
const skillDest = path.join(targetDir, 'gsd-core');
|
|
10854
|
-
// #2875 defect fix: this IS the mainline install step (installing
|
|
10855
|
-
// gsd-core/ itself) — unlike the optional legacy-cleanup blocks above,
|
|
10856
|
-
// install must still be able to proceed and actually write gsd-core/ even
|
|
10857
|
-
// when the staging root cannot be resolved. Degrade by skipping ONLY the
|
|
10858
|
-
// USER_OWNED_ARTIFACTS preserve/restore wrapper around the copy (warn),
|
|
10859
|
-
// never the copy itself.
|
|
10860
|
-
const _gsdArtifactsStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
|
|
10861
|
-
if (_gsdArtifactsStagingRoot === null) {
|
|
10862
|
-
console.warn(` ${yellow}!${reset} Skipping gsd-core/${USER_OWNED_ARTIFACTS.join(', gsd-core/')} preservation (staging unavailable) — it will be lost if present.`);
|
|
10863
|
-
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
|
|
10864
|
-
} else {
|
|
10865
|
-
const stagedGsdArtifacts = stageUserArtifacts(skillDest, USER_OWNED_ARTIFACTS, _gsdArtifactsStagingRoot);
|
|
10866
|
-
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
|
|
10867
|
-
restoreStagedUserArtifacts(skillDest, stagedGsdArtifacts);
|
|
10868
|
-
discardStagedUserArtifacts(stagedGsdArtifacts);
|
|
10869
|
-
}
|
|
10870
|
-
if (verifyInstalled(skillDest, 'gsd-core')) {
|
|
10871
|
-
console.log(` ${green}✓${reset} Installed workflow assets`);
|
|
10872
|
-
} else {
|
|
10873
|
-
failures.push('gsd-core');
|
|
10874
|
-
}
|
|
10875
|
-
|
|
10876
11350
|
// #2624: the .gsd-source marker is now written by _writeGsdSourceMarker()
|
|
10877
11351
|
// BEFORE staging reads it (see the early call above the _isSkillsRuntime
|
|
10878
11352
|
// block). The former write lived here — AFTER staging — which on an upgrade
|
|
@@ -10960,7 +11434,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10960
11434
|
} else if (_isSkillsRuntime) {
|
|
10961
11435
|
console.log(` ${dim}↳${reset} Agents installed via descriptor-driven layout (${runtime})`);
|
|
10962
11436
|
} else {
|
|
10963
|
-
const
|
|
11437
|
+
const _standaloneProjectDir = isGlobal ? process.cwd() : targetDir;
|
|
11438
|
+
const _standaloneAgentsResult = installAgentsKindStandalone(runtime, targetDir, _installScopeId, _resolvedProfile, pathPrefix, getCommitAttribution, _installedCapabilityRegistry, _standaloneProjectDir);
|
|
10964
11439
|
if (_standaloneAgentsResult) {
|
|
10965
11440
|
// #2875 defect fix: installAgentsKindStandalone now returns `null`
|
|
10966
11441
|
// (rather than a truthy result pointing at an empty destDir) whenever a
|
|
@@ -11374,10 +11849,21 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11374
11849
|
// abort a successful install — log a warning and continue.
|
|
11375
11850
|
// install() is never reached in --dry-run mode (the early-exit at the CLI
|
|
11376
11851
|
// dispatch handles preview), so cleanup here always applies for real.
|
|
11377
|
-
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11852
|
+
//
|
|
11853
|
+
// #3799: when --config-dir redirected the install, the scan is SCOPED to
|
|
11854
|
+
// that destination ([targetDir]) — the default home's live install must
|
|
11855
|
+
// never be planned for removal from a sandboxed install. --no-legacy-cleanup
|
|
11856
|
+
// skips the scan entirely.
|
|
11857
|
+
const skipNoLegacyCleanup = parseNoLegacyCleanupArg();
|
|
11858
|
+
const legacyCleanupScope = (explicitConfigDir !== null && isGlobal)
|
|
11859
|
+
? [targetDir]
|
|
11860
|
+
: undefined;
|
|
11861
|
+
if (!skipNoLegacyCleanup) {
|
|
11862
|
+
try {
|
|
11863
|
+
cleanupLegacyGsdCc({ dryRun: false, ...(legacyCleanupScope ? { configDirs: legacyCleanupScope } : {}) });
|
|
11864
|
+
} catch (cleanupErr) {
|
|
11865
|
+
console.warn(` ${yellow}Warning: legacy cleanup failed: ${cleanupErr.message}${reset}`);
|
|
11866
|
+
}
|
|
11381
11867
|
}
|
|
11382
11868
|
|
|
11383
11869
|
if (failures.length > 0) {
|
|
@@ -11480,7 +11966,20 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11480
11966
|
// #3245 CR finding 2 — any throw in the pre-config install operations (skills copy,
|
|
11481
11967
|
// agents copy, VERSION write, manifest write, etc.) triggers the Codex pre-config
|
|
11482
11968
|
// rollback so the caller is never left in a partially-installed state.
|
|
11483
|
-
rollbackInstallerMigrations()
|
|
11969
|
+
// (The second, identical rollbackInstallerMigrations() that used to sit here was
|
|
11970
|
+
// a duplicate of the line above, not a second phase — removed in #3725 review.)
|
|
11971
|
+
// #3712 — the test-home guard refuses before any LAYOUT-DRIVEN write, so no
|
|
11972
|
+
// gsd-* directory in the skills root has been touched and there is nothing
|
|
11973
|
+
// there to undo. (Legacy install migrations DO run first; that is why the
|
|
11974
|
+
// rollbackInstallerMigrations() calls above still execute, and why the one
|
|
11975
|
+
// migration that can reach a `home` override carries its own assertion.)
|
|
11976
|
+
// Running the codex rollback anyway would delete and recreate every
|
|
11977
|
+
// snapshotted gsd-* directory in the resolved skills root, which for an
|
|
11978
|
+
// un-sandboxed codex install IS the real ~/.agents/skills: the guard's own
|
|
11979
|
+
// refusal would provoke the mutation it exists to prevent. This is the only
|
|
11980
|
+
// _codexPreConfigRollback() call site, and applySurface/uninstall cannot
|
|
11981
|
+
// reach it. Every other error still rolls back. Found by review, not by CI.
|
|
11982
|
+
if (isTestHomeGuardRefusal(_earlyInstallErr)) throw _earlyInstallErr;
|
|
11484
11983
|
if (_codexPreConfigRollback) {
|
|
11485
11984
|
_codexPreConfigRollback();
|
|
11486
11985
|
}
|
|
@@ -11686,8 +12185,18 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11686
12185
|
// require()s managed-hooks-registry.cjs for MANAGED_HOOKS — so all four must be
|
|
11687
12186
|
// installed/refreshed together for every profile, or Codex is wired to a dependency
|
|
11688
12187
|
// chain the same installer never delivers.
|
|
11689
|
-
// We deliberately do *not* copy gsd-graphify-update.sh
|
|
11690
|
-
//
|
|
12188
|
+
// We deliberately do *not* copy gsd-graphify-update.sh for Codex in this
|
|
12189
|
+
// change (graphify auto-update support for Codex is out of scope for #3579).
|
|
12190
|
+
// hooks/lib/ WAS excluded here for the same reason, and that stopped being
|
|
12191
|
+
// correct when #3911 (2ea5efc15) gave gsd-context-monitor.js a real
|
|
12192
|
+
// `require('./lib/hook-exit.js')`: the allowlist below is flat and never
|
|
12193
|
+
// recursed, so the hook shipped without its helper and died with
|
|
12194
|
+
// MODULE_NOT_FOUND at load, before its own try/catch, on every registered
|
|
12195
|
+
// event (#4087, #4098). The libs are now derived from what the staged
|
|
12196
|
+
// scripts actually require rather than hand-listed — see the
|
|
12197
|
+
// stageTransitiveHookLibs call after the copy loop. The #3579 boundary is
|
|
12198
|
+
// preserved: helpers no staged Codex hook requires (graphify tooling among
|
|
12199
|
+
// them) are still not shipped.
|
|
11691
12200
|
const CODEX_HOOKS_TO_COPY = [
|
|
11692
12201
|
'gsd-check-update.js',
|
|
11693
12202
|
'gsd-check-update-worker.js',
|
|
@@ -11703,6 +12212,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11703
12212
|
// is not the same as an allowlisted file landing in it — see the marker
|
|
11704
12213
|
// gate below.
|
|
11705
12214
|
let codexStagedHooks = false;
|
|
12215
|
+
// The entries THIS invocation actually staged. Seeding the lib scan from
|
|
12216
|
+
// `existsSync` over the destination instead would also pick up a file
|
|
12217
|
+
// left by a PREVIOUS install whose source is no longer staged — e.g. a
|
|
12218
|
+
// name dropped from the allowlist — and derive helpers for a hook that is
|
|
12219
|
+
// no longer shipped (review of #4087).
|
|
12220
|
+
const codexStagedEntries = [];
|
|
11706
12221
|
for (const entry of fs.readdirSync(codexHooksSrc)) {
|
|
11707
12222
|
if (!CODEX_HOOKS_TO_COPY.includes(entry)) continue;
|
|
11708
12223
|
const srcFile = path.join(codexHooksSrc, entry);
|
|
@@ -11737,8 +12252,43 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11737
12252
|
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows */ }
|
|
11738
12253
|
}
|
|
11739
12254
|
codexStagedHooks = true;
|
|
12255
|
+
codexStagedEntries.push(entry);
|
|
12256
|
+
}
|
|
12257
|
+
// Stage the hooks/lib/ helpers the staged scripts require, transitively
|
|
12258
|
+
// (#4087, #4098). Shares writeCursorHooksJson's walker rather than a
|
|
12259
|
+
// second copy: both reduced bundles hand-pick SCRIPTS, and the identical
|
|
12260
|
+
// MODULE_NOT_FOUND was already fixed once for Cursor in 704859e9c. A flat
|
|
12261
|
+
// list of today's three helpers would re-break the next time a
|
|
12262
|
+
// Codex-bundled hook grows a lib dependency, which is exactly how this
|
|
12263
|
+
// regressed. Gated on codexStagedHooks for the same reason the CommonJS
|
|
12264
|
+
// marker below is: hooks/ is shared space, and staging nothing must not
|
|
12265
|
+
// leave a GSD-owned lib/ behind in a directory GSD created but did not
|
|
12266
|
+
// fill (#2544). Seeded from the copies staged by THIS invocation, so the
|
|
12267
|
+
// scan sees the same bytes Node will load and never derives helpers for a
|
|
12268
|
+
// hook left behind by an earlier install.
|
|
12269
|
+
let codexStagedLibs = [];
|
|
12270
|
+
if (codexStagedHooks) {
|
|
12271
|
+
codexStagedLibs = hooksSurface.stageTransitiveHookLibs({
|
|
12272
|
+
seedSources: codexStagedEntries
|
|
12273
|
+
.map((entry) => fs.readFileSync(path.join(codexHooksDest, entry), 'utf8')),
|
|
12274
|
+
srcLibDir: path.join(codexHooksSrc, 'lib'),
|
|
12275
|
+
destLibDir: path.join(codexHooksDest, 'lib'),
|
|
12276
|
+
runtimeLabel: 'Codex',
|
|
12277
|
+
// Same substitutions the .js branch above applies to hook scripts, so
|
|
12278
|
+
// a helper that ever gains a runtime path or version token is
|
|
12279
|
+
// rewritten identically instead of shipping a Claude-shaped path.
|
|
12280
|
+
// No-ops on today's helpers, which carry neither.
|
|
12281
|
+
transform: (content) => content
|
|
12282
|
+
.replace(/'\.claude'/g, configDirReplacement)
|
|
12283
|
+
.replace(/\/\.claude\//g, `/${getDirName(runtime)}/`)
|
|
12284
|
+
.replace(/\.claude\//g, `${getDirName(runtime)}/`)
|
|
12285
|
+
.replace(/\{\{GSD_VERSION\}\}/g, pkg.version),
|
|
12286
|
+
});
|
|
11740
12287
|
}
|
|
11741
12288
|
console.log(` ${green}✓${reset} Installed hooks (Codex)`);
|
|
12289
|
+
if (codexStagedLibs.length > 0) {
|
|
12290
|
+
console.log(` ${green}✓${reset} Installed hooks/lib/ helpers (${codexStagedLibs.join(', ')})`);
|
|
12291
|
+
}
|
|
11742
12292
|
// #2717: write the CommonJS marker into hooks/ alongside the staged .js
|
|
11743
12293
|
// scripts. Codex is excluded from installSharedHooksBundle by the
|
|
11744
12294
|
// !isCodex gate, so it never received the marker the shared-bundle path
|
|
@@ -12024,6 +12574,44 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12024
12574
|
if (kimiHooksResult.changed) {
|
|
12025
12575
|
console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
|
|
12026
12576
|
}
|
|
12577
|
+
|
|
12578
|
+
// #3031: opt-in reclaim of the pre-#2755 legacy root. Runs LAST in this
|
|
12579
|
+
// branch so the kimi-code install above is already complete and durable —
|
|
12580
|
+
// a reclaim can only ever remove, never leave the install half-written.
|
|
12581
|
+
//
|
|
12582
|
+
// Gated on `runtime === 'kimi-code'`: a `--kimi` install resolves this
|
|
12583
|
+
// very same `~/.kimi` as its own hooks root, so reclaiming there would
|
|
12584
|
+
// delete the hooks it just wrote. The flag is silently inert for kimi
|
|
12585
|
+
// rather than an error — `--all` passes every runtime through this branch,
|
|
12586
|
+
// and one opt-in flag must not fail an otherwise valid multi-runtime run.
|
|
12587
|
+
//
|
|
12588
|
+
// ALSO gated on kimi NOT being installed by this same invocation. The
|
|
12589
|
+
// flag asserts "I only use Kimi Code"; `--all`, or an explicit `--kimi
|
|
12590
|
+
// --kimi-code`, falsifies that outright. Both orderings put `kimi` BEFORE
|
|
12591
|
+
// `kimi-code` (selectRuntimesFromArgs), so without this guard the run
|
|
12592
|
+
// installs Kimi CLI's hooks and then deletes them moments later — the run
|
|
12593
|
+
// reports success and the user is left with the very breakage the opt-in
|
|
12594
|
+
// exists to prevent. Verified reproducible before this guard existed.
|
|
12595
|
+
const kimiInstalledThisRun = selectedRuntimes.includes('kimi');
|
|
12596
|
+
if (hasReclaimKimiLegacy && runtime === 'kimi-code' && kimiInstalledThisRun) {
|
|
12597
|
+
console.log(` ${dim}•${reset} Skipped --reclaim-kimi-legacy: this run also installs --kimi, so ${resolveKimiHooksTomlDir({ runtime: 'kimi' })} is a live Kimi CLI install`);
|
|
12598
|
+
} else if (hasReclaimKimiLegacy && runtime === 'kimi-code') {
|
|
12599
|
+
const legacyKimiRoot = resolveKimiHooksTomlDir({ runtime: 'kimi' });
|
|
12600
|
+
// Both roots honor their own env override (KIMI_SHARE_DIR /
|
|
12601
|
+
// KIMI_CODE_HOME). A user who points both at ONE directory collapses
|
|
12602
|
+
// "the legacy root" onto "the root this install just wrote", and an
|
|
12603
|
+
// unguarded reclaim would delete its own output. isSameDirectory compares
|
|
12604
|
+
// the DIRECTORIES, not the strings — case-insensitive filesystems and
|
|
12605
|
+
// symlinked aliases both name one dir with two spellings.
|
|
12606
|
+
if (isSameDirectory(legacyKimiRoot, kimiHooksRoot)) {
|
|
12607
|
+
console.log(` ${dim}•${reset} Skipped --reclaim-kimi-legacy: ${legacyKimiRoot} is this install's own hooks root`);
|
|
12608
|
+
} else {
|
|
12609
|
+
const reclaimed = reclaimKimiHooksRoot(legacyKimiRoot);
|
|
12610
|
+
console.log(reclaimed > 0
|
|
12611
|
+
? ` ${green}✓${reset} Reclaimed ${reclaimed} orphaned GSD artifact group(s) from ${legacyKimiRoot} (pre-#2755)`
|
|
12612
|
+
: ` ${dim}•${reset} No orphaned GSD artifacts found in ${legacyKimiRoot}`);
|
|
12613
|
+
}
|
|
12614
|
+
}
|
|
12027
12615
|
}
|
|
12028
12616
|
|
|
12029
12617
|
// ADR-1239 / #2100 Stage 2: Windsurf's own independent hooksSurface —
|
|
@@ -12117,9 +12705,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12117
12705
|
);
|
|
12118
12706
|
const hasGsdStatusline = sharedRaw.statusLine && sharedRaw.statusLine.command &&
|
|
12119
12707
|
isManagedHookCommand(sharedRaw.statusLine.command, { surface: 'settings-json' });
|
|
12120
|
-
|
|
12708
|
+
const needsMigration = hasGsdHooks || hasGsdStatusline;
|
|
12709
|
+
// readSettings returns null ONLY for an unparseable file — its documented
|
|
12710
|
+
// "preserve existing, don't touch" signal. Stand the WHOLE migration down
|
|
12711
|
+
// in that case: skipping just the local merge while still stripping the
|
|
12712
|
+
// shared file below would destroy the GSD entries outright instead of
|
|
12713
|
+
// relocating them. Leaving both files untouched lets the migration retry
|
|
12714
|
+
// once the user repairs the local file.
|
|
12715
|
+
const localRaw = needsMigration ? readSettings(settingsPath) : null;
|
|
12716
|
+
if (needsMigration && localRaw === null) {
|
|
12717
|
+
console.log(' ' + yellow + 'i' + reset + ' Skipping #338 migration — ' + settingsFileName +
|
|
12718
|
+
' could not be parsed. Your existing settings are preserved.');
|
|
12719
|
+
} else if (needsMigration) {
|
|
12121
12720
|
// Merge GSD entries into settings.local.json
|
|
12122
|
-
const localRaw = readSettings(settingsPath) || {};
|
|
12123
12721
|
if (hasGsdStatusline && !localRaw.statusLine) {
|
|
12124
12722
|
localRaw.statusLine = sharedRaw.statusLine;
|
|
12125
12723
|
}
|
|
@@ -12179,16 +12777,22 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12179
12777
|
if (rawSettings === null) {
|
|
12180
12778
|
console.log(' ' + yellow + 'i' + reset + ' Skipping settings.local.json configuration — file could not be parsed (comments or malformed JSON). Your existing settings are preserved.');
|
|
12181
12779
|
persistActiveProfileMarker();
|
|
12182
|
-
|
|
12780
|
+
// Callers index this result by `runtime` (installAllRuntimes' statusline
|
|
12781
|
+
// lookup), so every early exit must return the full shape — a bare return
|
|
12782
|
+
// crashes the install rather than skipping one file.
|
|
12783
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
12183
12784
|
}
|
|
12184
12785
|
const settings = validateHookFields(cleanupOrphanedHooks(rawSettings));
|
|
12185
|
-
// #3002 CR: rewrite legacy `node .../gsd-*.js` command strings
|
|
12186
|
-
//
|
|
12187
|
-
//
|
|
12188
|
-
//
|
|
12189
|
-
|
|
12786
|
+
// #3002 CR / #3662: rewrite legacy `node .../gsd-*.js` command strings (pre-
|
|
12787
|
+
// #2979 installs) AND entries baked with another environment's absolute node
|
|
12788
|
+
// path onto the runtime-resolving runner. Without this, existing managed
|
|
12789
|
+
// hook entries stay bare-`node`-prefixed or foreign-absolute across
|
|
12790
|
+
// reinstalls and remain broken under GUI/minimal-PATH runtimes and shared
|
|
12791
|
+
// config roots — the #3662 mixed state where no environment can run all
|
|
12792
|
+
// hooks.
|
|
12793
|
+
const settingsRunner = buildNodeRunnerChainToken();
|
|
12190
12794
|
if (settingsRunner && rewriteLegacyManagedNodeHookCommands(settings, settingsRunner, { platform: process.platform, runtime })) {
|
|
12191
|
-
console.log(` ${green}✓${reset} Rewrote legacy
|
|
12795
|
+
console.log(` ${green}✓${reset} Rewrote legacy managed-hook commands to the runtime-resolving node runner (#2979/#3662)`);
|
|
12192
12796
|
}
|
|
12193
12797
|
// Local installs anchor hook paths so they resolve regardless of cwd (#1906).
|
|
12194
12798
|
// Claude Code sets $CLAUDE_PROJECT_DIR; Antigravity does not — and on
|
|
@@ -12199,12 +12803,14 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12199
12803
|
// check inside projectLocalHookPrefix.
|
|
12200
12804
|
const localPrefix = projectLocalHookPrefix({ runtime, dirName, hookPathStyle: _hostBehaviors(runtime).hookPathStyle });
|
|
12201
12805
|
const hookOpts = { portableHooks: hasPortableHooks, runtime };
|
|
12202
|
-
// #2979: local-install hook commands also use
|
|
12203
|
-
//
|
|
12204
|
-
//
|
|
12205
|
-
|
|
12806
|
+
// #2979: local-install hook commands also use a runner GUI/minimal-PATH
|
|
12807
|
+
// runtimes can resolve. Bare `node` fails when the host launches the
|
|
12808
|
+
// runtime with a stripped PATH (Finder/Antigravity/etc) — #3662 replaces
|
|
12809
|
+
// the baked absolute path with the runtime-resolving chain (baked path
|
|
12810
|
+
// first, so the minimal-PATH guarantee is unchanged).
|
|
12811
|
+
const localNodeRunner = buildNodeRunnerChainToken();
|
|
12206
12812
|
const localBashRunner = resolveBashRunner({ platform: process.platform });
|
|
12207
|
-
// If we cannot resolve
|
|
12813
|
+
// If we cannot resolve a node runner AND this is a local install,
|
|
12208
12814
|
// skip managed-hook registration. Returning null from buildHookCommand on
|
|
12209
12815
|
// global installs has the same effect. Better to skip than to emit a bare
|
|
12210
12816
|
// `node` command that recreates the #2979 failure.
|
|
@@ -13209,31 +13815,49 @@ const _LEGACY_SCAN_SUBDIR_NAMES = [
|
|
|
13209
13815
|
* @param {object} [opts.logger=console] - injectable logger
|
|
13210
13816
|
* @returns {{ plan: {path:string,reason:string}[], result: object }}
|
|
13211
13817
|
*/
|
|
13212
|
-
function cleanupLegacyGsdCc({ homeDir = os.homedir(), dryRun = false, logger = console } = {}) {
|
|
13818
|
+
function cleanupLegacyGsdCc({ homeDir = os.homedir(), configDirs = null, dryRun = false, logger = console } = {}) {
|
|
13213
13819
|
// Build de-duplicated list of candidate config dirs to scan.
|
|
13214
13820
|
// Only scan under homeDir — never cwd — to prevent accidental deletion of
|
|
13215
13821
|
// the user's active-project hooks when the installer is invoked from a
|
|
13216
13822
|
// project directory that has .claude/hooks or similar subdirs.
|
|
13823
|
+
// #3799: an explicit configDirs override (install() passes [targetDir]
|
|
13824
|
+
// whenever --config-dir redirected the destination) scopes the WHOLE scan
|
|
13825
|
+
// to that dir — the default-home scan must never plan removals of a live
|
|
13826
|
+
// install that lives outside the destination the user chose.
|
|
13217
13827
|
const seen = new Set();
|
|
13218
|
-
const
|
|
13219
|
-
|
|
13220
|
-
const candidate
|
|
13221
|
-
|
|
13222
|
-
|
|
13223
|
-
|
|
13828
|
+
const scanDirs = [];
|
|
13829
|
+
if (Array.isArray(configDirs) && configDirs.length > 0) {
|
|
13830
|
+
for (const candidate of configDirs) {
|
|
13831
|
+
if (!seen.has(candidate) && fs.existsSync(candidate)) {
|
|
13832
|
+
seen.add(candidate);
|
|
13833
|
+
scanDirs.push(candidate);
|
|
13834
|
+
}
|
|
13835
|
+
}
|
|
13836
|
+
} else {
|
|
13837
|
+
for (const name of _LEGACY_SCAN_SUBDIR_NAMES) {
|
|
13838
|
+
const candidate = path.join(homeDir, name);
|
|
13839
|
+
if (!seen.has(candidate) && fs.existsSync(candidate)) {
|
|
13840
|
+
seen.add(candidate);
|
|
13841
|
+
scanDirs.push(candidate);
|
|
13842
|
+
}
|
|
13224
13843
|
}
|
|
13225
13844
|
}
|
|
13226
13845
|
|
|
13227
13846
|
// planLegacyCleanup scans each configDir and already includes the legacy
|
|
13228
13847
|
// shared cache (gsd-update-check.json) as a plan entry.
|
|
13229
|
-
const plan = planLegacyCleanup(
|
|
13848
|
+
const plan = planLegacyCleanup(scanDirs, { homeDir, ...(Array.isArray(configDirs) && configDirs.length > 0 ? { configDirs } : {}) });
|
|
13230
13849
|
|
|
13231
13850
|
// Apply the plan (dryRun honors the flag).
|
|
13232
13851
|
const result = applyLegacyCleanup(plan, { dryRun, logger });
|
|
13233
13852
|
|
|
13234
13853
|
// Also clear / preview the per-package cache so next session re-evaluates
|
|
13235
13854
|
// hook versions (replaces the former inline unlinkSync on line ~9104).
|
|
13236
|
-
|
|
13855
|
+
// #3799: under a configDirs override the cache is read/cleared under the
|
|
13856
|
+
// SCOPE root, never the default home — same invariant as the scan itself.
|
|
13857
|
+
const perPkgCacheRoot = (Array.isArray(configDirs) && configDirs.length > 0)
|
|
13858
|
+
? configDirs[0]
|
|
13859
|
+
: homeDir;
|
|
13860
|
+
const perPkgCacheFile = path.join(perPkgCacheRoot, '.cache', 'gsd', updateCacheFileName);
|
|
13237
13861
|
if (dryRun) {
|
|
13238
13862
|
logger.log('[dry-run] would remove: ' + perPkgCacheFile + ' (per-package-update-cache)');
|
|
13239
13863
|
} else {
|
|
@@ -13352,7 +13976,10 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
13352
13976
|
});
|
|
13353
13977
|
};
|
|
13354
13978
|
|
|
13355
|
-
|
|
13979
|
+
// `settings` is null on every early exit (unparseable file, skipped runtime),
|
|
13980
|
+
// and handleStatusline dereferences it — an install that declined to touch a
|
|
13981
|
+
// settings file has no statusline to prompt about, so fall through.
|
|
13982
|
+
if (primaryStatuslineResult && primaryStatuslineResult.settings) {
|
|
13356
13983
|
handleStatusline(primaryStatuslineResult.settings, isInteractive, continueAfterStatusline);
|
|
13357
13984
|
} else if (canInstallBanner) {
|
|
13358
13985
|
// No statusline-capable runtime, but at least one runtime can host the
|
|
@@ -13372,6 +13999,8 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
13372
13999
|
module.exports = {
|
|
13373
14000
|
// #3677 — hyphen-namespace normalization seam for agent bodies
|
|
13374
14001
|
shouldNormalizeHyphenNamespaceInAgentBody,
|
|
14002
|
+
// #3664: --config-dir foreign-agent-destination warning (warn-and-proceed)
|
|
14003
|
+
warnIfForeignAgentDest,
|
|
13375
14004
|
normalizeAgentBodyForRuntime,
|
|
13376
14005
|
yamlIdentifier,
|
|
13377
14006
|
getCodexSkillAdapterHeader,
|
|
@@ -13424,9 +14053,12 @@ module.exports = {
|
|
|
13424
14053
|
mergeClaudePermissions,
|
|
13425
14054
|
GSD_CLAUDE_ALLOW_PERMISSIONS,
|
|
13426
14055
|
GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS,
|
|
13427
|
-
|
|
14056
|
+
GSD_CLAUDE_LEGACY_DENY_PERMISSIONS,
|
|
13428
14057
|
GSD_CODEX_MARKER,
|
|
13429
|
-
|
|
14058
|
+
// #3897 rung 3 (ADR-3473 §8.3, HALT.md option 2)
|
|
14059
|
+
CODEX_SANDBOX_HOLDS,
|
|
14060
|
+
deriveCodexSandboxMode,
|
|
14061
|
+
validateCodexSandboxHolds,
|
|
13430
14062
|
getGlobalDir,
|
|
13431
14063
|
getConfigDirFromHome,
|
|
13432
14064
|
resolveKiloConfigPath,
|
|
@@ -13505,6 +14137,8 @@ module.exports = {
|
|
|
13505
14137
|
cleanupLegacyGsdCc,
|
|
13506
14138
|
// #1191 — exported so tests exercise the REAL readSettings, not a replica
|
|
13507
14139
|
readSettings,
|
|
14140
|
+
writeSettings,
|
|
14141
|
+
writeNonClaudeDefaults,
|
|
13508
14142
|
stripJsonComments,
|
|
13509
14143
|
copyWithPathReplacement,
|
|
13510
14144
|
};
|
|
@@ -13519,10 +14153,23 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
|
|
|
13519
14153
|
console.log('Dry run — no files will be modified.\n');
|
|
13520
14154
|
// cleanupLegacyGsdCc with dryRun:true is the single source of truth for
|
|
13521
14155
|
// both the legacy artifacts and the per-package cache path — no duplicate
|
|
13522
|
-
// printing here.
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
14156
|
+
// printing here. #3799: the preview honors the SAME scope and skip the
|
|
14157
|
+
// real install would apply (--config-dir scopes; --no-legacy-cleanup
|
|
14158
|
+
// skips) — a preview that listed default-home paths a real install would
|
|
14159
|
+
// never touch misrepresents the run.
|
|
14160
|
+
if (parseNoLegacyCleanupArg()) {
|
|
14161
|
+
console.log(' (--no-legacy-cleanup — legacy scan skipped)');
|
|
14162
|
+
} else {
|
|
14163
|
+
const previewScope = (explicitConfigDir !== null)
|
|
14164
|
+
? [getGlobalConfigDir(DEFAULT_RUNTIME, explicitConfigDir)]
|
|
14165
|
+
: undefined;
|
|
14166
|
+
const { plan } = cleanupLegacyGsdCc({
|
|
14167
|
+
dryRun: true,
|
|
14168
|
+
...(previewScope ? { configDirs: previewScope } : {}),
|
|
14169
|
+
});
|
|
14170
|
+
if (plan.length === 0) {
|
|
14171
|
+
console.log(' (no legacy get-shit-done-cc artifacts found)');
|
|
14172
|
+
}
|
|
13526
14173
|
}
|
|
13527
14174
|
process.exit(0);
|
|
13528
14175
|
} else if (hasSkillsRoot) {
|