@opengsd/gsd-core 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +642 -95
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/mempalace-capture.md +7 -3
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/review.md +2 -3
- package/gsd-core/bin/gsd-tools.cjs +335 -22
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/audit.cjs +81 -13
- package/gsd-core/bin/lib/capability-registry.cjs +82 -187
- package/gsd-core/bin/lib/capability-validator.cjs +0 -1
- package/gsd-core/bin/lib/check-command-router.cjs +101 -14
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +175 -42
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +33 -7
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +207 -41
- package/gsd-core/bin/lib/install-engine.cjs +13 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/milestone.cjs +18 -5
- package/gsd-core/bin/lib/model-resolver.cjs +159 -50
- package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +109 -7
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +227 -26
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +18 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +251 -31
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +42 -0
- package/gsd-core/bin/lib/surface.cjs +17 -1
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +8 -0
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/verification.cjs +268 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify.cjs +132 -25
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +11 -2
- package/gsd-core/references/tdd.md +27 -4
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +321 -0
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +3 -3
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +16 -6
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +63 -46
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
- package/gsd-core/workflows/complete-milestone.md +2 -2
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update.md +3 -3
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +36 -26
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +2 -2
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +2 -2
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +7 -7
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +24 -7
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +113 -13
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +21 -9
- package/gsd-core/workflows/reapply-patches.md +9 -3
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +4 -4
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +7 -7
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +81 -16
- package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
- package/hooks/dist/gsd-secret-read-guard.js +27 -1
- package/hooks/dist/gsd-statusline.js +70 -13
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-agent-isolation-guard.js +24 -0
- package/hooks/gsd-secret-read-guard.js +27 -1
- package/hooks/gsd-statusline.js +70 -13
- package/hooks/gsd-validate-commit.sh +63 -4
- package/package.json +3 -2
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/docs-guard-registry.cjs +28 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +290 -5
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +28 -9
- package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +7 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-review/SKILL.md +2 -3
- package/vscode/package.json +1 -1
package/bin/install.js
CHANGED
|
@@ -438,6 +438,17 @@ const GSD_CHANGESET_FILES = [
|
|
|
438
438
|
];
|
|
439
439
|
const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-scan.cjs', 'alias-drift-families.cjs', 'exit-code-registry.cjs', 'ndjson-reporter.cjs', 'ci-job-timing.cjs', 'shellcheck-fetch.cjs', 'npm-version-check-diagnosis.cjs', 'platform-conformance-tier.generated.cjs', 'suite-detection.cjs', 'macos-conformance-tier.generated.cjs'];
|
|
440
440
|
|
|
441
|
+
// #4544 — the Codex hook payload the install stages into <targetDir>/hooks/.
|
|
442
|
+
// Hoisted to module scope (the #3184 precedent above) so the rollback's
|
|
443
|
+
// incomplete-capture path can name exactly the files GSD owns without a
|
|
444
|
+
// second copy of the list drifting away from the staging site, which lives
|
|
445
|
+
// inside the Codex config block where the constant used to be declared.
|
|
446
|
+
const CODEX_HOOKS_TO_COPY = [
|
|
447
|
+
'gsd-check-update.js',
|
|
448
|
+
'gsd-check-update-worker.js',
|
|
449
|
+
'managed-hooks-registry.cjs',
|
|
450
|
+
];
|
|
451
|
+
|
|
441
452
|
/**
|
|
442
453
|
* Resolve a runtime's shared-hooks directory name from its descriptor.
|
|
443
454
|
*
|
|
@@ -812,6 +823,7 @@ const {
|
|
|
812
823
|
applyInstallerMigrationPlan,
|
|
813
824
|
discoverInstallerMigrations,
|
|
814
825
|
MANIFEST_SCHEMA_VERSION,
|
|
826
|
+
readInstallManifest,
|
|
815
827
|
runInstallerMigrations,
|
|
816
828
|
} = require(path.join(_gsdLibDir, 'installer-migrations.cjs'));
|
|
817
829
|
const {
|
|
@@ -925,6 +937,25 @@ const hasSkillsRoot = args.includes('--skills-root');
|
|
|
925
937
|
const hasPortableHooks = args.includes('--portable-hooks') || process.env.GSD_PORTABLE_HOOKS === '1';
|
|
926
938
|
const hasMinimal = args.includes('--minimal') || args.includes('--core-only');
|
|
927
939
|
const hasDryRun = args.includes('--dry-run');
|
|
940
|
+
// #4377: emit project-relative `@` includes (`.claude/gsd-core/...`) for a
|
|
941
|
+
// LOCAL install instead of this checkout's absolute path.
|
|
942
|
+
//
|
|
943
|
+
// Opt-in, and it stays opt-in: absolute includes work for a single checkout,
|
|
944
|
+
// which is nearly everyone, and flipping the default would change every
|
|
945
|
+
// existing local install to solve a problem those users do not have. The
|
|
946
|
+
// people who need it know they do — they run the same repo from several git
|
|
947
|
+
// worktrees, where a baked absolute path means every worktree reads its
|
|
948
|
+
// workflow prose out of whichever checkout happened to run the installer, and
|
|
949
|
+
// updating that one checkout breaks all the others at once with no way to
|
|
950
|
+
// stage it.
|
|
951
|
+
//
|
|
952
|
+
// Exported through the environment rather than threaded as a parameter,
|
|
953
|
+
// exactly like --portable-hooks/GSD_PORTABLE_HOOKS above: five separate seams
|
|
954
|
+
// compute a path prefix (the install engine, both rewrite entry points, the
|
|
955
|
+
// install plan, and applySurface), and one variable they all read cannot fall
|
|
956
|
+
// out of sync the way five signatures can.
|
|
957
|
+
const hasRelativeIncludes = args.includes('--relative-includes') || process.env.GSD_RELATIVE_INCLUDES === '1';
|
|
958
|
+
if (hasRelativeIncludes) process.env.GSD_RELATIVE_INCLUDES = '1';
|
|
928
959
|
// #3031: opt-in reclaim of the GSD artifacts a PRE-#2755 `--kimi-code` install
|
|
929
960
|
// orphaned in Kimi CLI's `~/.kimi`. Opt-in and not automatic because the stale
|
|
930
961
|
// block is BYTE-IDENTICAL to a legitimate Kimi CLI one — both runtimes render
|
|
@@ -1252,7 +1283,7 @@ if (hasUninstall) {
|
|
|
1252
1283
|
|
|
1253
1284
|
// Show help if requested
|
|
1254
1285
|
if (hasHelp) {
|
|
1255
|
-
console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--kimi-code${reset} Install for Kimi Code only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--zcode${reset} Install for ZCode only\n ${cyan}--pi${reset} Install for Pi only\n ${cyan}--gemini${reset} Install for Gemini CLI only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}--no-legacy-cleanup${reset} Skip the legacy get-shit-done-cc artifact scan\n (an explicit --config-dir already scopes the scan to it)\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n and resolve the node runner at hook-fire time via\n hooks/gsd-node-runner.sh (WSL/Docker bind-mount\n setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--reclaim-kimi-legacy${reset} With --kimi-code: also remove the GSD hooks a\n pre-1.10.0 --kimi-code install orphaned in ~/.kimi.\n Opt-in — those artifacts are indistinguishable from\n Kimi CLI's own, so skip it if you use Kimi CLI too.\n ${cyan}--profile=<name>${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi Code globally (its own ~/.kimi-code root)${reset}\n npx ${pkg.name} --kimi-code --global\n\n ${dim}# Kimi Code, also reclaiming hooks a pre-1.10.0 install left in ~/.kimi${reset}\n npx ${pkg.name} --kimi-code --global --reclaim-kimi-legacy\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline globally${reset}\n npx ${pkg.name} --cline --global\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / COPILOT_HOME / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to the first existing generic skills root: ${cyan}~/.config/agents/skills${reset}, then ${cyan}~/.agents/skills${reset}; if neither exists, GSD creates ${cyan}~/.config/agents${reset}.\n Kimi CLI and Kimi Code are separate products with separate hook roots: use ${cyan}--kimi${reset} (${cyan}~/.kimi${reset}, ${cyan}KIMI_SHARE_DIR${reset}) or ${cyan}--kimi-code${reset} (${cyan}~/.kimi-code${reset}, ${cyan}KIMI_CODE_HOME${reset}).\n`);
|
|
1286
|
+
console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--kimi-code${reset} Install for Kimi Code only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--zcode${reset} Install for ZCode only\n ${cyan}--pi${reset} Install for Pi only\n ${cyan}--gemini${reset} Install for Gemini CLI only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}--no-legacy-cleanup${reset} Skip the legacy get-shit-done-cc artifact scan\n (an explicit --config-dir already scopes the scan to it)\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n and resolve the node runner at hook-fire time via\n hooks/gsd-node-runner.sh (WSL/Docker bind-mount\n setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--relative-includes${reset} With --local: write project-relative @ includes\n (.claude/gsd-core/...) instead of this checkout's\n absolute path, so several git worktrees of one repo\n each read their own copy (also GSD_RELATIVE_INCLUDES=1)\n ${cyan}--reclaim-kimi-legacy${reset} With --kimi-code: also remove the GSD hooks a\n pre-1.10.0 --kimi-code install orphaned in ~/.kimi.\n Opt-in — those artifacts are indistinguishable from\n Kimi CLI's own, so skip it if you use Kimi CLI too.\n ${cyan}--profile=<name>${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi Code globally (its own ~/.kimi-code root)${reset}\n npx ${pkg.name} --kimi-code --global\n\n ${dim}# Kimi Code, also reclaiming hooks a pre-1.10.0 install left in ~/.kimi${reset}\n npx ${pkg.name} --kimi-code --global --reclaim-kimi-legacy\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline globally${reset}\n npx ${pkg.name} --cline --global\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Local install for a repo worked from several git worktrees${reset}\n npx ${pkg.name} --claude --local --relative-includes\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / COPILOT_HOME / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to the first existing generic skills root: ${cyan}~/.config/agents/skills${reset}, then ${cyan}~/.agents/skills${reset}; if neither exists, GSD creates ${cyan}~/.config/agents${reset}.\n Kimi CLI and Kimi Code are separate products with separate hook roots: use ${cyan}--kimi${reset} (${cyan}~/.kimi${reset}, ${cyan}KIMI_SHARE_DIR${reset}) or ${cyan}--kimi-code${reset} (${cyan}~/.kimi-code${reset}, ${cyan}KIMI_CODE_HOME${reset}).\n`);
|
|
1256
1287
|
process.exit(0);
|
|
1257
1288
|
}
|
|
1258
1289
|
|
|
@@ -1625,19 +1656,20 @@ const claudeToOpencodeTools = {
|
|
|
1625
1656
|
WebSearch: 'websearch', // Plugin/MCP - keep for compatibility
|
|
1626
1657
|
};
|
|
1627
1658
|
|
|
1628
|
-
// Tool name mapping from Claude Code to
|
|
1629
|
-
//
|
|
1630
|
-
const
|
|
1631
|
-
|
|
1659
|
+
// Tool name mapping from Claude Code to Antigravity
|
|
1660
|
+
// Antigravity uses Gemini's snake_case built-in tool names
|
|
1661
|
+
const claudeToAntigravityTools = {
|
|
1662
|
+
// #4705: Antigravity-NATIVE tool names (see src/runtime-artifact-conversion.cts)
|
|
1663
|
+
Read: 'view_file',
|
|
1632
1664
|
Write: 'write_file',
|
|
1633
|
-
Edit: '
|
|
1634
|
-
Bash: '
|
|
1665
|
+
Edit: 'replace_file_content',
|
|
1666
|
+
Bash: 'run_command',
|
|
1635
1667
|
Glob: 'glob',
|
|
1636
|
-
Grep: '
|
|
1668
|
+
Grep: 'grep_search',
|
|
1637
1669
|
WebSearch: 'google_web_search',
|
|
1638
1670
|
WebFetch: 'web_fetch',
|
|
1639
1671
|
TodoWrite: 'write_todos',
|
|
1640
|
-
}
|
|
1672
|
+
}
|
|
1641
1673
|
|
|
1642
1674
|
// Tool name mapping from Claude/GSD agents to Kimi CLI module paths.
|
|
1643
1675
|
// Kimi custom agent YAML requires fully-qualified module paths.
|
|
@@ -1687,24 +1719,24 @@ function convertToolName(claudeTool) {
|
|
|
1687
1719
|
}
|
|
1688
1720
|
|
|
1689
1721
|
/**
|
|
1690
|
-
* Convert a Claude Code tool name to
|
|
1691
|
-
* - Applies Claude→
|
|
1692
|
-
* - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in
|
|
1693
|
-
* - Filters out Task/Agent — agents are auto-registered as tools in
|
|
1694
|
-
* @returns {string|null}
|
|
1722
|
+
* Convert a Claude Code tool name to Antigravity format
|
|
1723
|
+
* - Applies Claude→Antigravity mapping (Read→read_file, Bash→run_shell_command, etc.)
|
|
1724
|
+
* - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in Antigravity
|
|
1725
|
+
* - Filters out Task/Agent — agents are auto-registered as tools in Antigravity
|
|
1726
|
+
* @returns {string|null} Antigravity tool name, or null if tool should be excluded
|
|
1695
1727
|
*/
|
|
1696
|
-
function
|
|
1728
|
+
function convertAntigravityToolName(claudeTool) {
|
|
1697
1729
|
// MCP tools: exclude — auto-discovered from mcpServers config at runtime
|
|
1698
1730
|
if (claudeTool.startsWith('mcp__')) {
|
|
1699
1731
|
return null;
|
|
1700
1732
|
}
|
|
1701
1733
|
// Task/Agent: exclude — agents are auto-registered as callable tools.
|
|
1702
|
-
// AskUserQuestion: exclude — Gemini
|
|
1703
|
-
// emitting it causes frontmatter validation errors (#3362).
|
|
1704
|
-
// Skill/SlashCommand: exclude — Gemini
|
|
1705
|
-
// the lowercase fallback would emit an invalid
|
|
1706
|
-
// that fails frontmatter validation
|
|
1707
|
-
// the entire agent load (#1394).
|
|
1734
|
+
// AskUserQuestion: exclude — Antigravity (Gemini tool dialect) does not expose
|
|
1735
|
+
// an ask_user tool; emitting it causes frontmatter validation errors (#3362).
|
|
1736
|
+
// Skill/SlashCommand: exclude — Antigravity (Gemini tool dialect) has no 'skill'
|
|
1737
|
+
// built-in tool; the lowercase fallback would emit an invalid
|
|
1738
|
+
// 'skill'/'slashcommand' name that fails frontmatter validation
|
|
1739
|
+
// (tools.N: Invalid tool name) and aborts the entire agent load (#1394).
|
|
1708
1740
|
if (
|
|
1709
1741
|
claudeTool === 'Task' ||
|
|
1710
1742
|
claudeTool === 'Agent' ||
|
|
@@ -1716,8 +1748,8 @@ function convertGeminiToolName(claudeTool) {
|
|
|
1716
1748
|
return null;
|
|
1717
1749
|
}
|
|
1718
1750
|
// Check for explicit mapping
|
|
1719
|
-
if (
|
|
1720
|
-
return
|
|
1751
|
+
if (claudeToAntigravityTools[claudeTool]) {
|
|
1752
|
+
return claudeToAntigravityTools[claudeTool];
|
|
1721
1753
|
}
|
|
1722
1754
|
// Default: lowercase
|
|
1723
1755
|
return claudeTool.toLowerCase();
|
|
@@ -2023,7 +2055,12 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
|
|
|
2023
2055
|
const names = cmdNames || readGsdCommandNames();
|
|
2024
2056
|
const normalizedBody = transformContentToHyphen(body, names);
|
|
2025
2057
|
|
|
2026
|
-
|
|
2058
|
+
// #4324: the description is the text the host's skill picker renders, so it
|
|
2059
|
+
// needs the same hyphen normalisation the body gets — otherwise a `/gsd:<cmd>`
|
|
2060
|
+
// mention in a command description ships the retired colon form to the user.
|
|
2061
|
+
const description = transformContentToHyphen(
|
|
2062
|
+
extractFrontmatterField(frontmatter, 'description') || '', names,
|
|
2063
|
+
);
|
|
2027
2064
|
const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
|
|
2028
2065
|
const agent = extractFrontmatterField(frontmatter, 'agent');
|
|
2029
2066
|
// #769: preserve context: from source command files so it is emitted into
|
|
@@ -2486,12 +2523,16 @@ function convertClaudeAgentToAntigravityAgent(content, isGlobal = false) {
|
|
|
2486
2523
|
const color = extractFrontmatterField(frontmatter, 'color');
|
|
2487
2524
|
const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || '';
|
|
2488
2525
|
|
|
2489
|
-
// Map tools to
|
|
2526
|
+
// Map tools to Antigravity equivalents (reuse existing convertAntigravityToolName)
|
|
2490
2527
|
const claudeTools = toolsRaw.split(',').map(t => t.trim()).filter(Boolean);
|
|
2491
|
-
const mappedTools = claudeTools.map(t =>
|
|
2528
|
+
const mappedTools = claudeTools.map(t => convertAntigravityToolName(t)).filter(Boolean);
|
|
2492
2529
|
|
|
2493
2530
|
// #2876: quote description for the same reason as the skill variant.
|
|
2494
|
-
|
|
2531
|
+
// #4705: tools is a YAML SEQUENCE of native names (see the src twin).
|
|
2532
|
+
const toolsBlock = mappedTools.length > 0
|
|
2533
|
+
? `tools:\n${mappedTools.map((t) => `- ${t}`).join('\n')}\n`
|
|
2534
|
+
: 'tools: []\n';
|
|
2535
|
+
let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n${toolsBlock}`;
|
|
2495
2536
|
if (color) fm += `color: ${color}\n`;
|
|
2496
2537
|
fm += '---';
|
|
2497
2538
|
|
|
@@ -2947,6 +2988,8 @@ function convertClaudeCommandToClineSkill(content, skillName, runtime = null, cm
|
|
|
2947
2988
|
let description = extractFrontmatterField(frontmatter, 'description');
|
|
2948
2989
|
if (!description) description = `Run GSD workflow ${skillName}.`;
|
|
2949
2990
|
description = toSingleLine(description);
|
|
2991
|
+
// #4324: same reason as the Claude skill converter above.
|
|
2992
|
+
description = transformContentToHyphen(description, names);
|
|
2950
2993
|
// Cline documented max is 1024 code points (not UTF-16 code units).
|
|
2951
2994
|
// Use Array.from to iterate by code point so that multibyte characters
|
|
2952
2995
|
// (e.g. emoji, astral-plane chars) are never split, which would produce
|
|
@@ -3921,6 +3964,22 @@ Typed mapping (agent_type-capable schema only):
|
|
|
3921
3964
|
never fabricate a manual worktree protocol — route through the negotiated
|
|
3922
3965
|
isolation adapter, which still fails closed for hosts declaring \`none\` (#3360).
|
|
3923
3966
|
|
|
3967
|
+
Foreground handoffs:
|
|
3968
|
+
- spawn_agent is asynchronous. When the source Agent(...) or Task(...) declares
|
|
3969
|
+
run_in_background=false, call collaboration.wait_agent(timeout_ms=...) immediately after
|
|
3970
|
+
spawn and keep the parent turn active until that child returns a terminal result.
|
|
3971
|
+
- collaboration.wait_agent is a mailbox wakeup, NOT a completion oracle: "Wait completed"
|
|
3972
|
+
can mean only that a child sent an interim MESSAGE or status update. After every wakeup,
|
|
3973
|
+
inspect the named child's update/status. Only a FINAL_ANSWER or a terminal agent status
|
|
3974
|
+
(completed, failed, or cancelled) ends the foreground handoff.
|
|
3975
|
+
- On an interim MESSAGE or any non-terminal status, do not report an outcome, send a
|
|
3976
|
+
continuation, start parent work, or end the parent turn. Call collaboration.wait_agent
|
|
3977
|
+
again for the same child. If a terminal response is absent after an abnormal end, reconcile
|
|
3978
|
+
the workflow's durable artifacts before classifying the child.
|
|
3979
|
+
- This applies to one foreground child as well as fan-out. The child retains its workflow's
|
|
3980
|
+
own checkpoint loop; do not report an outcome or start any further parent work before its
|
|
3981
|
+
terminal result is available.
|
|
3982
|
+
|
|
3924
3983
|
Generic-agent workaround (multi_agent_v1 schema — NO agent_type field):
|
|
3925
3984
|
When only the generic \`multi_agent_v1\` schema is available, typed GSD agent dispatch
|
|
3926
3985
|
(\`gsd-planner\`, \`gsd-executor\`, etc.) is NOT possible. This is a known Codex limitation
|
|
@@ -3948,6 +4007,9 @@ Spawn restriction:
|
|
|
3948
4007
|
defaulting to inline execution.
|
|
3949
4008
|
|
|
3950
4009
|
Parallel fan-out:
|
|
4010
|
+
- For each child, loop on collaboration.wait_agent(timeout_ms=...) until its own terminal
|
|
4011
|
+
result is observed. A mailbox update from one child never completes another child, and an
|
|
4012
|
+
interim MESSAGE never completes its sender.
|
|
3951
4013
|
- Spawn multiple agents → collect agent IDs → \`collaboration.wait_agent(timeout_ms=...)\` for each to complete
|
|
3952
4014
|
- Do NOT use \`functions.wait(cell_id=...)\` — that is an unrelated exec-cell tool, not the collaboration wait
|
|
3953
4015
|
|
|
@@ -7914,44 +7976,63 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7914
7976
|
content = filterRuntimeNotesForTarget(content, runtime);
|
|
7915
7977
|
|
|
7916
7978
|
if (!dispatch.mdSkipGenericRewrite) {
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
//
|
|
7924
|
-
//
|
|
7925
|
-
// (
|
|
7926
|
-
//
|
|
7927
|
-
//
|
|
7928
|
-
//
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
content =
|
|
7954
|
-
|
|
7979
|
+
// #4377: with a project-relative prefix, mask `${VAR:-default}` shell
|
|
7980
|
+
// defaults out of the substitutions below and restore them after. The
|
|
7981
|
+
// runtime launcher snippet probes gsd-tools through a chain of those
|
|
7982
|
+
// (`${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/...`, one per
|
|
7983
|
+
// runtime); they are shell word expansions, not markdown @ includes,
|
|
7984
|
+
// and a relative value there resolves against the shell's cwd instead
|
|
7985
|
+
// of the project. Swapping an include that points at the wrong
|
|
7986
|
+
// checkout for a path that points at nothing is not a fix, and the
|
|
7987
|
+
// launcher already probes `$(git rev-parse --show-toplevel)/.claude`
|
|
7988
|
+
// first, so the multi-worktree case is handled before these defaults
|
|
7989
|
+
// are ever reached. The shared helper is the single owner of the
|
|
7990
|
+
// balanced masking grammar used by this path and the rewrite engine.
|
|
7991
|
+
const rewriteGenericPaths = (body) => {
|
|
7992
|
+
content = body;
|
|
7993
|
+
const globalClaudeRegex = /~\/\.claude\//g;
|
|
7994
|
+
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
7995
|
+
const localClaudeRegex = /\.\/\.claude\//g;
|
|
7996
|
+
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
7997
|
+
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
|
7998
|
+
content = content.replace(localClaudeRegex, `./${dirName}/`);
|
|
7999
|
+
// #3544 review (Finding 1 fallout): guarded with the SAME
|
|
8000
|
+
// negative-lookahead convention already used at ~:2859-2860 below
|
|
8001
|
+
// ("preserve .claude-plugin and .claudeignore"). A naive `\b` here
|
|
8002
|
+
// is satisfied by ANY non-word character, including '-' — so for a
|
|
8003
|
+
// --config-dir whose name EXTENDS '.claude' (e.g. '.claude-work',
|
|
8004
|
+
// pathPrefix '$HOME/.claude-work/'), this pass re-matched the
|
|
8005
|
+
// '$HOME/.claude' PREFIX of its own slash-form output (lines above)
|
|
8006
|
+
// and re-appended the full prefix, corrupting every emitted path to
|
|
8007
|
+
// '$HOME/.claude-work-work/...'. Harmless no-op for the literal
|
|
8008
|
+
// default '.claude' (self-replace with an identical string), which
|
|
8009
|
+
// is why this went undetected until a non-default config-dir name
|
|
8010
|
+
// was exercised.
|
|
8011
|
+
content = content.replace(/~\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
|
|
8012
|
+
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
|
|
8013
|
+
content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
|
|
8014
|
+
content = content.replace(/~\/\.qwen\//g, pathPrefix);
|
|
8015
|
+
content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
|
|
8016
|
+
content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
|
|
8017
|
+
content = content.replace(/~\/\.hermes\//g, pathPrefix);
|
|
8018
|
+
content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
|
|
8019
|
+
content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
|
|
8020
|
+
// #3544: restore @-file-reference lines to the tilde form Claude Code
|
|
8021
|
+
// actually expands — the SAME correction #3133 already applies to
|
|
8022
|
+
// skill/command bodies via _applyRuntimeRewrites's 'claude' case (see
|
|
8023
|
+
// restoreClaudeGlobalAtRefTilde's doc comment in
|
|
8024
|
+
// runtime-artifact-conversion.cts). This is the gsd-core/ spec-tree
|
|
8025
|
+
// emit path, which never had it: every @~/.claude/gsd-core/… include
|
|
8026
|
+
// in a global install's workflows/references tree silently resolved
|
|
8027
|
+
// to nothing (54 includes across 22 files on a live install).
|
|
8028
|
+
if (runtime === 'claude') {
|
|
8029
|
+
content = runtimeArtifactConversion._restoreClaudeGlobalAtRefTilde(content, pathPrefix);
|
|
8030
|
+
}
|
|
8031
|
+
return content;
|
|
8032
|
+
};
|
|
8033
|
+
content = runtimeArtifactConversion._isRelativePathPrefix(pathPrefix)
|
|
8034
|
+
? runtimeArtifactConversion._withShellDefaultsPreserved(content, rewriteGenericPaths)
|
|
8035
|
+
: rewriteGenericPaths(content);
|
|
7955
8036
|
}
|
|
7956
8037
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
7957
8038
|
|
|
@@ -9890,6 +9971,11 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
9890
9971
|
// it from the directory it happened to be found in (#2872).
|
|
9891
9972
|
runtime,
|
|
9892
9973
|
scope: resolvedScope,
|
|
9974
|
+
// #4377: a surface re-apply is a separate process and cannot rely on the
|
|
9975
|
+
// installer's environment. Persist only a safe project-relative prefix.
|
|
9976
|
+
relativeIncludePrefix: resolvedScope === 'local' && hasRelativeIncludes
|
|
9977
|
+
? runtimeArtifactConversion._projectRelativePrefixFromProjectRoot(process.cwd(), configDir)
|
|
9978
|
+
: undefined,
|
|
9893
9979
|
files: {},
|
|
9894
9980
|
};
|
|
9895
9981
|
|
|
@@ -10773,6 +10859,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10773
10859
|
isWindowsHost,
|
|
10774
10860
|
resolvedTarget,
|
|
10775
10861
|
homeDir,
|
|
10862
|
+
// #4377: the runtime's own localConfigDir. This is the prefix that reaches
|
|
10863
|
+
// copyWithPathReplacement, i.e. the one actually written into every
|
|
10864
|
+
// emitted command/skill/workflow body — the rewrite-engine seams below
|
|
10865
|
+
// handle re-applied surfaces, not the first install.
|
|
10866
|
+
localDirName: _hostBehaviors(runtime).localTargetIsProjectRoot === true ? undefined : getDirName(runtime),
|
|
10776
10867
|
});
|
|
10777
10868
|
|
|
10778
10869
|
// runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239
|
|
@@ -10783,6 +10874,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10783
10874
|
|
|
10784
10875
|
// Track installation failures
|
|
10785
10876
|
const failures = [];
|
|
10877
|
+
const configuredEntrypoints = [];
|
|
10786
10878
|
let installerMigrationResult = null;
|
|
10787
10879
|
const rollbackInstallerMigrations = () => {
|
|
10788
10880
|
if (!installerMigrationResult || typeof installerMigrationResult.rollback !== 'function') return;
|
|
@@ -10835,7 +10927,48 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10835
10927
|
// Map<filename, Buffer> — content snapshot of each pre-existing gsd-* agent file.
|
|
10836
10928
|
const codexPreInstallAgentContents = new Map();
|
|
10837
10929
|
let codexPreInstallVersionBytes = null;
|
|
10838
|
-
|
|
10930
|
+
// #4544 — manifest-driven snapshot state (captured in the block below):
|
|
10931
|
+
// codexPreInstallManagedFiles — Map<normalizedRelPath, Buffer|null>; one
|
|
10932
|
+
// entry per path the PRIOR install's gsd-file-manifest.json recorded.
|
|
10933
|
+
// null means the path did not exist pre-install, so rollback re-deletes
|
|
10934
|
+
// whatever this install put there instead of resurrecting it.
|
|
10935
|
+
// codexPreInstallManifestBytes — Buffer (or null) of the prior manifest file
|
|
10936
|
+
// itself, which a reinstall rewrites.
|
|
10937
|
+
// codexPreInstallHooksTree — Map<relPath, Buffer>, a full recursive
|
|
10938
|
+
// snapshot of <targetDir>/hooks/. The Codex manifest deliberately omits
|
|
10939
|
+
// hooks/ (the !isCodex gate on shared-hooks tracking), and hooks/ is
|
|
10940
|
+
// shared space, so the restore is wholesale: user files that predate
|
|
10941
|
+
// the install are in the snapshot and come back; anything the failed
|
|
10942
|
+
// install staged does not.
|
|
10943
|
+
const codexPreInstallManagedFiles = new Map();
|
|
10944
|
+
let codexPreInstallManifestBytes = null;
|
|
10945
|
+
const codexPreInstallHooksTree = new Map();
|
|
10946
|
+
// #4544 (review) — capture-state flags the restore must consult:
|
|
10947
|
+
// codexManagedSnapshotCaptured — the capture gate ran at all. When
|
|
10948
|
+
// false (non-Codex runtimes, minimal mode) NO pre-install state was
|
|
10949
|
+
// recorded, and the only safe restore is no restore: an empty
|
|
10950
|
+
// snapshot must never be read as "hooks/ was absent".
|
|
10951
|
+
// codexPreInstallHooksDirPreExisted — hooks/ existed as a DIRECTORY
|
|
10952
|
+
// pre-install. A pre-existing hooks FILE is left alone on rollback
|
|
10953
|
+
// rather than deleted.
|
|
10954
|
+
// codexPreInstallHooksCaptureIncomplete — some part of the hooks/ tree
|
|
10955
|
+
// could not be read (permissions, special files). The restore
|
|
10956
|
+
// downgrades to per-file so an uncapturable user file is never
|
|
10957
|
+
// destroyed by a wholesale delete whose snapshot lacked it.
|
|
10958
|
+
let codexManagedSnapshotCaptured = false;
|
|
10959
|
+
// null = the gate never ran; true/false = the gate ran and hooks/ (did|did
|
|
10960
|
+
// not) exist as a directory pre-install. Two states are load-bearing: a
|
|
10961
|
+
// clean first install records false, so its rollback removes the staged
|
|
10962
|
+
// hooks/ tree entirely; a non-Codex runtime records null, so rollback does
|
|
10963
|
+
// nothing.
|
|
10964
|
+
let codexPreInstallHooksDirPreExisted = null;
|
|
10965
|
+
let codexPreInstallHooksCaptureIncomplete = false;
|
|
10966
|
+
// #4249 CR: not gated on install mode. restoreCodexSnapshot is reachable for
|
|
10967
|
+
// a core/--minimal install too (#2695), and its pass-2 sweeps remove every
|
|
10968
|
+
// gsd-* skill dir / agent file the snapshot does not claim — so an empty
|
|
10969
|
+
// minimal-mode snapshot deleted the whole surface with nothing to restore.
|
|
10970
|
+
if (_hostBehaviors(runtime).tomlConfigInstall) {
|
|
10971
|
+
codexManagedSnapshotCaptured = true;
|
|
10839
10972
|
const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
|
|
10840
10973
|
if (fs.existsSync(_preSkillsDir)) {
|
|
10841
10974
|
for (const entry of fs.readdirSync(_preSkillsDir, { withFileTypes: true })) {
|
|
@@ -10877,18 +11010,212 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10877
11010
|
if (fs.existsSync(_preVersionPath)) {
|
|
10878
11011
|
try { codexPreInstallVersionBytes = fs.readFileSync(_preVersionPath); } catch (_) { /* best-effort */ }
|
|
10879
11012
|
}
|
|
10880
|
-
|
|
11013
|
+
// #4544 — capture the manifest-driven surfaces, same best-effort
|
|
11014
|
+
// conventions as the skills/ snapshot above. readInstallManifest is the
|
|
11015
|
+
// same hardened reader installer-migrations uses (array/ garbage shapes
|
|
11016
|
+
// degrade to an empty file set — rollback then simply covers less, never
|
|
11017
|
+
// crashes), and resolveInstallRelativePath keeps a hostile manifest key
|
|
11018
|
+
// from turning into a write outside the install root.
|
|
11019
|
+
const _priorManifest = readInstallManifest(targetDir);
|
|
11020
|
+
for (const rel of Object.keys(_priorManifest.files)) {
|
|
11021
|
+
const resolved = resolveInstallRelativePath(targetDir, rel);
|
|
11022
|
+
if (!resolved) continue;
|
|
11023
|
+
try {
|
|
11024
|
+
codexPreInstallManagedFiles.set(resolved.relPath, fs.readFileSync(resolved.fullPath));
|
|
11025
|
+
} catch (_) {
|
|
11026
|
+
// Listed but absent/unreadable pre-install: snapshot absence, so
|
|
11027
|
+
// rollback re-deletes instead of resurrecting.
|
|
11028
|
+
codexPreInstallManagedFiles.set(resolved.relPath, null);
|
|
11029
|
+
}
|
|
11030
|
+
}
|
|
11031
|
+
const _preManifestPath = path.join(targetDir, MANIFEST_NAME);
|
|
11032
|
+
if (fs.existsSync(_preManifestPath)) {
|
|
11033
|
+
try { codexPreInstallManifestBytes = fs.readFileSync(_preManifestPath); } catch (_) { /* best-effort */ }
|
|
11034
|
+
}
|
|
11035
|
+
// #4544 (review) — a clean FIRST install has no prior manifest, so nothing
|
|
11036
|
+
// above records the payload this install is about to write, and a failed
|
|
11037
|
+
// clean install would roll back to a half-written tree. Enumerate the SAME
|
|
11038
|
+
// source directories the installer copies (a directory walk tracks the
|
|
11039
|
+
// source tree automatically — no second file list to keep in parity) and
|
|
11040
|
+
// record every path as absent-pre-install. On a reinstall most of these
|
|
11041
|
+
// already carry entries from the prior manifest; any that do not (files
|
|
11042
|
+
// new in this version) snapshot their pre-install bytes or absence exactly
|
|
11043
|
+
// like the rest, which also closes the new-version-file residual.
|
|
11044
|
+
const _recordWritePlanTree = (srcDir, relPrefix) => {
|
|
11045
|
+
let children;
|
|
11046
|
+
try { children = fs.readdirSync(srcDir, { withFileTypes: true }); } catch (_) { return; }
|
|
11047
|
+
for (const child of children) {
|
|
11048
|
+
const rel = relPrefix ? `${relPrefix}/${child.name}` : child.name;
|
|
11049
|
+
if (child.isDirectory()) {
|
|
11050
|
+
_recordWritePlanTree(path.join(srcDir, child.name), rel);
|
|
11051
|
+
} else if (child.isFile()) {
|
|
11052
|
+
if (codexPreInstallManagedFiles.has(rel)) continue;
|
|
11053
|
+
// USER_OWNED_ARTIFACTS are manifest-relative to gsd-core/ (#2771):
|
|
11054
|
+
// they are durably staged across reinstalls and must never enter a
|
|
11055
|
+
// rollback delete-set.
|
|
11056
|
+
const manifestRel = rel.startsWith('gsd-core/') ? rel.slice('gsd-core/'.length) : rel;
|
|
11057
|
+
if (USER_OWNED_ARTIFACTS.includes(manifestRel)) continue;
|
|
11058
|
+
const resolved = resolveInstallRelativePath(targetDir, rel);
|
|
11059
|
+
if (!resolved) continue;
|
|
11060
|
+
try {
|
|
11061
|
+
codexPreInstallManagedFiles.set(rel, fs.existsSync(resolved.fullPath) ? fs.readFileSync(resolved.fullPath) : null);
|
|
11062
|
+
} catch (_) {
|
|
11063
|
+
codexPreInstallManagedFiles.set(rel, null);
|
|
11064
|
+
}
|
|
11065
|
+
}
|
|
11066
|
+
}
|
|
11067
|
+
};
|
|
11068
|
+
_recordWritePlanTree(path.join(src, 'gsd-core'), 'gsd-core');
|
|
11069
|
+
_recordWritePlanTree(path.join(src, 'scripts', 'changeset'), 'scripts/changeset');
|
|
11070
|
+
_recordWritePlanTree(path.join(src, 'scripts', 'lib'), 'scripts/lib');
|
|
11071
|
+
// gsd-core/CHANGELOG.md is sourced from the repo root (not src/gsd-core)
|
|
11072
|
+
// and gsd-core/.gsd-runtime is generated at install time — neither appears
|
|
11073
|
+
// in the directory walks, so record them explicitly.
|
|
11074
|
+
for (const standalone of ['gsd-core/CHANGELOG.md', 'gsd-core/.gsd-runtime', 'scripts/fix-slash-commands.cjs', 'scripts/gen-capability-registry.cjs', 'scripts/gen-loop-host-contract.cjs']) {
|
|
11075
|
+
if (codexPreInstallManagedFiles.has(standalone)) continue;
|
|
11076
|
+
const resolved = resolveInstallRelativePath(targetDir, standalone);
|
|
11077
|
+
if (!resolved) continue;
|
|
11078
|
+
try {
|
|
11079
|
+
codexPreInstallManagedFiles.set(standalone, fs.existsSync(resolved.fullPath) ? fs.readFileSync(resolved.fullPath) : null);
|
|
11080
|
+
} catch (_) {
|
|
11081
|
+
codexPreInstallManagedFiles.set(standalone, null);
|
|
11082
|
+
}
|
|
11083
|
+
}
|
|
11084
|
+
// hooks/ — full recursive snapshot, but never blind: lstat every entry so
|
|
11085
|
+
// a symlink under hooks/ is neither followed (a link to a FIFO would hang
|
|
11086
|
+
// the installer, /dev/zero would exhaust memory, and a link to private
|
|
11087
|
+
// data would copy that data into the snapshot — hooks/ is user-writable
|
|
11088
|
+
// shared space and, for local installs, repo-controllable) nor restored
|
|
11089
|
+
// as a link. Anything unreadable or special marks the capture INCOMPLETE
|
|
11090
|
+
// so the restore downgrades to per-file instead of wholesale-deleting a
|
|
11091
|
+
// tree it never fully saw. A pre-existing hooks FILE (not directory) is
|
|
11092
|
+
// recorded as such and left alone on rollback.
|
|
11093
|
+
const _preHooksPath = path.join(targetDir, 'hooks');
|
|
11094
|
+
let _preHooksStat = null;
|
|
11095
|
+
try { _preHooksStat = fs.lstatSync(_preHooksPath); } catch (_) { /* absent */ }
|
|
11096
|
+
codexPreInstallHooksDirPreExisted = Boolean(_preHooksStat && _preHooksStat.isDirectory());
|
|
11097
|
+
if (codexPreInstallHooksDirPreExisted) {
|
|
11098
|
+
const _snapshotHooksDir = (dir, relBase) => {
|
|
11099
|
+
let children;
|
|
11100
|
+
try { children = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) {
|
|
11101
|
+
codexPreInstallHooksCaptureIncomplete = true;
|
|
11102
|
+
return;
|
|
11103
|
+
}
|
|
11104
|
+
for (const child of children) {
|
|
11105
|
+
const relPath = relBase ? `${relBase}/${child.name}` : child.name;
|
|
11106
|
+
const fullPath = path.join(dir, child.name);
|
|
11107
|
+
let st = null;
|
|
11108
|
+
try { st = fs.lstatSync(fullPath); } catch (_) {
|
|
11109
|
+
codexPreInstallHooksCaptureIncomplete = true;
|
|
11110
|
+
continue;
|
|
11111
|
+
}
|
|
11112
|
+
if (st.isDirectory()) {
|
|
11113
|
+
_snapshotHooksDir(fullPath, relPath);
|
|
11114
|
+
} else if (st.isFile()) {
|
|
11115
|
+
try { codexPreInstallHooksTree.set(relPath, fs.readFileSync(fullPath)); } catch (_) {
|
|
11116
|
+
codexPreInstallHooksCaptureIncomplete = true;
|
|
11117
|
+
}
|
|
11118
|
+
} else {
|
|
11119
|
+
codexPreInstallHooksCaptureIncomplete = true;
|
|
11120
|
+
}
|
|
11121
|
+
}
|
|
11122
|
+
};
|
|
11123
|
+
_snapshotHooksDir(_preHooksPath, '');
|
|
11124
|
+
}
|
|
11125
|
+
}
|
|
11126
|
+
|
|
11127
|
+
// #4544 — shared restore for the manifest-driven surfaces. Called by BOTH
|
|
11128
|
+
// rollback paths: _codexPreConfigRollback (CHANGELOG.md, scripts/, the
|
|
11129
|
+
// initial manifest write AND — via installer migrations' stale-hook removal
|
|
11130
|
+
// — hooks/ itself are all mutated BEFORE config.toml is touched, so the
|
|
11131
|
+
// early path must cover them) and the full restoreCodexSnapshot() below.
|
|
11132
|
+
// Best-effort throughout, matching the #3245 convention: restore failures
|
|
11133
|
+
// never mask the original install error.
|
|
11134
|
+
const restoreCodexManagedSnapshot = () => {
|
|
11135
|
+
// #4544 (review) — if the capture never ran (non-Codex runtimes, minimal
|
|
11136
|
+
// mode), no pre-install state was recorded. The only safe action is NONE:
|
|
11137
|
+
// an empty snapshot must never be read as "hooks/ was absent", or a
|
|
11138
|
+
// minimal-mode rollback would delete the user's entire hooks/ tree.
|
|
11139
|
+
if (!codexManagedSnapshotCaptured) return;
|
|
11140
|
+
// hooks/ — the pre-install tree is restored wholesale: a user file that
|
|
11141
|
+
// predated the install is IN the snapshot and comes back; anything the
|
|
11142
|
+
// failed install staged is not, and goes away with the tree. When the
|
|
11143
|
+
// capture was INCOMPLETE, wholesale deletion would permanently destroy a
|
|
11144
|
+
// file whose bytes were never captured, so the restore downgrades to
|
|
11145
|
+
// per-file: put back what was captured and remove only the names GSD
|
|
11146
|
+
// itself stages (the hoisted CODEX_HOOKS_TO_COPY set plus the CommonJS
|
|
11147
|
+
// marker). hooks/lib/ is left untouched in that mode — its contents are
|
|
11148
|
+
// transitive and cannot be enumerated safely without the capture.
|
|
11149
|
+
if (codexPreInstallHooksDirPreExisted !== null) {
|
|
11150
|
+
const _hooksRestoreDir = path.join(targetDir, 'hooks');
|
|
11151
|
+
if (!codexPreInstallHooksDirPreExisted) {
|
|
11152
|
+
// Clean first install: nothing pre-existed under hooks/, so nothing
|
|
11153
|
+
// the failed install staged may survive either.
|
|
11154
|
+
try { fs.rmSync(_hooksRestoreDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
|
|
11155
|
+
} else if (!codexPreInstallHooksCaptureIncomplete) {
|
|
11156
|
+
try { fs.rmSync(_hooksRestoreDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
|
|
11157
|
+
for (const [relPath, buf] of codexPreInstallHooksTree) {
|
|
11158
|
+
const destFile = path.join(_hooksRestoreDir, relPath);
|
|
11159
|
+
try {
|
|
11160
|
+
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
11161
|
+
fs.writeFileSync(destFile, buf);
|
|
11162
|
+
} catch (_) { /* best-effort */ }
|
|
11163
|
+
}
|
|
11164
|
+
} else {
|
|
11165
|
+
// GSD-owned names are removed FIRST: several of them are also
|
|
11166
|
+
// legitimate pre-install files the snapshot just restored, and a
|
|
11167
|
+
// removal pass after the restore would delete the restored bytes.
|
|
11168
|
+
for (const hookName of CODEX_HOOKS_TO_COPY) {
|
|
11169
|
+
try { fs.rmSync(path.join(_hooksRestoreDir, hookName), { force: true }); } catch (_) { /* best-effort */ }
|
|
11170
|
+
}
|
|
11171
|
+
try { fs.rmSync(path.join(_hooksRestoreDir, 'package.json'), { force: true }); } catch (_) { /* best-effort */ }
|
|
11172
|
+
for (const [relPath, buf] of codexPreInstallHooksTree) {
|
|
11173
|
+
const destFile = path.join(_hooksRestoreDir, relPath);
|
|
11174
|
+
try {
|
|
11175
|
+
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
11176
|
+
fs.writeFileSync(destFile, buf);
|
|
11177
|
+
} catch (_) { /* best-effort */ }
|
|
11178
|
+
}
|
|
11179
|
+
}
|
|
11180
|
+
}
|
|
11181
|
+
// Every GSD-owned path the prior manifest recorded (plus the clean-install
|
|
11182
|
+
// write plan): restore bytes, or re-delete a path that was absent
|
|
11183
|
+
// pre-install.
|
|
11184
|
+
for (const [relPath, buf] of codexPreInstallManagedFiles) {
|
|
11185
|
+
const resolved = resolveInstallRelativePath(targetDir, relPath);
|
|
11186
|
+
if (!resolved) continue;
|
|
11187
|
+
try {
|
|
11188
|
+
if (buf !== null) {
|
|
11189
|
+
fs.mkdirSync(path.dirname(resolved.fullPath), { recursive: true });
|
|
11190
|
+
fs.writeFileSync(resolved.fullPath, buf);
|
|
11191
|
+
} else if (fs.existsSync(resolved.fullPath)) {
|
|
11192
|
+
fs.rmSync(resolved.fullPath, { force: true });
|
|
11193
|
+
}
|
|
11194
|
+
} catch (_) { /* best-effort */ }
|
|
11195
|
+
}
|
|
11196
|
+
// The prior manifest file itself: reinstall rewrites it; rollback returns
|
|
11197
|
+
// the previous install's manifest (or removes it on a clean first install).
|
|
11198
|
+
const _manifestRestorePath = path.join(targetDir, MANIFEST_NAME);
|
|
11199
|
+
if (codexPreInstallManifestBytes !== null) {
|
|
11200
|
+
try { fs.writeFileSync(_manifestRestorePath, codexPreInstallManifestBytes); } catch (_) { /* best-effort */ }
|
|
11201
|
+
} else if (fs.existsSync(_manifestRestorePath)) {
|
|
11202
|
+
try { fs.unlinkSync(_manifestRestorePath); } catch (_) { /* best-effort */ }
|
|
11203
|
+
}
|
|
11204
|
+
};
|
|
10881
11205
|
|
|
10882
11206
|
// #3245 CR finding 2 — Rollback coverage extends to ALL post-snapshot operations,
|
|
10883
11207
|
// not just the Codex config/hook error paths. Any throw between snapshot capture and
|
|
10884
11208
|
// the Codex config block (skills copy, agents copy, VERSION write, manifest write, etc.)
|
|
10885
11209
|
// must also trigger rollback so the caller is never left in a partially-installed state.
|
|
10886
11210
|
//
|
|
10887
|
-
// _codexPreConfigRollback covers the
|
|
10888
|
-
// config.toml is touched: skills/, agents/, gsd-core/VERSION,
|
|
11211
|
+
// _codexPreConfigRollback covers the surfaces that can be mutated before
|
|
11212
|
+
// config.toml is touched: skills/, agents/, gsd-core/VERSION, the manifest-
|
|
11213
|
+
// driven surfaces (#4544 — CHANGELOG.md, scripts/, .gsd-runtime and the
|
|
11214
|
+
// manifest itself are all rewritten in this window), and orphaned
|
|
10889
11215
|
// atomic-write temp files. It is safe to call before any writes have happened.
|
|
10890
11216
|
// The full restoreCodexSnapshot() (defined inside the config block) additionally
|
|
10891
|
-
// handles config.toml, which
|
|
11217
|
+
// handles config.toml and the staged hooks/ tree, which are not yet touched
|
|
11218
|
+
// at this point in the pipeline.
|
|
10892
11219
|
const _codexPreConfigRollback = !_hostBehaviors(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
|
|
10893
11220
|
rollbackInstallerMigrations();
|
|
10894
11221
|
// skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
|
|
@@ -10949,6 +11276,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10949
11276
|
} else if (fs.existsSync(_earlyVersionPath)) {
|
|
10950
11277
|
try { fs.unlinkSync(_earlyVersionPath); } catch (_) { /* best-effort */ }
|
|
10951
11278
|
}
|
|
11279
|
+
// #4544 — manifest-driven surfaces (CHANGELOG.md, scripts/, the initial
|
|
11280
|
+
// manifest write, and — via installer migrations' stale-hook removal —
|
|
11281
|
+
// hooks/ itself are all mutated in this window). The shared restore is
|
|
11282
|
+
// also idempotent against an untouched tree.
|
|
11283
|
+
restoreCodexManagedSnapshot();
|
|
10952
11284
|
// Orphaned atomic-write temp files.
|
|
10953
11285
|
const _earlyTmpPattern = /\.tmp-\d+-\d+$/;
|
|
10954
11286
|
function _earlyCleanTmpFiles(dir) {
|
|
@@ -11809,7 +12141,14 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11809
12141
|
console.log(` ${green}✓${reset} Wrote ${sharedHooksDirName}/package.json (CommonJS mode)`);
|
|
11810
12142
|
break;
|
|
11811
12143
|
case 'preserved-foreign':
|
|
11812
|
-
|
|
12144
|
+
// #4759: the foreign file usually DOES declare "type": "commonjs" —
|
|
12145
|
+
// any hand-written or formatter-touched package.json does — and Node
|
|
12146
|
+
// then loads the staged .js hooks as CommonJS, so the old
|
|
12147
|
+
// unconditional "may not resolve" claim was usually false. The
|
|
12148
|
+
// sibling plugin path (src/install-engine.cts) words this same
|
|
12149
|
+
// outcome conditionally; match it and keep will-not-load conditional
|
|
12150
|
+
// on "type": "module", the only case where it is true.
|
|
12151
|
+
console.warn(` ${yellow}⚠${reset} Left existing ${sharedHooksDirName}/package.json untouched (not GSD's marker). If it declares "type": "module", the staged hooks will not load.`);
|
|
11813
12152
|
break;
|
|
11814
12153
|
case 'failed':
|
|
11815
12154
|
// Best-effort: a read-only or full config dir must not abort the
|
|
@@ -12041,6 +12380,42 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12041
12380
|
manifestFiles = null;
|
|
12042
12381
|
}
|
|
12043
12382
|
if (manifestFiles !== null) {
|
|
12383
|
+
// #4667: codex-installed artifacts must not keep `@~/.claude/gsd-core/…`
|
|
12384
|
+
// include references — the `@` form resolves into the CLAUDE install
|
|
12385
|
+
// (wrong copy on dual-runtime machines at divergent versions, nothing at
|
|
12386
|
+
// all on codex-only ones; #570 cause 2 residue). Every target ships in
|
|
12387
|
+
// the codex install, so rewriting the `@~/` include form to the codex
|
|
12388
|
+
// root is mechanical and correct. This runs after all .md emitters
|
|
12389
|
+
// (several bypass the per-runtime converters — that is how the leak
|
|
12390
|
+
// survived the per-emitter fixes; the agent .tomls are generated later
|
|
12391
|
+
// and prefix themselves), and before the scan below, which stays as the
|
|
12392
|
+
// verification backstop. The `_GSD_RUNTIME_ROOT`/`$PREFERRED_CONFIG_DIR`
|
|
12393
|
+
// fallback chains and prose `.claude` mentions carry no `@~/` prefix and
|
|
12394
|
+
// are deliberately untouched, as is CHANGELOG.md.
|
|
12395
|
+
if (runtime === 'codex') {
|
|
12396
|
+
for (const relPath of manifestFiles) {
|
|
12397
|
+
const fileName = path.basename(relPath);
|
|
12398
|
+
if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
|
|
12399
|
+
if (fileName === 'CHANGELOG.md') continue;
|
|
12400
|
+
const rewritePath = path.join(targetDir, relPath);
|
|
12401
|
+
let rewriteContent;
|
|
12402
|
+
try {
|
|
12403
|
+
rewriteContent = fs.readFileSync(rewritePath, 'utf8');
|
|
12404
|
+
} catch (rewriteErr) {
|
|
12405
|
+
continue; // inaccessible or missing — the scan below reports or skips it
|
|
12406
|
+
}
|
|
12407
|
+
const rewritten = rewriteContent
|
|
12408
|
+
.split('@~/.claude/gsd-core/').join('@~/.codex/gsd-core/')
|
|
12409
|
+
.split('@$HOME/.claude/gsd-core/').join('@$HOME/.codex/gsd-core/');
|
|
12410
|
+
if (rewritten !== rewriteContent) {
|
|
12411
|
+
try {
|
|
12412
|
+
fs.writeFileSync(rewritePath, rewritten);
|
|
12413
|
+
} catch (writeErr) {
|
|
12414
|
+
continue; // never fail the install over the rewrite; the scan still warns
|
|
12415
|
+
}
|
|
12416
|
+
}
|
|
12417
|
+
}
|
|
12418
|
+
}
|
|
12044
12419
|
for (const relPath of manifestFiles) {
|
|
12045
12420
|
const fileName = path.basename(relPath);
|
|
12046
12421
|
if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
|
|
@@ -12249,6 +12624,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12249
12624
|
try { fs.unlinkSync(_rollbackVersionPath); } catch (_) { /* best-effort */ }
|
|
12250
12625
|
}
|
|
12251
12626
|
|
|
12627
|
+
// 4b. #4544 — manifest-driven surfaces: the staged hooks/ tree, every
|
|
12628
|
+
// GSD-owned path the prior manifest recorded (scripts/, gsd-core/
|
|
12629
|
+
// payload), and the prior manifest file itself.
|
|
12630
|
+
restoreCodexManagedSnapshot();
|
|
12631
|
+
|
|
12252
12632
|
// 5. Orphaned atomic-write temp files (<file>.tmp-<pid>-<n>) in targetDir.
|
|
12253
12633
|
// These can accumulate if an atomic write fails mid-rename. Best-effort scan.
|
|
12254
12634
|
//
|
|
@@ -12322,11 +12702,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12322
12702
|
// ENOENT -> allow(undefined), every invocation, every event, no exceptions).
|
|
12323
12703
|
// A pre-#2586 install's stale copy + hooks.json registrations are cleaned
|
|
12324
12704
|
// up below (see the CODEX_EXTENDED_HOOK_EVENTS loop), not re-added here.
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
'gsd-check-update-worker.js',
|
|
12328
|
-
'managed-hooks-registry.cjs',
|
|
12329
|
-
];
|
|
12705
|
+
// CODEX_HOOKS_TO_COPY itself lives at module scope (#4544) — the rollback's
|
|
12706
|
+
// incomplete-capture path must name the same set without a second literal.
|
|
12330
12707
|
const codexHooksSrc = path.join(src, 'hooks', 'dist');
|
|
12331
12708
|
if (fs.existsSync(codexHooksSrc)) {
|
|
12332
12709
|
const codexHooksDest = path.join(targetDir, 'hooks');
|
|
@@ -12520,6 +12897,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12520
12897
|
absoluteRunner: codexNodeRunner,
|
|
12521
12898
|
platform: process.platform,
|
|
12522
12899
|
});
|
|
12900
|
+
configuredEntrypoints.push(...(hookWrite.configuredEntrypoints || []));
|
|
12523
12901
|
if (hookWrite.wrote) {
|
|
12524
12902
|
console.log(` ${green}✓${reset} Configured Codex hooks (SessionStart via hooks.json)`);
|
|
12525
12903
|
} else {
|
|
@@ -12599,7 +12977,21 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12599
12977
|
}
|
|
12600
12978
|
|
|
12601
12979
|
persistActiveProfileMarker();
|
|
12602
|
-
|
|
12980
|
+
// #4249: expose restoreCodexSnapshot (#3245) as a SECOND, separately named
|
|
12981
|
+
// rollback rather than rebinding `rollbackInstallerMigrations` to it. A
|
|
12982
|
+
// configured-entrypoint validation failure discovered later (outside this
|
|
12983
|
+
// function, after Codex's own hooks.json/config.toml write already
|
|
12984
|
+
// succeeded) previously had only the installer-migrations closure to call,
|
|
12985
|
+
// leaving the just-written config.toml/hooks.json broken on disk despite
|
|
12986
|
+
// Codex already owning a full pre-install snapshot/restore for exactly this.
|
|
12987
|
+
//
|
|
12988
|
+
// Every runtime's `rollbackInstallerMigrations` therefore still means what
|
|
12989
|
+
// it says — the installer-migrations-only closure, which is what a
|
|
12990
|
+
// finalize-stage failure that is NOT an entrypoint-validation failure gets
|
|
12991
|
+
// (the Phase 4 contract). `rollbackPreInstallSnapshot` is Codex-only and is
|
|
12992
|
+
// chosen only for entrypoint-validation failures. See the selection in
|
|
12993
|
+
// installAllRuntimes' rollbackFinalizedInstallerMigrations.
|
|
12994
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints, rollbackInstallerMigrations, rollbackPreInstallSnapshot: restoreCodexSnapshot };
|
|
12603
12995
|
}
|
|
12604
12996
|
|
|
12605
12997
|
if (plan.installSurface === 'copilot-instructions') {
|
|
@@ -12629,7 +13021,18 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12629
13021
|
writeCopilotHookConfig(targetDir);
|
|
12630
13022
|
console.log(` ${green}✓${reset} Configured Copilot lifecycle hook (sessionStart)`);
|
|
12631
13023
|
persistActiveProfileMarker();
|
|
12632
|
-
|
|
13024
|
+
// #4249: `[]`, not omitted — every Copilot hook is an inline `printf`
|
|
13025
|
+
// one-liner (GSD_COPILOT_*_HOOK_BASH/PWSH in src/runtime-hooks-surface.cts),
|
|
13026
|
+
// so this runtime genuinely launches no GSD-managed script and has no
|
|
13027
|
+
// interpreter to resolve. Stated explicitly like every other branch rather
|
|
13028
|
+
// than leaning on installAllRuntimes' `|| []` defence.
|
|
13029
|
+
// #4249 (antigravity review): `rollbackInstallerMigrations` was missing here
|
|
13030
|
+
// — every other branch returns it. This PR's own aggregate entrypoint gate
|
|
13031
|
+
// is what makes the gap reachable: an unrelated runtime's invalid entrypoint
|
|
13032
|
+
// now triggers rollbackFinalizedInstallerMigrations for every result in the
|
|
13033
|
+
// batch, and a Copilot result with no rollback function silently skips
|
|
13034
|
+
// reverting Copilot's own installer migrations.
|
|
13035
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: [], rollbackInstallerMigrations };
|
|
12633
13036
|
}
|
|
12634
13037
|
|
|
12635
13038
|
if (plan.installSurface === 'cursor-hooks-json') {
|
|
@@ -12652,7 +13055,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12652
13055
|
// The re-run is retained for parity with the settings.json install path.
|
|
12653
13056
|
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
12654
13057
|
persistActiveProfileMarker();
|
|
12655
|
-
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
13058
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: cursorHookResult.configuredEntrypoints, rollbackInstallerMigrations };
|
|
12656
13059
|
}
|
|
12657
13060
|
|
|
12658
13061
|
if (plan.installSurface === 'profile-marker-only') {
|
|
@@ -12708,6 +13111,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12708
13111
|
const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
|
|
12709
13112
|
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
12710
13113
|
const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
|
|
13114
|
+
configuredEntrypoints.push(...kimiHooksResult.configuredEntrypoints);
|
|
12711
13115
|
if (kimiHooksResult.changed) {
|
|
12712
13116
|
console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
|
|
12713
13117
|
}
|
|
@@ -12764,6 +13168,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12764
13168
|
const windsurfHookResult = writeWindsurfHooksJson(targetDir, src, {
|
|
12765
13169
|
platform: process.platform,
|
|
12766
13170
|
});
|
|
13171
|
+
configuredEntrypoints.push(...windsurfHookResult.configuredEntrypoints);
|
|
12767
13172
|
if (windsurfHookResult.changed) {
|
|
12768
13173
|
console.log(` ${green}✓${reset} Configured Windsurf lifecycle hooks (pre_write_code, pre_run_command)`);
|
|
12769
13174
|
} else {
|
|
@@ -12779,19 +13184,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12779
13184
|
}
|
|
12780
13185
|
|
|
12781
13186
|
persistActiveProfileMarker();
|
|
12782
|
-
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
13187
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints, rollbackInstallerMigrations };
|
|
12783
13188
|
}
|
|
12784
13189
|
|
|
12785
13190
|
if (plan.installSurface === 'cline-rules') {
|
|
12786
13191
|
// Cline uses the `.clinerules/` directory form (issue #787): GSD rules live
|
|
12787
13192
|
// at .clinerules/gsd.md and a PreToolUse lifecycle hook at
|
|
12788
13193
|
// .clinerules/hooks/PreToolUse. Global installs also get ~/.agents/AGENTS.md.
|
|
12789
|
-
writeClineArtifacts(targetDir, isGlobal);
|
|
13194
|
+
const clineArtifacts = writeClineArtifacts(targetDir, isGlobal);
|
|
12790
13195
|
// Re-run the manifest pass: these artifacts are written *after* the earlier
|
|
12791
13196
|
// writeManifest() call, so a second pass is needed to hash-track them.
|
|
12792
13197
|
writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
|
|
12793
13198
|
persistActiveProfileMarker();
|
|
12794
|
-
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
|
13199
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: clineArtifacts.configuredEntrypoints, rollbackInstallerMigrations };
|
|
12795
13200
|
}
|
|
12796
13201
|
|
|
12797
13202
|
// Configure statusline and hooks in settings.json (or settings.local.json for local Claude installs).
|
|
@@ -12916,8 +13321,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12916
13321
|
persistActiveProfileMarker();
|
|
12917
13322
|
// Callers index this result by `runtime` (installAllRuntimes' statusline
|
|
12918
13323
|
// lookup), so every early exit must return the full shape — a bare return
|
|
12919
|
-
// crashes the install rather than skipping one file.
|
|
12920
|
-
|
|
13324
|
+
// crashes the install rather than skipping one file. That includes
|
|
13325
|
+
// configuredEntrypoints/rollbackInstallerMigrations: rollbackFinalizedInstallerMigrations
|
|
13326
|
+
// reads result.rollbackInstallerMigrations unconditionally, and an omitted
|
|
13327
|
+
// field there silently skips this runtime's rollback on a finalize-stage failure.
|
|
13328
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: [], rollbackInstallerMigrations };
|
|
12921
13329
|
}
|
|
12922
13330
|
const settings = validateHookFields(cleanupOrphanedHooks(rawSettings));
|
|
12923
13331
|
// #3002 CR / #3662: rewrite legacy `node .../gsd-*.js` command strings (pre-
|
|
@@ -12939,7 +13347,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12939
13347
|
// runtime's hostBehaviors instead of a hardcoded `runtime === 'antigravity'`
|
|
12940
13348
|
// check inside projectLocalHookPrefix.
|
|
12941
13349
|
const localPrefix = projectLocalHookPrefix({ runtime, dirName, hookPathStyle: _hostBehaviors(runtime).hookPathStyle });
|
|
12942
|
-
const
|
|
13350
|
+
const settingsEntrypoints = [];
|
|
13351
|
+
const hookOpts = {
|
|
13352
|
+
portableHooks: hasPortableHooks,
|
|
13353
|
+
runtime,
|
|
13354
|
+
configPath: settingsPath,
|
|
13355
|
+
// #4249: track unconditionally. Gating on `plan.hooksSurface ===
|
|
13356
|
+
// 'settings-json'` made tracking depend on an unasserted
|
|
13357
|
+
// installSurface/hooksSurface coupling — a descriptor that broke it would
|
|
13358
|
+
// silently drop this runtime out of validation. Everything recorded here
|
|
13359
|
+
// lands in settings.json by construction, and the registered-command
|
|
13360
|
+
// filter below already discards entries no hook actually references.
|
|
13361
|
+
configuredEntrypoints: settingsEntrypoints,
|
|
13362
|
+
};
|
|
12943
13363
|
// #2979: local-install hook commands also use a runner GUI/minimal-PATH
|
|
12944
13364
|
// runtimes can resolve. Bare `node` fails when the host launches the
|
|
12945
13365
|
// runtime with a stripped PATH (Finder/Antigravity/etc) — #3662 replaces
|
|
@@ -12953,19 +13373,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12953
13373
|
// `node` command that recreates the #2979 failure.
|
|
12954
13374
|
const localCmd = (hookFile) => localNodeRunner === null
|
|
12955
13375
|
? null
|
|
12956
|
-
: projectShellCommandText({
|
|
13376
|
+
: hooksSurface.recordConfiguredHookCommand(projectShellCommandText({
|
|
12957
13377
|
runnerToken: localNodeRunner,
|
|
12958
13378
|
argTokens: [`${localPrefix}/hooks/${hookFile}`],
|
|
12959
13379
|
runtime,
|
|
12960
13380
|
platform: process.platform,
|
|
12961
|
-
});
|
|
12962
|
-
const localShellCmd = (hookFile) => buildLocalShellHookCommand({
|
|
13381
|
+
}), targetDir, hookFile, hookOpts);
|
|
13382
|
+
const localShellCmd = (hookFile) => hooksSurface.recordConfiguredHookCommand(buildLocalShellHookCommand({
|
|
12963
13383
|
localPrefix,
|
|
12964
13384
|
hookFile,
|
|
12965
13385
|
bashRunner: localBashRunner,
|
|
12966
13386
|
runtime,
|
|
12967
13387
|
platform: process.platform,
|
|
12968
|
-
});
|
|
13388
|
+
}), targetDir, hookFile, hookOpts);
|
|
12969
13389
|
const statuslineCommand = isGlobal
|
|
12970
13390
|
? buildHookCommand(targetDir, 'gsd-statusline.js', hookOpts)
|
|
12971
13391
|
: localCmd('gsd-statusline.js');
|
|
@@ -13035,6 +13455,31 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
13035
13455
|
? buildHookCommand(targetDir, 'gsd-update-banner.js', hookOpts)
|
|
13036
13456
|
: localCmd('gsd-update-banner.js'));
|
|
13037
13457
|
|
|
13458
|
+
const registeredHookCommands = Object.values(settings.hooks || {})
|
|
13459
|
+
.flatMap(groups => Array.isArray(groups) ? groups : [])
|
|
13460
|
+
.flatMap(group => Array.isArray(group && group.hooks) ? group.hooks : [])
|
|
13461
|
+
.map(hook => hook && hook.command)
|
|
13462
|
+
.filter(command => typeof command === 'string');
|
|
13463
|
+
// #4249: match by the managed script's `/hooks/<basename>` path segment, not
|
|
13464
|
+
// by exact command-string equality. The blocking-guard hooks above register
|
|
13465
|
+
// only-if-absent, so a hook already present from a prior install keeps its
|
|
13466
|
+
// OLD command untouched — but `track()` always records the FRESHLY computed
|
|
13467
|
+
// command for it, which never equals what's actually persisted. Matching on
|
|
13468
|
+
// the segment (present in the persisted command either way, since every
|
|
13469
|
+
// entry.scriptPath is <configDir>/hooks/<name> by construction) keeps an
|
|
13470
|
+
// already-registered, still-active hook in the validated set instead of
|
|
13471
|
+
// silently dropping it (#4154 Blocker) — anchored on `/hooks/` rather than a
|
|
13472
|
+
// bare basename so an unrelated user command that merely mentions the same
|
|
13473
|
+
// filename can't false-positive into GSD's validated set.
|
|
13474
|
+
configuredEntrypoints.push(
|
|
13475
|
+
...settingsEntrypoints.filter(entry => {
|
|
13476
|
+
const hooksSegment = '/hooks/' + path.basename(entry.scriptPath);
|
|
13477
|
+
return registeredHookCommands.some(command => command.includes(hooksSegment));
|
|
13478
|
+
}),
|
|
13479
|
+
);
|
|
13480
|
+
const statuslineEntrypoints = settingsEntrypoints.filter(entry => entry.command === statuslineCommand);
|
|
13481
|
+
const updateBannerEntrypoints = settingsEntrypoints.filter(entry => entry.command === updateBannerCommand);
|
|
13482
|
+
|
|
13038
13483
|
// #683: Set worktree.baseRef:"head" in settings.local.json for local Claude installs.
|
|
13039
13484
|
// Both fresh and upgrade paths apply only when worktrees are enabled for the project.
|
|
13040
13485
|
// Never applies to global installs, non-Claude runtimes, or when the user already
|
|
@@ -13103,15 +13548,65 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
13103
13548
|
settings,
|
|
13104
13549
|
statuslineCommand,
|
|
13105
13550
|
updateBannerCommand,
|
|
13551
|
+
statuslineEntrypoints,
|
|
13552
|
+
updateBannerEntrypoints,
|
|
13106
13553
|
runtime,
|
|
13107
13554
|
configDir: targetDir,
|
|
13108
13555
|
rollbackInstallerMigrations,
|
|
13556
|
+
configuredEntrypoints,
|
|
13109
13557
|
};
|
|
13110
13558
|
}
|
|
13111
13559
|
|
|
13112
|
-
|
|
13113
|
-
|
|
13114
|
-
|
|
13560
|
+
// #4249 (review, Major): rollback consequence differs by runtime surface —
|
|
13561
|
+
// see docs/how-to/update-gsd.md's rollback-matrix paragraph, which this
|
|
13562
|
+
// mirrors. Codex reverts (pre-install snapshot restore); Cursor/Windsurf/
|
|
13563
|
+
// Kimi/Kimi Code/Cline already wrote their config file inside install(),
|
|
13564
|
+
// ahead of this gate, with no revert path, so it is left on disk broken;
|
|
13565
|
+
// every other (settings.json-based) runtime writes strictly after this gate,
|
|
13566
|
+
// so a failure here means nothing new was persisted for it.
|
|
13567
|
+
const ENTRYPOINT_LEFT_UNREVERTED_RUNTIMES = new Set(['cursor', 'windsurf', 'kimi', 'kimi-code', 'cline']);
|
|
13568
|
+
function describeEntrypointConsequence(invalidRuntime) {
|
|
13569
|
+
if (invalidRuntime === 'codex') return 'reverted: its pre-install snapshot was restored';
|
|
13570
|
+
if (ENTRYPOINT_LEFT_UNREVERTED_RUNTIMES.has(invalidRuntime)) return 'NOT reverted: its config file is already written and was left on disk — fix the reported path and rerun install';
|
|
13571
|
+
return 'not persisted: this runtime writes its config after this check';
|
|
13572
|
+
}
|
|
13573
|
+
|
|
13574
|
+
function assertConfiguredEntrypoints(entries) {
|
|
13575
|
+
// #4249: some writers push the same (configPath, scriptPath) pair more than
|
|
13576
|
+
// once (e.g. Kimi's context-monitor hook registered under several events,
|
|
13577
|
+
// or the portable resolver script shared by every portable JS hook) — keep
|
|
13578
|
+
// one so a broken entry is reported once, not once per duplicate.
|
|
13579
|
+
const seen = new Set();
|
|
13580
|
+
const deduped = (entries || []).filter((entry) => {
|
|
13581
|
+
const key = JSON.stringify([entry.configPath, entry.scriptPath]);
|
|
13582
|
+
if (seen.has(key)) return false;
|
|
13583
|
+
seen.add(key);
|
|
13584
|
+
return true;
|
|
13585
|
+
});
|
|
13586
|
+
const validation = hooksSurface.validateConfiguredEntrypoints(deduped);
|
|
13587
|
+
if (validation.ok) return;
|
|
13588
|
+
|
|
13589
|
+
const error = new Error(
|
|
13590
|
+
// #4249: lead each entry with its runtime, and name the actual consequence
|
|
13591
|
+
// for that runtime (review, Major) — the aggregate gate is all-or-nothing
|
|
13592
|
+
// across every runtime being installed, and a failure here can revert a
|
|
13593
|
+
// runtime whose own entrypoints were fine (see
|
|
13594
|
+
// rollbackFinalizedInstallerMigrations) while leaving another runtime's
|
|
13595
|
+
// already-written config broken on disk with no revert at all, so an
|
|
13596
|
+
// operator reading only this message must be able to tell WHOSE
|
|
13597
|
+
// entrypoint broke and WHAT that means for their config, not just that
|
|
13598
|
+
// something did.
|
|
13599
|
+
`Configured entrypoint validation failed: ${validation.invalid.map(({ runtime: invalidRuntime, role, path: invalidPath, reason }) => `${invalidRuntime} ${role} ${invalidPath} (${reason}) [${describeEntrypointConsequence(invalidRuntime)}]`).join(', ')}`,
|
|
13600
|
+
);
|
|
13601
|
+
error.configuredEntrypointValidation = validation;
|
|
13602
|
+
throw error;
|
|
13603
|
+
}
|
|
13604
|
+
|
|
13605
|
+
// #4249: `bannerOpts.configuredEntrypoints` is the ONLY source assertConfiguredEntrypoints
|
|
13606
|
+
// checks below — a caller that omits it (or calls finishInstall directly instead of
|
|
13607
|
+
// through installAllRuntimes) gets zero entrypoint validation, silently. installAllRuntimes
|
|
13608
|
+
// always passes the full set (per-runtime entries plus statusline/updateBanner); any other
|
|
13609
|
+
// caller must do the same for this gate to mean anything.
|
|
13115
13610
|
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = DEFAULT_RUNTIME, isGlobal = true, configDir = null, bannerOpts = {}) {
|
|
13116
13611
|
// #2093: isKilo dropped — the Kilo permissions-writer call below is gated
|
|
13117
13612
|
// on plan.finishPermissionWriter === 'kilo' (descriptor-driven), not this flag.
|
|
@@ -13125,6 +13620,19 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
13125
13620
|
const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
|
|
13126
13621
|
const plan = resolveInstallPlan(runtime);
|
|
13127
13622
|
|
|
13623
|
+
// #4249 Major: validate BEFORE this function's own settings.json write (and
|
|
13624
|
+
// before writeNonClaudeDefaults) instead of after. Cursor/Windsurf/Kimi/Cline
|
|
13625
|
+
// already persisted their config inside install() by this point, with no
|
|
13626
|
+
// rollback path covering those writes; Codex also persists inside install()
|
|
13627
|
+
// but its rollback binds to a full pre-install snapshot restore, so it IS
|
|
13628
|
+
// covered (see docs/how-to/update-gsd.md). For the settings-json surface
|
|
13629
|
+
// this ordering means a failing validation never reaches this function's
|
|
13630
|
+
// own write at all. On the production path this is a redundant backstop —
|
|
13631
|
+
// installAllRuntimes's own aggregate assertConfiguredEntrypoints call
|
|
13632
|
+
// already validates the superset before finishInstall runs for any
|
|
13633
|
+
// runtime — kept for a caller that invokes finishInstall directly.
|
|
13634
|
+
assertConfiguredEntrypoints(bannerOpts.configuredEntrypoints);
|
|
13635
|
+
|
|
13128
13636
|
if (shouldInstallStatusline && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
|
|
13129
13637
|
if (!isGlobal && !forceStatusline) {
|
|
13130
13638
|
// Local installs skip statusLine by default: repo settings.json takes precedence over
|
|
@@ -14022,10 +14530,32 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
14022
14530
|
|
|
14023
14531
|
const rollbackFinalizedInstallerMigrations = (error) => {
|
|
14024
14532
|
const rollbackFailures = [];
|
|
14533
|
+
// #4249: this discriminates on the error's KIND, never on which runtime
|
|
14534
|
+
// owns the failing entrypoint. `wide` is true for ANY entrypoint-validation
|
|
14535
|
+
// failure from ANY runtime, by design: the aggregate gate exists so a
|
|
14536
|
+
// multi-runtime install cannot report success while one of its entrypoints
|
|
14537
|
+
// is broken, so an invalid Cline entrypoint reverts Codex's pre-install
|
|
14538
|
+
// snapshot too — even though Codex itself was fine and its own "Done!"
|
|
14539
|
+
// summary already printed. tests/configured-entrypoint-validation.test.cjs
|
|
14540
|
+
// ('an aggregate entrypoint validation failure rolls the Codex install
|
|
14541
|
+
// back') exercises exactly that, and it is the all-or-nothing behaviour
|
|
14542
|
+
// docs/how-to/update-gsd.md documents.
|
|
14543
|
+
//
|
|
14544
|
+
// What this narrows is the OTHER axis: a finalize-stage exception that is
|
|
14545
|
+
// not an entrypoint-validation failure at all — e.g. a sibling runtime's
|
|
14546
|
+
// permission-config write dying with EACCES — gets only the
|
|
14547
|
+
// installer-migrations-only rollback that Phase 4 specifies
|
|
14548
|
+
// (docs/installer-migrations.md#phase-4-installupdate-integration).
|
|
14549
|
+
// Un-installing (and, on update, downgrading) an already-"Done!" Codex over
|
|
14550
|
+
// an unrelated error is not an outcome any doc promises, while the sibling
|
|
14551
|
+
// surfaces that write config inside install() would keep theirs regardless.
|
|
14552
|
+
const wide = !!(error && error.configuredEntrypointValidation);
|
|
14025
14553
|
for (const result of [...results].reverse()) {
|
|
14026
|
-
if (!result
|
|
14554
|
+
if (!result) continue;
|
|
14555
|
+
const rollback = (wide && result.rollbackPreInstallSnapshot) || result.rollbackInstallerMigrations;
|
|
14556
|
+
if (typeof rollback !== 'function') continue;
|
|
14027
14557
|
try {
|
|
14028
|
-
|
|
14558
|
+
rollback();
|
|
14029
14559
|
} catch (rollbackError) {
|
|
14030
14560
|
rollbackFailures.push({
|
|
14031
14561
|
runtime: result.runtime,
|
|
@@ -14053,6 +14583,19 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
14053
14583
|
|
|
14054
14584
|
const finalize = (shouldInstallStatusline, shouldInstallBanner) => {
|
|
14055
14585
|
try {
|
|
14586
|
+
const selectedConfiguredEntrypoints = (result) => {
|
|
14587
|
+
if (!result || result.skipped) return [];
|
|
14588
|
+
const useStatusline = statuslineRuntimes.includes(result.runtime)
|
|
14589
|
+
&& shouldInstallStatusline
|
|
14590
|
+
&& (isGlobal || forceStatusline);
|
|
14591
|
+
return [
|
|
14592
|
+
...(result.configuredEntrypoints || []),
|
|
14593
|
+
...(useStatusline ? (result.statuslineEntrypoints || []) : []),
|
|
14594
|
+
...(shouldInstallBanner ? (result.updateBannerEntrypoints || []) : []),
|
|
14595
|
+
];
|
|
14596
|
+
};
|
|
14597
|
+
assertConfiguredEntrypoints(results.flatMap(selectedConfiguredEntrypoints));
|
|
14598
|
+
|
|
14056
14599
|
const printSummaries = () => {
|
|
14057
14600
|
for (const result of results) {
|
|
14058
14601
|
if (result && result.skipped) continue;
|
|
@@ -14066,7 +14609,11 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
14066
14609
|
result.runtime,
|
|
14067
14610
|
isGlobal,
|
|
14068
14611
|
result.configDir,
|
|
14069
|
-
{
|
|
14612
|
+
{
|
|
14613
|
+
shouldInstallBanner: !!shouldInstallBanner,
|
|
14614
|
+
bannerCommand: result.updateBannerCommand,
|
|
14615
|
+
configuredEntrypoints: selectedConfiguredEntrypoints(result),
|
|
14616
|
+
}
|
|
14070
14617
|
);
|
|
14071
14618
|
}
|
|
14072
14619
|
};
|