diff --git a/.changeset/README.md b/.changeset/README.md index 7903c406e..94310153f 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -37,8 +37,14 @@ PRs that legitimately have no user-facing impact can add the `no-changelog` labe ## At release time +Promotion is **automatic**. The release workflow's `finalize` job runs: + ```bash -node scripts/changeset/cli.cjs render --version vX.Y.Z --date YYYY-MM-DD +node scripts/changeset/cli.cjs render --version vX.Y.Z --date YYYY-MM-DD --allow-empty ``` -Reads every fragment, groups bullets by `type:`, replaces `## [Unreleased]` with a new `## [vX.Y.Z] - YYYY-MM-DD` block, opens a fresh `## [Unreleased]` above, deletes consumed fragments. Idempotent. +This reads every fragment, groups bullets by `type:`, replaces `## [Unreleased]` with a new `## [vX.Y.Z] - YYYY-MM-DD` block, opens a fresh `## [Unreleased]` above, and deletes consumed fragments. The `--allow-empty` flag ensures a no-change release still gets a dated heading (with a `_No notable changes._` placeholder). A subsequent `verify` step confirms the promotion landed correctly. Maintainers do **not** run this by hand. + +## Archived fragments + +`.changeset/archived/` holds fragments for already-shipped releases (≤ 1.3.1), retained for provenance. Their content was hand-curated into the dated `## [1.x.y]` sections of `CHANGELOG.md` during the #690 backfill — they were never consumed by `render`. All changeset tooling enumerates `.changeset/` non-recursively, so archived fragments are never picked up or rendered. Do not move them back to the top level. diff --git a/.changeset/105-no-auto-switch-worktrees-false.md b/.changeset/archived/105-no-auto-switch-worktrees-false.md similarity index 100% rename from .changeset/105-no-auto-switch-worktrees-false.md rename to .changeset/archived/105-no-auto-switch-worktrees-false.md diff --git a/.changeset/113-malicious-markdown-links.md b/.changeset/archived/113-malicious-markdown-links.md similarity index 100% rename from .changeset/113-malicious-markdown-links.md rename to .changeset/archived/113-malicious-markdown-links.md diff --git a/.changeset/117-npm-bootstrap.md b/.changeset/archived/117-npm-bootstrap.md similarity index 100% rename from .changeset/117-npm-bootstrap.md rename to .changeset/archived/117-npm-bootstrap.md diff --git a/.changeset/130-finishinstall-testmode-guard.md b/.changeset/archived/130-finishinstall-testmode-guard.md similarity index 100% rename from .changeset/130-finishinstall-testmode-guard.md rename to .changeset/archived/130-finishinstall-testmode-guard.md diff --git a/.changeset/138-nyquist-config-get-default.md b/.changeset/archived/138-nyquist-config-get-default.md similarity index 100% rename from .changeset/138-nyquist-config-get-default.md rename to .changeset/archived/138-nyquist-config-get-default.md diff --git a/.changeset/14-progress-auto-flag.md b/.changeset/archived/14-progress-auto-flag.md similarity index 100% rename from .changeset/14-progress-auto-flag.md rename to .changeset/archived/14-progress-auto-flag.md diff --git a/.changeset/142-runtime-name-policy-aliases.md b/.changeset/archived/142-runtime-name-policy-aliases.md similarity index 100% rename from .changeset/142-runtime-name-policy-aliases.md rename to .changeset/archived/142-runtime-name-policy-aliases.md diff --git a/.changeset/160-route0-resume-incomplete-phase.md b/.changeset/archived/160-route0-resume-incomplete-phase.md similarity index 100% rename from .changeset/160-route0-resume-incomplete-phase.md rename to .changeset/archived/160-route0-resume-incomplete-phase.md diff --git a/.changeset/166-windows-claude-sh-hook-command.md b/.changeset/archived/166-windows-claude-sh-hook-command.md similarity index 100% rename from .changeset/166-windows-claude-sh-hook-command.md rename to .changeset/archived/166-windows-claude-sh-hook-command.md diff --git a/.changeset/167-query-meta-command.md b/.changeset/archived/167-query-meta-command.md similarity index 100% rename from .changeset/167-query-meta-command.md rename to .changeset/archived/167-query-meta-command.md diff --git a/.changeset/170-workflow-fallback-package-hint.md b/.changeset/archived/170-workflow-fallback-package-hint.md similarity index 100% rename from .changeset/170-workflow-fallback-package-hint.md rename to .changeset/archived/170-workflow-fallback-package-hint.md diff --git a/.changeset/176-hub-p1.2-review-findings.md b/.changeset/archived/176-hub-p1.2-review-findings.md similarity index 100% rename from .changeset/176-hub-p1.2-review-findings.md rename to .changeset/archived/176-hub-p1.2-review-findings.md diff --git a/.changeset/176-typed-result-discriminated-union.md b/.changeset/archived/176-typed-result-discriminated-union.md similarity index 100% rename from .changeset/176-typed-result-discriminated-union.md rename to .changeset/archived/176-typed-result-discriminated-union.md diff --git a/.changeset/177-observability-seam.md b/.changeset/archived/177-observability-seam.md similarity index 100% rename from .changeset/177-observability-seam.md rename to .changeset/archived/177-observability-seam.md diff --git a/.changeset/178-trace-id-propagation.md b/.changeset/archived/178-trace-id-propagation.md similarity index 100% rename from .changeset/178-trace-id-propagation.md rename to .changeset/archived/178-trace-id-propagation.md diff --git a/.changeset/191-retire-sdk-package-seam.md b/.changeset/archived/191-retire-sdk-package-seam.md similarity index 100% rename from .changeset/191-retire-sdk-package-seam.md rename to .changeset/archived/191-retire-sdk-package-seam.md diff --git a/.changeset/192-retire-sdk-release-pipeline.md b/.changeset/archived/192-retire-sdk-release-pipeline.md similarity index 100% rename from .changeset/192-retire-sdk-release-pipeline.md rename to .changeset/archived/192-retire-sdk-release-pipeline.md diff --git a/.changeset/195-workflow-gsd-tools-query.md b/.changeset/archived/195-workflow-gsd-tools-query.md similarity index 100% rename from .changeset/195-workflow-gsd-tools-query.md rename to .changeset/archived/195-workflow-gsd-tools-query.md diff --git a/.changeset/211-launcher-claude-home-fallback.md b/.changeset/archived/211-launcher-claude-home-fallback.md similarity index 100% rename from .changeset/211-launcher-claude-home-fallback.md rename to .changeset/archived/211-launcher-claude-home-fallback.md diff --git a/.changeset/213-antigravity-2-runtime-dirs.md b/.changeset/archived/213-antigravity-2-runtime-dirs.md similarity index 100% rename from .changeset/213-antigravity-2-runtime-dirs.md rename to .changeset/archived/213-antigravity-2-runtime-dirs.md diff --git a/.changeset/245-summary-rescue-copy-failure.md b/.changeset/archived/245-summary-rescue-copy-failure.md similarity index 100% rename from .changeset/245-summary-rescue-copy-failure.md rename to .changeset/archived/245-summary-rescue-copy-failure.md diff --git a/.changeset/2937-statusline-context-position.md b/.changeset/archived/2937-statusline-context-position.md similarity index 100% rename from .changeset/2937-statusline-context-position.md rename to .changeset/archived/2937-statusline-context-position.md diff --git a/.changeset/3033-sdk-flag-wired.md b/.changeset/archived/3033-sdk-flag-wired.md similarity index 100% rename from .changeset/3033-sdk-flag-wired.md rename to .changeset/archived/3033-sdk-flag-wired.md diff --git a/.changeset/308-websearch-timeout-retry.md b/.changeset/archived/308-websearch-timeout-retry.md similarity index 100% rename from .changeset/308-websearch-timeout-retry.md rename to .changeset/archived/308-websearch-timeout-retry.md diff --git a/.changeset/3095-quota-failure-classification.md b/.changeset/archived/3095-quota-failure-classification.md similarity index 100% rename from .changeset/3095-quota-failure-classification.md rename to .changeset/archived/3095-quota-failure-classification.md diff --git a/.changeset/311-subrepo-routing-index.md b/.changeset/archived/311-subrepo-routing-index.md similarity index 100% rename from .changeset/311-subrepo-routing-index.md rename to .changeset/archived/311-subrepo-routing-index.md diff --git a/.changeset/312-arg-projection-single-pass.md b/.changeset/archived/312-arg-projection-single-pass.md similarity index 100% rename from .changeset/312-arg-projection-single-pass.md rename to .changeset/archived/312-arg-projection-single-pass.md diff --git a/.changeset/314-roadmap-plan-id-map.md b/.changeset/archived/314-roadmap-plan-id-map.md similarity index 100% rename from .changeset/314-roadmap-plan-id-map.md rename to .changeset/archived/314-roadmap-plan-id-map.md diff --git a/.changeset/315-subrepo-detect-memo.md b/.changeset/archived/315-subrepo-detect-memo.md similarity index 100% rename from .changeset/315-subrepo-detect-memo.md rename to .changeset/archived/315-subrepo-detect-memo.md diff --git a/.changeset/3156-plan-phase-opencode-dispatch.md b/.changeset/archived/3156-plan-phase-opencode-dispatch.md similarity index 100% rename from .changeset/3156-plan-phase-opencode-dispatch.md rename to .changeset/archived/3156-plan-phase-opencode-dispatch.md diff --git a/.changeset/316-state-lock-buffer-hoist.md b/.changeset/archived/316-state-lock-buffer-hoist.md similarity index 100% rename from .changeset/316-state-lock-buffer-hoist.md rename to .changeset/archived/316-state-lock-buffer-hoist.md diff --git a/.changeset/3166-graphify-inline-build.md b/.changeset/archived/3166-graphify-inline-build.md similarity index 100% rename from .changeset/3166-graphify-inline-build.md rename to .changeset/archived/3166-graphify-inline-build.md diff --git a/.changeset/3170-graphify-commit-staleness.md b/.changeset/archived/3170-graphify-commit-staleness.md similarity index 100% rename from .changeset/3170-graphify-commit-staleness.md rename to .changeset/archived/3170-graphify-commit-staleness.md diff --git a/.changeset/3184-phase-uat-passed.md b/.changeset/archived/3184-phase-uat-passed.md similarity index 100% rename from .changeset/3184-phase-uat-passed.md rename to .changeset/archived/3184-phase-uat-passed.md diff --git a/.changeset/3195-quick-resurrection-guard.md b/.changeset/archived/3195-quick-resurrection-guard.md similarity index 100% rename from .changeset/3195-quick-resurrection-guard.md rename to .changeset/archived/3195-quick-resurrection-guard.md diff --git a/.changeset/3198-retrospective-canonical.md b/.changeset/archived/3198-retrospective-canonical.md similarity index 100% rename from .changeset/3198-retrospective-canonical.md rename to .changeset/archived/3198-retrospective-canonical.md diff --git a/.changeset/320-hoist-state-table-regex.md b/.changeset/archived/320-hoist-state-table-regex.md similarity index 100% rename from .changeset/320-hoist-state-table-regex.md rename to .changeset/archived/320-hoist-state-table-regex.md diff --git a/.changeset/3210-fallow-structural-review.md b/.changeset/archived/3210-fallow-structural-review.md similarity index 100% rename from .changeset/3210-fallow-structural-review.md rename to .changeset/archived/3210-fallow-structural-review.md diff --git a/.changeset/3251-non-family-aliases.md b/.changeset/archived/3251-non-family-aliases.md similarity index 100% rename from .changeset/3251-non-family-aliases.md rename to .changeset/archived/3251-non-family-aliases.md diff --git a/.changeset/3262-extract-scan-phase-plans.md b/.changeset/archived/3262-extract-scan-phase-plans.md similarity index 100% rename from .changeset/3262-extract-scan-phase-plans.md rename to .changeset/archived/3262-extract-scan-phase-plans.md diff --git a/.changeset/3271-sdk-adr-structure.md b/.changeset/archived/3271-sdk-adr-structure.md similarity index 100% rename from .changeset/3271-sdk-adr-structure.md rename to .changeset/archived/3271-sdk-adr-structure.md diff --git a/.changeset/3298-phase-dir-prefix-drift-workflows.md b/.changeset/archived/3298-phase-dir-prefix-drift-workflows.md similarity index 100% rename from .changeset/3298-phase-dir-prefix-drift-workflows.md rename to .changeset/archived/3298-phase-dir-prefix-drift-workflows.md diff --git a/.changeset/33-model-profile-adaptive.md b/.changeset/archived/33-model-profile-adaptive.md similarity index 100% rename from .changeset/33-model-profile-adaptive.md rename to .changeset/archived/33-model-profile-adaptive.md diff --git a/.changeset/3312-sdk-first-architecture-seams.md b/.changeset/archived/3312-sdk-first-architecture-seams.md similarity index 100% rename from .changeset/3312-sdk-first-architecture-seams.md rename to .changeset/archived/3312-sdk-first-architecture-seams.md diff --git a/.changeset/3346-codex-aot-toml-key.md b/.changeset/archived/3346-codex-aot-toml-key.md similarity index 100% rename from .changeset/3346-codex-aot-toml-key.md rename to .changeset/archived/3346-codex-aot-toml-key.md diff --git a/.changeset/3347-graphify-auto-update-hook.md b/.changeset/archived/3347-graphify-auto-update-hook.md similarity index 100% rename from .changeset/3347-graphify-auto-update-hook.md rename to .changeset/archived/3347-graphify-auto-update-hook.md diff --git a/.changeset/338-local-install-settings-local-json.md b/.changeset/archived/338-local-install-settings-local-json.md similarity index 100% rename from .changeset/338-local-install-settings-local-json.md rename to .changeset/archived/338-local-install-settings-local-json.md diff --git a/.changeset/3409-phase-remove-force-position.md b/.changeset/archived/3409-phase-remove-force-position.md similarity index 100% rename from .changeset/3409-phase-remove-force-position.md rename to .changeset/archived/3409-phase-remove-force-position.md diff --git a/.changeset/3450-shell-projection-merge-repair.md b/.changeset/archived/3450-shell-projection-merge-repair.md similarity index 100% rename from .changeset/3450-shell-projection-merge-repair.md rename to .changeset/archived/3450-shell-projection-merge-repair.md diff --git a/.changeset/3488-decimal-phase-depends-on.md b/.changeset/archived/3488-decimal-phase-depends-on.md similarity index 100% rename from .changeset/3488-decimal-phase-depends-on.md rename to .changeset/archived/3488-decimal-phase-depends-on.md diff --git a/.changeset/3489-complete-phase-idempotent.md b/.changeset/archived/3489-complete-phase-idempotent.md similarity index 100% rename from .changeset/3489-complete-phase-idempotent.md rename to .changeset/archived/3489-complete-phase-idempotent.md diff --git a/.changeset/3491-nested-git-detection.md b/.changeset/archived/3491-nested-git-detection.md similarity index 100% rename from .changeset/3491-nested-git-detection.md rename to .changeset/archived/3491-nested-git-detection.md diff --git a/.changeset/3493-extract-milestone-generic-heading.md b/.changeset/archived/3493-extract-milestone-generic-heading.md similarity index 100% rename from .changeset/3493-extract-milestone-generic-heading.md rename to .changeset/archived/3493-extract-milestone-generic-heading.md diff --git a/.changeset/3517-phase-complete-state-staleness.md b/.changeset/archived/3517-phase-complete-state-staleness.md similarity index 100% rename from .changeset/3517-phase-complete-state-staleness.md rename to .changeset/archived/3517-phase-complete-state-staleness.md diff --git a/.changeset/3537-phase-regex-fanout.md b/.changeset/archived/3537-phase-regex-fanout.md similarity index 100% rename from .changeset/3537-phase-regex-fanout.md rename to .changeset/archived/3537-phase-regex-fanout.md diff --git a/.changeset/3541-installer-migration-prompt-user-resolution.md b/.changeset/archived/3541-installer-migration-prompt-user-resolution.md similarity index 100% rename from .changeset/3541-installer-migration-prompt-user-resolution.md rename to .changeset/archived/3541-installer-migration-prompt-user-resolution.md diff --git a/.changeset/3542-executor-git-stash-prohibition.md b/.changeset/archived/3542-executor-git-stash-prohibition.md similarity index 100% rename from .changeset/3542-executor-git-stash-prohibition.md rename to .changeset/archived/3542-executor-git-stash-prohibition.md diff --git a/.changeset/3544-workstream-inventory-builder.md b/.changeset/archived/3544-workstream-inventory-builder.md similarity index 100% rename from .changeset/3544-workstream-inventory-builder.md rename to .changeset/archived/3544-workstream-inventory-builder.md diff --git a/.changeset/3562-codex-install-skill-surface.md b/.changeset/archived/3562-codex-install-skill-surface.md similarity index 100% rename from .changeset/3562-codex-install-skill-surface.md rename to .changeset/archived/3562-codex-install-skill-surface.md diff --git a/.changeset/3566-codex-hooks-canonical-feature-key.md b/.changeset/archived/3566-codex-hooks-canonical-feature-key.md similarity index 100% rename from .changeset/3566-codex-hooks-canonical-feature-key.md rename to .changeset/archived/3566-codex-hooks-canonical-feature-key.md diff --git a/.changeset/3571-configuration-manifest-install-layout.md b/.changeset/archived/3571-configuration-manifest-install-layout.md similarity index 100% rename from .changeset/3571-configuration-manifest-install-layout.md rename to .changeset/archived/3571-configuration-manifest-install-layout.md diff --git a/.changeset/3577-adr-violations-and-validation-port.md b/.changeset/archived/3577-adr-violations-and-validation-port.md similarity index 100% rename from .changeset/3577-adr-violations-and-validation-port.md rename to .changeset/archived/3577-adr-violations-and-validation-port.md diff --git a/.changeset/3577-config-ensure-section-parity.md b/.changeset/archived/3577-config-ensure-section-parity.md similarity index 100% rename from .changeset/3577-config-ensure-section-parity.md rename to .changeset/archived/3577-config-ensure-section-parity.md diff --git a/.changeset/3577-docker-test-fixup.md b/.changeset/archived/3577-docker-test-fixup.md similarity index 100% rename from .changeset/3577-docker-test-fixup.md rename to .changeset/archived/3577-docker-test-fixup.md diff --git a/.changeset/3589-planning-paths-workstream-validation.md b/.changeset/archived/3589-planning-paths-workstream-validation.md similarity index 100% rename from .changeset/3589-planning-paths-workstream-validation.md rename to .changeset/archived/3589-planning-paths-workstream-validation.md diff --git a/.changeset/3591-gsdtools-native-workstream.md b/.changeset/archived/3591-gsdtools-native-workstream.md similarity index 100% rename from .changeset/3591-gsdtools-native-workstream.md rename to .changeset/archived/3591-gsdtools-native-workstream.md diff --git a/.changeset/3593-cli-negative-matrix-harness.md b/.changeset/archived/3593-cli-negative-matrix-harness.md similarity index 100% rename from .changeset/3593-cli-negative-matrix-harness.md rename to .changeset/archived/3593-cli-negative-matrix-harness.md diff --git a/.changeset/3594-parser-adversarial-fixtures.md b/.changeset/archived/3594-parser-adversarial-fixtures.md similarity index 100% rename from .changeset/3594-parser-adversarial-fixtures.md rename to .changeset/archived/3594-parser-adversarial-fixtures.md diff --git a/.changeset/3595-fs-fault-injection-atomic-write.md b/.changeset/archived/3595-fs-fault-injection-atomic-write.md similarity index 100% rename from .changeset/3595-fs-fault-injection-atomic-write.md rename to .changeset/archived/3595-fs-fault-injection-atomic-write.md diff --git a/.changeset/3597-test-suite-split-node-matrix.md b/.changeset/archived/3597-test-suite-split-node-matrix.md similarity index 100% rename from .changeset/3597-test-suite-split-node-matrix.md rename to .changeset/archived/3597-test-suite-split-node-matrix.md diff --git a/.changeset/3597-windows-argv-overflow.md b/.changeset/archived/3597-windows-argv-overflow.md similarity index 100% rename from .changeset/3597-windows-argv-overflow.md rename to .changeset/archived/3597-windows-argv-overflow.md diff --git a/.changeset/3599-roadmap-get-phase-project-code-prefix.md b/.changeset/archived/3599-roadmap-get-phase-project-code-prefix.md similarity index 100% rename from .changeset/3599-roadmap-get-phase-project-code-prefix.md rename to .changeset/archived/3599-roadmap-get-phase-project-code-prefix.md diff --git a/.changeset/3600-milestone-phase-filter-project-code.md b/.changeset/archived/3600-milestone-phase-filter-project-code.md similarity index 100% rename from .changeset/3600-milestone-phase-filter-project-code.md rename to .changeset/archived/3600-milestone-phase-filter-project-code.md diff --git a/.changeset/3601-phase-remove-decimal-section-loss.md b/.changeset/archived/3601-phase-remove-decimal-section-loss.md similarity index 100% rename from .changeset/3601-phase-remove-decimal-section-loss.md rename to .changeset/archived/3601-phase-remove-decimal-section-loss.md diff --git a/.changeset/3602-phase-remove-slugged-plan-refs.md b/.changeset/archived/3602-phase-remove-slugged-plan-refs.md similarity index 100% rename from .changeset/3602-phase-remove-slugged-plan-refs.md rename to .changeset/archived/3602-phase-remove-slugged-plan-refs.md diff --git a/.changeset/3605-stale-agent-command-refs.md b/.changeset/archived/3605-stale-agent-command-refs.md similarity index 100% rename from .changeset/3605-stale-agent-command-refs.md rename to .changeset/archived/3605-stale-agent-command-refs.md diff --git a/.changeset/3608-antigravity-update-runtime.md b/.changeset/archived/3608-antigravity-update-runtime.md similarity index 100% rename from .changeset/3608-antigravity-update-runtime.md rename to .changeset/archived/3608-antigravity-update-runtime.md diff --git a/.changeset/3610-codex-install-bundled-hooks-blocker.md b/.changeset/archived/3610-codex-install-bundled-hooks-blocker.md similarity index 100% rename from .changeset/3610-codex-install-bundled-hooks-blocker.md rename to .changeset/archived/3610-codex-install-bundled-hooks-blocker.md diff --git a/.changeset/3621-cherry-pick-test-fixtures.md b/.changeset/archived/3621-cherry-pick-test-fixtures.md similarity index 100% rename from .changeset/3621-cherry-pick-test-fixtures.md rename to .changeset/archived/3621-cherry-pick-test-fixtures.md diff --git a/.changeset/3628-bundled-hook-whitelist.md b/.changeset/archived/3628-bundled-hook-whitelist.md similarity index 100% rename from .changeset/3628-bundled-hook-whitelist.md rename to .changeset/archived/3628-bundled-hook-whitelist.md diff --git a/.changeset/3643-resolve-model-claude-runtime.md b/.changeset/archived/3643-resolve-model-claude-runtime.md similarity index 100% rename from .changeset/3643-resolve-model-claude-runtime.md rename to .changeset/archived/3643-resolve-model-claude-runtime.md diff --git a/.changeset/3653-graphify-hook-sdk-commit-visibility.md b/.changeset/archived/3653-graphify-hook-sdk-commit-visibility.md similarity index 100% rename from .changeset/3653-graphify-hook-sdk-commit-visibility.md rename to .changeset/archived/3653-graphify-hook-sdk-commit-visibility.md diff --git a/.changeset/3689-resume-glob-nomatch-fix.md b/.changeset/archived/3689-resume-glob-nomatch-fix.md similarity index 100% rename from .changeset/3689-resume-glob-nomatch-fix.md rename to .changeset/archived/3689-resume-glob-nomatch-fix.md diff --git a/.changeset/370-affected-tests-exclude-install-slow.md b/.changeset/archived/370-affected-tests-exclude-install-slow.md similarity index 100% rename from .changeset/370-affected-tests-exclude-install-slow.md rename to .changeset/archived/370-affected-tests-exclude-install-slow.md diff --git a/.changeset/3707-worktree-orphan-reap.md b/.changeset/archived/3707-worktree-orphan-reap.md similarity index 100% rename from .changeset/3707-worktree-orphan-reap.md rename to .changeset/archived/3707-worktree-orphan-reap.md diff --git a/.changeset/3740-consolidate-phase-tests.md b/.changeset/archived/3740-consolidate-phase-tests.md similarity index 100% rename from .changeset/3740-consolidate-phase-tests.md rename to .changeset/archived/3740-consolidate-phase-tests.md diff --git a/.changeset/3742-consolidate-worktree-tests.md b/.changeset/archived/3742-consolidate-worktree-tests.md similarity index 100% rename from .changeset/3742-consolidate-worktree-tests.md rename to .changeset/archived/3742-consolidate-worktree-tests.md diff --git a/.changeset/3753-consolidate-milestone-tests.md b/.changeset/archived/3753-consolidate-milestone-tests.md similarity index 100% rename from .changeset/3753-consolidate-milestone-tests.md rename to .changeset/archived/3753-consolidate-milestone-tests.md diff --git a/.changeset/3755-consolidate-init-tests.md b/.changeset/archived/3755-consolidate-init-tests.md similarity index 100% rename from .changeset/3755-consolidate-init-tests.md rename to .changeset/archived/3755-consolidate-init-tests.md diff --git a/.changeset/3757-consolidate-runtime-artifact-layout.md b/.changeset/archived/3757-consolidate-runtime-artifact-layout.md similarity index 100% rename from .changeset/3757-consolidate-runtime-artifact-layout.md rename to .changeset/archived/3757-consolidate-runtime-artifact-layout.md diff --git a/.changeset/3758-consolidate-installer-tests.md b/.changeset/archived/3758-consolidate-installer-tests.md similarity index 100% rename from .changeset/3758-consolidate-installer-tests.md rename to .changeset/archived/3758-consolidate-installer-tests.md diff --git a/.changeset/376-claude-js-hook-gsd-rewriter.md b/.changeset/archived/376-claude-js-hook-gsd-rewriter.md similarity index 100% rename from .changeset/376-claude-js-hook-gsd-rewriter.md rename to .changeset/archived/376-claude-js-hook-gsd-rewriter.md diff --git a/.changeset/3761-consolidate-graphify-tests.md b/.changeset/archived/3761-consolidate-graphify-tests.md similarity index 100% rename from .changeset/3761-consolidate-graphify-tests.md rename to .changeset/archived/3761-consolidate-graphify-tests.md diff --git a/.changeset/3772-fix-acquirestatelock-non-eexist.md b/.changeset/archived/3772-fix-acquirestatelock-non-eexist.md similarity index 100% rename from .changeset/3772-fix-acquirestatelock-non-eexist.md rename to .changeset/archived/3772-fix-acquirestatelock-non-eexist.md diff --git a/.changeset/3776-fix-acquirestatelock-retry-allowlist.md b/.changeset/archived/3776-fix-acquirestatelock-retry-allowlist.md similarity index 100% rename from .changeset/3776-fix-acquirestatelock-retry-allowlist.md rename to .changeset/archived/3776-fix-acquirestatelock-retry-allowlist.md diff --git a/.changeset/378-update-check-scoped-name.md b/.changeset/archived/378-update-check-scoped-name.md similarity index 100% rename from .changeset/378-update-check-scoped-name.md rename to .changeset/archived/378-update-check-scoped-name.md diff --git a/.changeset/3804-worktree-cleanup-summary-rescue.md b/.changeset/archived/3804-worktree-cleanup-summary-rescue.md similarity index 100% rename from .changeset/3804-worktree-cleanup-summary-rescue.md rename to .changeset/archived/3804-worktree-cleanup-summary-rescue.md diff --git a/.changeset/3805-fast-md-log-to-state-schema-aware.md b/.changeset/archived/3805-fast-md-log-to-state-schema-aware.md similarity index 100% rename from .changeset/3805-fast-md-log-to-state-schema-aware.md rename to .changeset/archived/3805-fast-md-log-to-state-schema-aware.md diff --git a/.changeset/3806-cjs-bundle-drift-w005-w006-i001.md b/.changeset/archived/3806-cjs-bundle-drift-w005-w006-i001.md similarity index 100% rename from .changeset/3806-cjs-bundle-drift-w005-w006-i001.md rename to .changeset/archived/3806-cjs-bundle-drift-w005-w006-i001.md diff --git a/.changeset/3808-codex-adapter-text-mode-fallback.md b/.changeset/archived/3808-codex-adapter-text-mode-fallback.md similarity index 100% rename from .changeset/3808-codex-adapter-text-mode-fallback.md rename to .changeset/archived/3808-codex-adapter-text-mode-fallback.md diff --git a/.changeset/3815-phase-insert-bullet-roadmap.md b/.changeset/archived/3815-phase-insert-bullet-roadmap.md similarity index 100% rename from .changeset/3815-phase-insert-bullet-roadmap.md rename to .changeset/archived/3815-phase-insert-bullet-roadmap.md diff --git a/.changeset/3816-milestone-archive-version-sort.md b/.changeset/archived/3816-milestone-archive-version-sort.md similarity index 100% rename from .changeset/3816-milestone-archive-version-sort.md rename to .changeset/archived/3816-milestone-archive-version-sort.md diff --git a/.changeset/384-agents-dir-runtime-aware.md b/.changeset/archived/384-agents-dir-runtime-aware.md similarity index 100% rename from .changeset/384-agents-dir-runtime-aware.md rename to .changeset/archived/384-agents-dir-runtime-aware.md diff --git a/.changeset/397-state-preserve-executor-authored.md b/.changeset/archived/397-state-preserve-executor-authored.md similarity index 100% rename from .changeset/397-state-preserve-executor-authored.md rename to .changeset/archived/397-state-preserve-executor-authored.md diff --git a/.changeset/407-planning-lock-sab-hoist.md b/.changeset/archived/407-planning-lock-sab-hoist.md similarity index 100% rename from .changeset/407-planning-lock-sab-hoist.md rename to .changeset/archived/407-planning-lock-sab-hoist.md diff --git a/.changeset/408-ci-test-scope-smoke-alignment.md b/.changeset/archived/408-ci-test-scope-smoke-alignment.md similarity index 100% rename from .changeset/408-ci-test-scope-smoke-alignment.md rename to .changeset/archived/408-ci-test-scope-smoke-alignment.md diff --git a/.changeset/410-install-defaults-test-mode-guard.md b/.changeset/archived/410-install-defaults-test-mode-guard.md similarity index 100% rename from .changeset/410-install-defaults-test-mode-guard.md rename to .changeset/archived/410-install-defaults-test-mode-guard.md diff --git a/.changeset/416-active-milestone-archive-null.md b/.changeset/archived/416-active-milestone-archive-null.md similarity index 100% rename from .changeset/416-active-milestone-archive-null.md rename to .changeset/archived/416-active-milestone-archive-null.md diff --git a/.changeset/442-config-dir-equals-truncation.md b/.changeset/archived/442-config-dir-equals-truncation.md similarity index 100% rename from .changeset/442-config-dir-equals-truncation.md rename to .changeset/archived/442-config-dir-equals-truncation.md diff --git a/.changeset/444-resolver-local-claude-path.md b/.changeset/archived/444-resolver-local-claude-path.md similarity index 100% rename from .changeset/444-resolver-local-claude-path.md rename to .changeset/archived/444-resolver-local-claude-path.md diff --git a/.changeset/498-package-identity-seam.md b/.changeset/archived/498-package-identity-seam.md similarity index 100% rename from .changeset/498-package-identity-seam.md rename to .changeset/archived/498-package-identity-seam.md diff --git a/.changeset/5-decision-coverage-xml-bodies.md b/.changeset/archived/5-decision-coverage-xml-bodies.md similarity index 100% rename from .changeset/5-decision-coverage-xml-bodies.md rename to .changeset/archived/5-decision-coverage-xml-bodies.md diff --git a/.changeset/501-flat-phase-details-milestone-leak.md b/.changeset/archived/501-flat-phase-details-milestone-leak.md similarity index 100% rename from .changeset/501-flat-phase-details-milestone-leak.md rename to .changeset/archived/501-flat-phase-details-milestone-leak.md diff --git a/.changeset/503-antigravity-agent-local-detection.md b/.changeset/archived/503-antigravity-agent-local-detection.md similarity index 100% rename from .changeset/503-antigravity-agent-local-detection.md rename to .changeset/archived/503-antigravity-agent-local-detection.md diff --git a/.changeset/546-changelog-1-2-0.md b/.changeset/archived/546-changelog-1-2-0.md similarity index 100% rename from .changeset/546-changelog-1-2-0.md rename to .changeset/archived/546-changelog-1-2-0.md diff --git a/.changeset/549-total-phases-decimal-overcounting.md b/.changeset/archived/549-total-phases-decimal-overcounting.md similarity index 100% rename from .changeset/549-total-phases-decimal-overcounting.md rename to .changeset/archived/549-total-phases-decimal-overcounting.md diff --git a/.changeset/557-details-summary-milestone-strip.md b/.changeset/archived/557-details-summary-milestone-strip.md similarity index 100% rename from .changeset/557-details-summary-milestone-strip.md rename to .changeset/archived/557-details-summary-milestone-strip.md diff --git a/.changeset/566-spawn-liveness-banner.md b/.changeset/archived/566-spawn-liveness-banner.md similarity index 100% rename from .changeset/566-spawn-liveness-banner.md rename to .changeset/archived/566-spawn-liveness-banner.md diff --git a/.changeset/604-rename-get-shit-done-to-gsd-core.md b/.changeset/archived/604-rename-get-shit-done-to-gsd-core.md similarity index 100% rename from .changeset/604-rename-get-shit-done-to-gsd-core.md rename to .changeset/archived/604-rename-get-shit-done-to-gsd-core.md diff --git a/.changeset/614-discuss-phase-shim-resolution.md b/.changeset/archived/614-discuss-phase-shim-resolution.md similarity index 100% rename from .changeset/614-discuss-phase-shim-resolution.md rename to .changeset/archived/614-discuss-phase-shim-resolution.md diff --git a/.changeset/archived/README.md b/.changeset/archived/README.md new file mode 100644 index 000000000..70bcc4f97 --- /dev/null +++ b/.changeset/archived/README.md @@ -0,0 +1,20 @@ +# Archived changeset fragments + +These fragments describe changes that shipped in **gsd-core ≤ 1.3.1**. Their +user-facing notes were already hand-curated into the dated `## [1.2.0]`, +`## [1.3.0]`, and `## [1.3.1]` sections of [`../../CHANGELOG.md`](../../CHANGELOG.md) +during the #690 backfill (PR #694) and the earlier 1.2.0 promotion. + +They were never consumed by a `render` run (CHANGELOG promotion was a manual +operator step that was skipped — see [#690](https://github.com/open-gsd/gsd-core/issues/690)), +so they accumulated here. They are retained for provenance only. + +## Do not render these + +`render` (`scripts/changeset/cli.cjs`) and every other changeset tool enumerate +`.changeset/` **non-recursively**, so nothing in this `archived/` subdirectory is +ever picked up. That is deliberate: rendering these would duplicate and +mis-attribute work that already shipped. Do not move them back to the parent +directory. + +Genuinely-unreleased fragments live one level up, in `.changeset/*.md`. diff --git a/.changeset/adr-0002-command-contract-validation.md b/.changeset/archived/adr-0002-command-contract-validation.md similarity index 100% rename from .changeset/adr-0002-command-contract-validation.md rename to .changeset/archived/adr-0002-command-contract-validation.md diff --git a/.changeset/agile-birds-cheer.md b/.changeset/archived/agile-birds-cheer.md similarity index 100% rename from .changeset/agile-birds-cheer.md rename to .changeset/archived/agile-birds-cheer.md diff --git a/.changeset/blue-stones-topology.md b/.changeset/archived/blue-stones-topology.md similarity index 100% rename from .changeset/blue-stones-topology.md rename to .changeset/archived/blue-stones-topology.md diff --git a/.changeset/bold-elks-zip.md b/.changeset/archived/bold-elks-zip.md similarity index 100% rename from .changeset/bold-elks-zip.md rename to .changeset/archived/bold-elks-zip.md diff --git a/.changeset/bold-finches-rally.md b/.changeset/archived/bold-finches-rally.md similarity index 100% rename from .changeset/bold-finches-rally.md rename to .changeset/archived/bold-finches-rally.md diff --git a/.changeset/bold-orcas-howl.md b/.changeset/archived/bold-orcas-howl.md similarity index 100% rename from .changeset/bold-orcas-howl.md rename to .changeset/archived/bold-orcas-howl.md diff --git a/.changeset/brave-mice-build.md b/.changeset/archived/brave-mice-build.md similarity index 100% rename from .changeset/brave-mice-build.md rename to .changeset/archived/brave-mice-build.md diff --git a/.changeset/brave-wolves-rally.md b/.changeset/archived/brave-wolves-rally.md similarity index 100% rename from .changeset/brave-wolves-rally.md rename to .changeset/archived/brave-wolves-rally.md diff --git a/.changeset/bright-pumas-fold.md b/.changeset/archived/bright-pumas-fold.md similarity index 100% rename from .changeset/bright-pumas-fold.md rename to .changeset/archived/bright-pumas-fold.md diff --git a/.changeset/bug-3446-resume-continue-here-discovery.md b/.changeset/archived/bug-3446-resume-continue-here-discovery.md similarity index 100% rename from .changeset/bug-3446-resume-continue-here-discovery.md rename to .changeset/archived/bug-3446-resume-continue-here-discovery.md diff --git a/.changeset/build-hooks-atomic-write.md b/.changeset/archived/build-hooks-atomic-write.md similarity index 100% rename from .changeset/build-hooks-atomic-write.md rename to .changeset/archived/build-hooks-atomic-write.md diff --git a/.changeset/calm-birds-greet.md b/.changeset/archived/calm-birds-greet.md similarity index 100% rename from .changeset/calm-birds-greet.md rename to .changeset/archived/calm-birds-greet.md diff --git a/.changeset/calm-cranes-roar.md b/.changeset/archived/calm-cranes-roar.md similarity index 100% rename from .changeset/calm-cranes-roar.md rename to .changeset/archived/calm-cranes-roar.md diff --git a/.changeset/calm-herons-wake.md b/.changeset/archived/calm-herons-wake.md similarity index 100% rename from .changeset/calm-herons-wake.md rename to .changeset/archived/calm-herons-wake.md diff --git a/.changeset/calm-ibex-jump.md b/.changeset/archived/calm-ibex-jump.md similarity index 100% rename from .changeset/calm-ibex-jump.md rename to .changeset/archived/calm-ibex-jump.md diff --git a/.changeset/calm-koalas-hop.md b/.changeset/archived/calm-koalas-hop.md similarity index 100% rename from .changeset/calm-koalas-hop.md rename to .changeset/archived/calm-koalas-hop.md diff --git a/.changeset/calm-tigers-click.md b/.changeset/archived/calm-tigers-click.md similarity index 100% rename from .changeset/calm-tigers-click.md rename to .changeset/archived/calm-tigers-click.md diff --git a/.changeset/calm-tigers-frolic.md b/.changeset/archived/calm-tigers-frolic.md similarity index 100% rename from .changeset/calm-tigers-frolic.md rename to .changeset/archived/calm-tigers-frolic.md diff --git a/.changeset/careful-model-transport.md b/.changeset/archived/careful-model-transport.md similarity index 100% rename from .changeset/careful-model-transport.md rename to .changeset/archived/careful-model-transport.md diff --git a/.changeset/clever-deer-wander.md b/.changeset/archived/clever-deer-wander.md similarity index 100% rename from .changeset/clever-deer-wander.md rename to .changeset/archived/clever-deer-wander.md diff --git a/.changeset/clever-pumas-frolic.md b/.changeset/archived/clever-pumas-frolic.md similarity index 100% rename from .changeset/clever-pumas-frolic.md rename to .changeset/archived/clever-pumas-frolic.md diff --git a/.changeset/clever-wasps-parade.md b/.changeset/archived/clever-wasps-parade.md similarity index 100% rename from .changeset/clever-wasps-parade.md rename to .changeset/archived/clever-wasps-parade.md diff --git a/.changeset/clever-yaks-cheer.md b/.changeset/archived/clever-yaks-cheer.md similarity index 100% rename from .changeset/clever-yaks-cheer.md rename to .changeset/archived/clever-yaks-cheer.md diff --git a/.changeset/clever-zebras-snooze.md b/.changeset/archived/clever-zebras-snooze.md similarity index 100% rename from .changeset/clever-zebras-snooze.md rename to .changeset/archived/clever-zebras-snooze.md diff --git a/.changeset/code-review-flags-ts-migration.md b/.changeset/archived/code-review-flags-ts-migration.md similarity index 100% rename from .changeset/code-review-flags-ts-migration.md rename to .changeset/archived/code-review-flags-ts-migration.md diff --git a/.changeset/code-review-leaf-batch-1-ts.md b/.changeset/archived/code-review-leaf-batch-1-ts.md similarity index 100% rename from .changeset/code-review-leaf-batch-1-ts.md rename to .changeset/archived/code-review-leaf-batch-1-ts.md diff --git a/.changeset/codex-bare-node-fix.md b/.changeset/archived/codex-bare-node-fix.md similarity index 100% rename from .changeset/codex-bare-node-fix.md rename to .changeset/archived/codex-bare-node-fix.md diff --git a/.changeset/codex-discuss-fallback.md b/.changeset/archived/codex-discuss-fallback.md similarity index 100% rename from .changeset/codex-discuss-fallback.md rename to .changeset/archived/codex-discuss-fallback.md diff --git a/.changeset/codex-windows-bash-runner.md b/.changeset/archived/codex-windows-bash-runner.md similarity index 100% rename from .changeset/codex-windows-bash-runner.md rename to .changeset/archived/codex-windows-bash-runner.md diff --git a/.changeset/codex-windows-hook-script-paths.md b/.changeset/archived/codex-windows-hook-script-paths.md similarity index 100% rename from .changeset/codex-windows-hook-script-paths.md rename to .changeset/archived/codex-windows-hook-script-paths.md diff --git a/.changeset/cool-monkeys-smell.md b/.changeset/archived/cool-monkeys-smell.md similarity index 100% rename from .changeset/cool-monkeys-smell.md rename to .changeset/archived/cool-monkeys-smell.md diff --git a/.changeset/crisp-seams-align.md b/.changeset/archived/crisp-seams-align.md similarity index 100% rename from .changeset/crisp-seams-align.md rename to .changeset/archived/crisp-seams-align.md diff --git a/.changeset/curious-bears-march.md b/.changeset/archived/curious-bears-march.md similarity index 100% rename from .changeset/curious-bears-march.md rename to .changeset/archived/curious-bears-march.md diff --git a/.changeset/curious-bears-zip.md b/.changeset/archived/curious-bears-zip.md similarity index 100% rename from .changeset/curious-bears-zip.md rename to .changeset/archived/curious-bears-zip.md diff --git a/.changeset/curious-cats-parade.md b/.changeset/archived/curious-cats-parade.md similarity index 100% rename from .changeset/curious-cats-parade.md rename to .changeset/archived/curious-cats-parade.md diff --git a/.changeset/curious-lynx-travel.md b/.changeset/archived/curious-lynx-travel.md similarity index 100% rename from .changeset/curious-lynx-travel.md rename to .changeset/archived/curious-lynx-travel.md diff --git a/.changeset/curious-tigers-jump.md b/.changeset/archived/curious-tigers-jump.md similarity index 100% rename from .changeset/curious-tigers-jump.md rename to .changeset/archived/curious-tigers-jump.md diff --git a/.changeset/daring-badgers-munch.md b/.changeset/archived/daring-badgers-munch.md similarity index 100% rename from .changeset/daring-badgers-munch.md rename to .changeset/archived/daring-badgers-munch.md diff --git a/.changeset/daring-yaks-zip.md b/.changeset/archived/daring-yaks-zip.md similarity index 100% rename from .changeset/daring-yaks-zip.md rename to .changeset/archived/daring-yaks-zip.md diff --git a/.changeset/disable-milestone-tags.md b/.changeset/archived/disable-milestone-tags.md similarity index 100% rename from .changeset/disable-milestone-tags.md rename to .changeset/archived/disable-milestone-tags.md diff --git a/.changeset/docs-1-40-0-audit.md b/.changeset/archived/docs-1-40-0-audit.md similarity index 100% rename from .changeset/docs-1-40-0-audit.md rename to .changeset/archived/docs-1-40-0-audit.md diff --git a/.changeset/dynamic-routing.md b/.changeset/archived/dynamic-routing.md similarity index 100% rename from .changeset/dynamic-routing.md rename to .changeset/archived/dynamic-routing.md diff --git a/.changeset/eager-badgers-purr.md b/.changeset/archived/eager-badgers-purr.md similarity index 100% rename from .changeset/eager-badgers-purr.md rename to .changeset/archived/eager-badgers-purr.md diff --git a/.changeset/eager-elks-purr.md b/.changeset/archived/eager-elks-purr.md similarity index 100% rename from .changeset/eager-elks-purr.md rename to .changeset/archived/eager-elks-purr.md diff --git a/.changeset/eager-elks-romp.md b/.changeset/archived/eager-elks-romp.md similarity index 100% rename from .changeset/eager-elks-romp.md rename to .changeset/archived/eager-elks-romp.md diff --git a/.changeset/eager-foxes-zip.md b/.changeset/archived/eager-foxes-zip.md similarity index 100% rename from .changeset/eager-foxes-zip.md rename to .changeset/archived/eager-foxes-zip.md diff --git a/.changeset/eager-hawks-rally.md b/.changeset/archived/eager-hawks-rally.md similarity index 100% rename from .changeset/eager-hawks-rally.md rename to .changeset/archived/eager-hawks-rally.md diff --git a/.changeset/eager-koalas-zip.md b/.changeset/archived/eager-koalas-zip.md similarity index 100% rename from .changeset/eager-koalas-zip.md rename to .changeset/archived/eager-koalas-zip.md diff --git a/.changeset/eager-lynx-chatter.md b/.changeset/archived/eager-lynx-chatter.md similarity index 100% rename from .changeset/eager-lynx-chatter.md rename to .changeset/archived/eager-lynx-chatter.md diff --git a/.changeset/eager-lynx-munch.md b/.changeset/archived/eager-lynx-munch.md similarity index 100% rename from .changeset/eager-lynx-munch.md rename to .changeset/archived/eager-lynx-munch.md diff --git a/.changeset/eager-pumas-dance.md b/.changeset/archived/eager-pumas-dance.md similarity index 100% rename from .changeset/eager-pumas-dance.md rename to .changeset/archived/eager-pumas-dance.md diff --git a/.changeset/eager-wolves-tumble.md b/.changeset/archived/eager-wolves-tumble.md similarity index 100% rename from .changeset/eager-wolves-tumble.md rename to .changeset/archived/eager-wolves-tumble.md diff --git a/.changeset/fair-hawks-play.md b/.changeset/archived/fair-hawks-play.md similarity index 100% rename from .changeset/fair-hawks-play.md rename to .changeset/archived/fair-hawks-play.md diff --git a/.changeset/feat-3408-skill-profiles.md b/.changeset/archived/feat-3408-skill-profiles.md similarity index 100% rename from .changeset/feat-3408-skill-profiles.md rename to .changeset/archived/feat-3408-skill-profiles.md diff --git a/.changeset/feat-3408-skill-surface.md b/.changeset/archived/feat-3408-skill-surface.md similarity index 100% rename from .changeset/feat-3408-skill-surface.md rename to .changeset/archived/feat-3408-skill-surface.md diff --git a/.changeset/feat-41-ship-tdd-audit.md b/.changeset/archived/feat-41-ship-tdd-audit.md similarity index 100% rename from .changeset/feat-41-ship-tdd-audit.md rename to .changeset/archived/feat-41-ship-tdd-audit.md diff --git a/.changeset/fierce-birds-wake.md b/.changeset/archived/fierce-birds-wake.md similarity index 100% rename from .changeset/fierce-birds-wake.md rename to .changeset/archived/fierce-birds-wake.md diff --git a/.changeset/fierce-finches-munch.md b/.changeset/archived/fierce-finches-munch.md similarity index 100% rename from .changeset/fierce-finches-munch.md rename to .changeset/archived/fierce-finches-munch.md diff --git a/.changeset/fierce-geese-march.md b/.changeset/archived/fierce-geese-march.md similarity index 100% rename from .changeset/fierce-geese-march.md rename to .changeset/archived/fierce-geese-march.md diff --git a/.changeset/fierce-rams-rest.md b/.changeset/archived/fierce-rams-rest.md similarity index 100% rename from .changeset/fierce-rams-rest.md rename to .changeset/archived/fierce-rams-rest.md diff --git a/.changeset/fix-3-milestone-complete-noise.md b/.changeset/archived/fix-3-milestone-complete-noise.md similarity index 100% rename from .changeset/fix-3-milestone-complete-noise.md rename to .changeset/archived/fix-3-milestone-complete-noise.md diff --git a/.changeset/fix-3054-doc-anchor-and-token-check.md b/.changeset/archived/fix-3054-doc-anchor-and-token-check.md similarity index 100% rename from .changeset/fix-3054-doc-anchor-and-token-check.md rename to .changeset/archived/fix-3054-doc-anchor-and-token-check.md diff --git a/.changeset/fix-3056-worktree-path-assertion.md b/.changeset/archived/fix-3056-worktree-path-assertion.md similarity index 100% rename from .changeset/fix-3056-worktree-path-assertion.md rename to .changeset/archived/fix-3056-worktree-path-assertion.md diff --git a/.changeset/fix-306-learnings-dedupe-index.md b/.changeset/archived/fix-306-learnings-dedupe-index.md similarity index 100% rename from .changeset/fix-306-learnings-dedupe-index.md rename to .changeset/archived/fix-306-learnings-dedupe-index.md diff --git a/.changeset/fix-3072-findings-probe-assertions.md b/.changeset/archived/fix-3072-findings-probe-assertions.md similarity index 100% rename from .changeset/fix-3072-findings-probe-assertions.md rename to .changeset/archived/fix-3072-findings-probe-assertions.md diff --git a/.changeset/fix-3087-planner-directive-language.md b/.changeset/archived/fix-3087-planner-directive-language.md similarity index 100% rename from .changeset/fix-3087-planner-directive-language.md rename to .changeset/archived/fix-3087-planner-directive-language.md diff --git a/.changeset/fix-3088-milestone-state-fallback-sections.md b/.changeset/archived/fix-3088-milestone-state-fallback-sections.md similarity index 100% rename from .changeset/fix-3088-milestone-state-fallback-sections.md rename to .changeset/archived/fix-3088-milestone-state-fallback-sections.md diff --git a/.changeset/fix-3094-progress-stale-assumptions.md b/.changeset/archived/fix-3094-progress-stale-assumptions.md similarity index 100% rename from .changeset/fix-3094-progress-stale-assumptions.md rename to .changeset/archived/fix-3094-progress-stale-assumptions.md diff --git a/.changeset/fix-3096-ai-integration-parallel-race.md b/.changeset/archived/fix-3096-ai-integration-parallel-race.md similarity index 100% rename from .changeset/fix-3096-ai-integration-parallel-race.md rename to .changeset/archived/fix-3096-ai-integration-parallel-race.md diff --git a/.changeset/fix-3097-3099-executor-worktree-path.md b/.changeset/archived/fix-3097-3099-executor-worktree-path.md similarity index 100% rename from .changeset/fix-3097-3099-executor-worktree-path.md rename to .changeset/archived/fix-3097-3099-executor-worktree-path.md diff --git a/.changeset/fix-3120-secure-phase-empty-register.md b/.changeset/archived/fix-3120-secure-phase-empty-register.md similarity index 100% rename from .changeset/fix-3120-secure-phase-empty-register.md rename to .changeset/archived/fix-3120-secure-phase-empty-register.md diff --git a/.changeset/fix-3121-gsd-tools-commands-verb.md b/.changeset/archived/fix-3121-gsd-tools-commands-verb.md similarity index 100% rename from .changeset/fix-3121-gsd-tools-commands-verb.md rename to .changeset/archived/fix-3121-gsd-tools-commands-verb.md diff --git a/.changeset/fix-3126-global-skills-base-runtime.md b/.changeset/archived/fix-3126-global-skills-base-runtime.md similarity index 100% rename from .changeset/fix-3126-global-skills-base-runtime.md rename to .changeset/archived/fix-3126-global-skills-base-runtime.md diff --git a/.changeset/fix-3127-state-begin-phase-idempotent.md b/.changeset/archived/fix-3127-state-begin-phase-idempotent.md similarity index 100% rename from .changeset/fix-3127-state-begin-phase-idempotent.md rename to .changeset/archived/fix-3127-state-begin-phase-idempotent.md diff --git a/.changeset/fix-3128-roadmap-plan-count-slug.md b/.changeset/archived/fix-3128-roadmap-plan-count-slug.md similarity index 100% rename from .changeset/fix-3128-roadmap-plan-count-slug.md rename to .changeset/archived/fix-3128-roadmap-plan-count-slug.md diff --git a/.changeset/fix-3129-validate-commit-bypass.md b/.changeset/archived/fix-3129-validate-commit-bypass.md similarity index 100% rename from .changeset/fix-3129-validate-commit-bypass.md rename to .changeset/archived/fix-3129-validate-commit-bypass.md diff --git a/.changeset/fix-3130-update-npx-robust.md b/.changeset/archived/fix-3130-update-npx-robust.md similarity index 100% rename from .changeset/fix-3130-update-npx-robust.md rename to .changeset/archived/fix-3130-update-npx-robust.md diff --git a/.changeset/fix-3135-capture-backlog-workflow.md b/.changeset/archived/fix-3135-capture-backlog-workflow.md similarity index 100% rename from .changeset/fix-3135-capture-backlog-workflow.md rename to .changeset/archived/fix-3135-capture-backlog-workflow.md diff --git a/.changeset/fix-3150-stats-json-decimal-gap-regression.md b/.changeset/archived/fix-3150-stats-json-decimal-gap-regression.md similarity index 100% rename from .changeset/fix-3150-stats-json-decimal-gap-regression.md rename to .changeset/archived/fix-3150-stats-json-decimal-gap-regression.md diff --git a/.changeset/fix-3153-statusline-percent-next-phases.md b/.changeset/archived/fix-3153-statusline-percent-next-phases.md similarity index 100% rename from .changeset/fix-3153-statusline-percent-next-phases.md rename to .changeset/archived/fix-3153-statusline-percent-next-phases.md diff --git a/.changeset/fix-3163-codex-agents-md.md b/.changeset/archived/fix-3163-codex-agents-md.md similarity index 100% rename from .changeset/fix-3163-codex-agents-md.md rename to .changeset/archived/fix-3163-codex-agents-md.md diff --git a/.changeset/fix-3196-workstream-milestone-op.md b/.changeset/archived/fix-3196-workstream-milestone-op.md similarity index 100% rename from .changeset/fix-3196-workstream-milestone-op.md rename to .changeset/archived/fix-3196-workstream-milestone-op.md diff --git a/.changeset/fix-3197-gsd-tools-config-whitelist.md b/.changeset/archived/fix-3197-gsd-tools-config-whitelist.md similarity index 100% rename from .changeset/fix-3197-gsd-tools-config-whitelist.md rename to .changeset/archived/fix-3197-gsd-tools-config-whitelist.md diff --git a/.changeset/fix-3229-model-catalog-source-of-truth.md b/.changeset/archived/fix-3229-model-catalog-source-of-truth.md similarity index 100% rename from .changeset/fix-3229-model-catalog-source-of-truth.md rename to .changeset/archived/fix-3229-model-catalog-source-of-truth.md diff --git a/.changeset/fix-3321-verifier-probes.md b/.changeset/archived/fix-3321-verifier-probes.md similarity index 100% rename from .changeset/fix-3321-verifier-probes.md rename to .changeset/archived/fix-3321-verifier-probes.md diff --git a/.changeset/fix-3339-human-needed-verification-pending.md b/.changeset/archived/fix-3339-human-needed-verification-pending.md similarity index 100% rename from .changeset/fix-3339-human-needed-verification-pending.md rename to .changeset/archived/fix-3339-human-needed-verification-pending.md diff --git a/.changeset/fix-3344-gemini-agent-tool.md b/.changeset/archived/fix-3344-gemini-agent-tool.md similarity index 100% rename from .changeset/fix-3344-gemini-agent-tool.md rename to .changeset/archived/fix-3344-gemini-agent-tool.md diff --git a/.changeset/fix-3355-phase-remove-roadmap-renumber.md b/.changeset/archived/fix-3355-phase-remove-roadmap-renumber.md similarity index 100% rename from .changeset/fix-3355-phase-remove-roadmap-renumber.md rename to .changeset/archived/fix-3355-phase-remove-roadmap-renumber.md diff --git a/.changeset/fix-3357-codex-legacy-hooks-json.md b/.changeset/archived/fix-3357-codex-legacy-hooks-json.md similarity index 100% rename from .changeset/fix-3357-codex-legacy-hooks-json.md rename to .changeset/archived/fix-3357-codex-legacy-hooks-json.md diff --git a/.changeset/fix-3358-sdk-init-progress-models.md b/.changeset/archived/fix-3358-sdk-init-progress-models.md similarity index 100% rename from .changeset/fix-3358-sdk-init-progress-models.md rename to .changeset/archived/fix-3358-sdk-init-progress-models.md diff --git a/.changeset/fix-3359-stale-sdk-path-version.md b/.changeset/archived/fix-3359-stale-sdk-path-version.md similarity index 100% rename from .changeset/fix-3359-stale-sdk-path-version.md rename to .changeset/archived/fix-3359-stale-sdk-path-version.md diff --git a/.changeset/fix-3360-codex-execute-worktrees.md b/.changeset/archived/fix-3360-codex-execute-worktrees.md similarity index 100% rename from .changeset/fix-3360-codex-execute-worktrees.md rename to .changeset/archived/fix-3360-codex-execute-worktrees.md diff --git a/.changeset/fix-3362-windows-powershell-gemini.md b/.changeset/archived/fix-3362-windows-powershell-gemini.md similarity index 100% rename from .changeset/fix-3362-windows-powershell-gemini.md rename to .changeset/archived/fix-3362-windows-powershell-gemini.md diff --git a/.changeset/fix-3381-init-verify-work-ws.md b/.changeset/archived/fix-3381-init-verify-work-ws.md similarity index 100% rename from .changeset/fix-3381-init-verify-work-ws.md rename to .changeset/archived/fix-3381-init-verify-work-ws.md diff --git a/.changeset/fix-3384-worktree-merge-safety.md b/.changeset/archived/fix-3384-worktree-merge-safety.md similarity index 100% rename from .changeset/fix-3384-worktree-merge-safety.md rename to .changeset/archived/fix-3384-worktree-merge-safety.md diff --git a/.changeset/fix-3406-detect-stale-sdk-shadow.md b/.changeset/archived/fix-3406-detect-stale-sdk-shadow.md similarity index 100% rename from .changeset/fix-3406-detect-stale-sdk-shadow.md rename to .changeset/archived/fix-3406-detect-stale-sdk-shadow.md diff --git a/.changeset/fix-3509-path-spaces-test-suite.md b/.changeset/archived/fix-3509-path-spaces-test-suite.md similarity index 100% rename from .changeset/fix-3509-path-spaces-test-suite.md rename to .changeset/archived/fix-3509-path-spaces-test-suite.md diff --git a/.changeset/fix-3579-graphify-hook-publish.md b/.changeset/archived/fix-3579-graphify-hook-publish.md similarity index 100% rename from .changeset/fix-3579-graphify-hook-publish.md rename to .changeset/archived/fix-3579-graphify-hook-publish.md diff --git a/.changeset/fix-3588-npm-audit-clean.md b/.changeset/archived/fix-3588-npm-audit-clean.md similarity index 100% rename from .changeset/fix-3588-npm-audit-clean.md rename to .changeset/archived/fix-3588-npm-audit-clean.md diff --git a/.changeset/fix-3631-sdk-raw-flag-routers.md b/.changeset/archived/fix-3631-sdk-raw-flag-routers.md similarity index 100% rename from .changeset/fix-3631-sdk-raw-flag-routers.md rename to .changeset/archived/fix-3631-sdk-raw-flag-routers.md diff --git a/.changeset/fix-3632-lint-handsync-pair-fanout.md b/.changeset/archived/fix-3632-lint-handsync-pair-fanout.md similarity index 100% rename from .changeset/fix-3632-lint-handsync-pair-fanout.md rename to .changeset/archived/fix-3632-lint-handsync-pair-fanout.md diff --git a/.changeset/fix-3677-agent-colon-namespace-leak.md b/.changeset/archived/fix-3677-agent-colon-namespace-leak.md similarity index 100% rename from .changeset/fix-3677-agent-colon-namespace-leak.md rename to .changeset/archived/fix-3677-agent-colon-namespace-leak.md diff --git a/.changeset/fix-3678-commit-docs-respect.md b/.changeset/archived/fix-3678-commit-docs-respect.md similarity index 100% rename from .changeset/fix-3678-commit-docs-respect.md rename to .changeset/archived/fix-3678-commit-docs-respect.md diff --git a/.changeset/fix-canary-2-release-gates.md b/.changeset/archived/fix-canary-2-release-gates.md similarity index 100% rename from .changeset/fix-canary-2-release-gates.md rename to .changeset/archived/fix-canary-2-release-gates.md diff --git a/.changeset/fix-next-gate-regressions.md b/.changeset/archived/fix-next-gate-regressions.md similarity index 100% rename from .changeset/fix-next-gate-regressions.md rename to .changeset/archived/fix-next-gate-regressions.md diff --git a/.changeset/forensics-regex-escape.md b/.changeset/archived/forensics-regex-escape.md similarity index 100% rename from .changeset/forensics-regex-escape.md rename to .changeset/archived/forensics-regex-escape.md diff --git a/.changeset/gallant-badgers-bark.md b/.changeset/archived/gallant-badgers-bark.md similarity index 100% rename from .changeset/gallant-badgers-bark.md rename to .changeset/archived/gallant-badgers-bark.md diff --git a/.changeset/gallant-ravens-travel.md b/.changeset/archived/gallant-ravens-travel.md similarity index 100% rename from .changeset/gallant-ravens-travel.md rename to .changeset/archived/gallant-ravens-travel.md diff --git a/.changeset/gemini-skip-local-when-global.md b/.changeset/archived/gemini-skip-local-when-global.md similarity index 100% rename from .changeset/gemini-skip-local-when-global.md rename to .changeset/archived/gemini-skip-local-when-global.md diff --git a/.changeset/gentle-bears-wave.md b/.changeset/archived/gentle-bears-wave.md similarity index 100% rename from .changeset/gentle-bears-wave.md rename to .changeset/archived/gentle-bears-wave.md diff --git a/.changeset/gentle-birds-caper.md b/.changeset/archived/gentle-birds-caper.md similarity index 100% rename from .changeset/gentle-birds-caper.md rename to .changeset/archived/gentle-birds-caper.md diff --git a/.changeset/gentle-goats-fly.md b/.changeset/archived/gentle-goats-fly.md similarity index 100% rename from .changeset/gentle-goats-fly.md rename to .changeset/archived/gentle-goats-fly.md diff --git a/.changeset/gentle-jays-rest.md b/.changeset/archived/gentle-jays-rest.md similarity index 100% rename from .changeset/gentle-jays-rest.md rename to .changeset/archived/gentle-jays-rest.md diff --git a/.changeset/gentle-jays-zip.md b/.changeset/archived/gentle-jays-zip.md similarity index 100% rename from .changeset/gentle-jays-zip.md rename to .changeset/archived/gentle-jays-zip.md diff --git a/.changeset/gentle-tigers-roar.md b/.changeset/archived/gentle-tigers-roar.md similarity index 100% rename from .changeset/gentle-tigers-roar.md rename to .changeset/archived/gentle-tigers-roar.md diff --git a/.changeset/graceful-geese-tumble.md b/.changeset/archived/graceful-geese-tumble.md similarity index 100% rename from .changeset/graceful-geese-tumble.md rename to .changeset/archived/graceful-geese-tumble.md diff --git a/.changeset/graceful-jays-hop.md b/.changeset/archived/graceful-jays-hop.md similarity index 100% rename from .changeset/graceful-jays-hop.md rename to .changeset/archived/graceful-jays-hop.md diff --git a/.changeset/graceful-mice-wave.md b/.changeset/archived/graceful-mice-wave.md similarity index 100% rename from .changeset/graceful-mice-wave.md rename to .changeset/archived/graceful-mice-wave.md diff --git a/.changeset/graceful-moles-wave.md b/.changeset/archived/graceful-moles-wave.md similarity index 100% rename from .changeset/graceful-moles-wave.md rename to .changeset/archived/graceful-moles-wave.md diff --git a/.changeset/graceful-otters-wave.md b/.changeset/archived/graceful-otters-wave.md similarity index 100% rename from .changeset/graceful-otters-wave.md rename to .changeset/archived/graceful-otters-wave.md diff --git a/.changeset/graceful-quails-hop.md b/.changeset/archived/graceful-quails-hop.md similarity index 100% rename from .changeset/graceful-quails-hop.md rename to .changeset/archived/graceful-quails-hop.md diff --git a/.changeset/graceful-sloths-wake.md b/.changeset/archived/graceful-sloths-wake.md similarity index 100% rename from .changeset/graceful-sloths-wake.md rename to .changeset/archived/graceful-sloths-wake.md diff --git a/.changeset/graceful-tigers-fly.md b/.changeset/archived/graceful-tigers-fly.md similarity index 100% rename from .changeset/graceful-tigers-fly.md rename to .changeset/archived/graceful-tigers-fly.md diff --git a/.changeset/happy-dogs-chatter.md b/.changeset/archived/happy-dogs-chatter.md similarity index 100% rename from .changeset/happy-dogs-chatter.md rename to .changeset/archived/happy-dogs-chatter.md diff --git a/.changeset/happy-herons-snooze.md b/.changeset/archived/happy-herons-snooze.md similarity index 100% rename from .changeset/happy-herons-snooze.md rename to .changeset/archived/happy-herons-snooze.md diff --git a/.changeset/happy-jays-greet.md b/.changeset/archived/happy-jays-greet.md similarity index 100% rename from .changeset/happy-jays-greet.md rename to .changeset/archived/happy-jays-greet.md diff --git a/.changeset/happy-jays-wake.md b/.changeset/archived/happy-jays-wake.md similarity index 100% rename from .changeset/happy-jays-wake.md rename to .changeset/archived/happy-jays-wake.md diff --git a/.changeset/happy-pandas-glide.md b/.changeset/archived/happy-pandas-glide.md similarity index 100% rename from .changeset/happy-pandas-glide.md rename to .changeset/archived/happy-pandas-glide.md diff --git a/.changeset/happy-tigers-travel.md b/.changeset/archived/happy-tigers-travel.md similarity index 100% rename from .changeset/happy-tigers-travel.md rename to .changeset/archived/happy-tigers-travel.md diff --git a/.changeset/help-passthrough.md b/.changeset/archived/help-passthrough.md similarity index 100% rename from .changeset/help-passthrough.md rename to .changeset/archived/help-passthrough.md diff --git a/.changeset/humble-geese-wave.md b/.changeset/archived/humble-geese-wave.md similarity index 100% rename from .changeset/humble-geese-wave.md rename to .changeset/archived/humble-geese-wave.md diff --git a/.changeset/humble-goats-swim.md b/.changeset/archived/humble-goats-swim.md similarity index 100% rename from .changeset/humble-goats-swim.md rename to .changeset/archived/humble-goats-swim.md diff --git a/.changeset/humble-pandas-wave.md b/.changeset/archived/humble-pandas-wave.md similarity index 100% rename from .changeset/humble-pandas-wave.md rename to .changeset/archived/humble-pandas-wave.md diff --git a/.changeset/humble-tunas-leap.md b/.changeset/archived/humble-tunas-leap.md similarity index 100% rename from .changeset/humble-tunas-leap.md rename to .changeset/archived/humble-tunas-leap.md diff --git a/.changeset/humble-tunas-zip.md b/.changeset/archived/humble-tunas-zip.md similarity index 100% rename from .changeset/humble-tunas-zip.md rename to .changeset/archived/humble-tunas-zip.md diff --git a/.changeset/install-shell-path-probe.md b/.changeset/archived/install-shell-path-probe.md similarity index 100% rename from .changeset/install-shell-path-probe.md rename to .changeset/archived/install-shell-path-probe.md diff --git a/.changeset/issue-driven-orchestration.md b/.changeset/archived/issue-driven-orchestration.md similarity index 100% rename from .changeset/issue-driven-orchestration.md rename to .changeset/archived/issue-driven-orchestration.md diff --git a/.changeset/jolly-cranes-chatter.md b/.changeset/archived/jolly-cranes-chatter.md similarity index 100% rename from .changeset/jolly-cranes-chatter.md rename to .changeset/archived/jolly-cranes-chatter.md diff --git a/.changeset/jolly-hawks-forage.md b/.changeset/archived/jolly-hawks-forage.md similarity index 100% rename from .changeset/jolly-hawks-forage.md rename to .changeset/archived/jolly-hawks-forage.md diff --git a/.changeset/jolly-jaguars-leap.md b/.changeset/archived/jolly-jaguars-leap.md similarity index 100% rename from .changeset/jolly-jaguars-leap.md rename to .changeset/archived/jolly-jaguars-leap.md diff --git a/.changeset/jolly-moles-climb.md b/.changeset/archived/jolly-moles-climb.md similarity index 100% rename from .changeset/jolly-moles-climb.md rename to .changeset/archived/jolly-moles-climb.md diff --git a/.changeset/jolly-newts-roam.md b/.changeset/archived/jolly-newts-roam.md similarity index 100% rename from .changeset/jolly-newts-roam.md rename to .changeset/archived/jolly-newts-roam.md diff --git a/.changeset/jolly-pandas-parade.md b/.changeset/archived/jolly-pandas-parade.md similarity index 100% rename from .changeset/jolly-pandas-parade.md rename to .changeset/archived/jolly-pandas-parade.md diff --git a/.changeset/jolly-pumas-dance.md b/.changeset/archived/jolly-pumas-dance.md similarity index 100% rename from .changeset/jolly-pumas-dance.md rename to .changeset/archived/jolly-pumas-dance.md diff --git a/.changeset/jolly-quails-caper.md b/.changeset/archived/jolly-quails-caper.md similarity index 100% rename from .changeset/jolly-quails-caper.md rename to .changeset/archived/jolly-quails-caper.md diff --git a/.changeset/jolly-wolves-hop.md b/.changeset/archived/jolly-wolves-hop.md similarity index 100% rename from .changeset/jolly-wolves-hop.md rename to .changeset/archived/jolly-wolves-hop.md diff --git a/.changeset/kind-foxes-click.md b/.changeset/archived/kind-foxes-click.md similarity index 100% rename from .changeset/kind-foxes-click.md rename to .changeset/archived/kind-foxes-click.md diff --git a/.changeset/kind-moles-dance.md b/.changeset/archived/kind-moles-dance.md similarity index 100% rename from .changeset/kind-moles-dance.md rename to .changeset/archived/kind-moles-dance.md diff --git a/.changeset/kind-tunas-gather.md b/.changeset/archived/kind-tunas-gather.md similarity index 100% rename from .changeset/kind-tunas-gather.md rename to .changeset/archived/kind-tunas-gather.md diff --git a/.changeset/lively-bears-sprint.md b/.changeset/archived/lively-bears-sprint.md similarity index 100% rename from .changeset/lively-bears-sprint.md rename to .changeset/archived/lively-bears-sprint.md diff --git a/.changeset/lively-foxes-roam.md b/.changeset/archived/lively-foxes-roam.md similarity index 100% rename from .changeset/lively-foxes-roam.md rename to .changeset/archived/lively-foxes-roam.md diff --git a/.changeset/lively-goats-run.md b/.changeset/archived/lively-goats-run.md similarity index 100% rename from .changeset/lively-goats-run.md rename to .changeset/archived/lively-goats-run.md diff --git a/.changeset/lively-lemurs-glide.md b/.changeset/archived/lively-lemurs-glide.md similarity index 100% rename from .changeset/lively-lemurs-glide.md rename to .changeset/archived/lively-lemurs-glide.md diff --git a/.changeset/lively-moles-caper.md b/.changeset/archived/lively-moles-caper.md similarity index 100% rename from .changeset/lively-moles-caper.md rename to .changeset/archived/lively-moles-caper.md diff --git a/.changeset/lively-newts-romp.md b/.changeset/archived/lively-newts-romp.md similarity index 100% rename from .changeset/lively-newts-romp.md rename to .changeset/archived/lively-newts-romp.md diff --git a/.changeset/lively-otters-gather.md b/.changeset/archived/lively-otters-gather.md similarity index 100% rename from .changeset/lively-otters-gather.md rename to .changeset/archived/lively-otters-gather.md diff --git a/.changeset/lucid-docs-rebrand.md b/.changeset/archived/lucid-docs-rebrand.md similarity index 100% rename from .changeset/lucid-docs-rebrand.md rename to .changeset/archived/lucid-docs-rebrand.md diff --git a/.changeset/lucky-birds-bark.md b/.changeset/archived/lucky-birds-bark.md similarity index 100% rename from .changeset/lucky-birds-bark.md rename to .changeset/archived/lucky-birds-bark.md diff --git a/.changeset/lucky-lynx-wave.md b/.changeset/archived/lucky-lynx-wave.md similarity index 100% rename from .changeset/lucky-lynx-wave.md rename to .changeset/archived/lucky-lynx-wave.md diff --git a/.changeset/mcp-token-budget-docs.md b/.changeset/archived/mcp-token-budget-docs.md similarity index 100% rename from .changeset/mcp-token-budget-docs.md rename to .changeset/archived/mcp-token-budget-docs.md diff --git a/.changeset/mellow-hawks-greet.md b/.changeset/archived/mellow-hawks-greet.md similarity index 100% rename from .changeset/mellow-hawks-greet.md rename to .changeset/archived/mellow-hawks-greet.md diff --git a/.changeset/mellow-herons-greet.md b/.changeset/archived/mellow-herons-greet.md similarity index 100% rename from .changeset/mellow-herons-greet.md rename to .changeset/archived/mellow-herons-greet.md diff --git a/.changeset/mellow-lemurs-click.md b/.changeset/archived/mellow-lemurs-click.md similarity index 100% rename from .changeset/mellow-lemurs-click.md rename to .changeset/archived/mellow-lemurs-click.md diff --git a/.changeset/mellow-lynx-forage.md b/.changeset/archived/mellow-lynx-forage.md similarity index 100% rename from .changeset/mellow-lynx-forage.md rename to .changeset/archived/mellow-lynx-forage.md diff --git a/.changeset/mellow-mice-forage.md b/.changeset/archived/mellow-mice-forage.md similarity index 100% rename from .changeset/mellow-mice-forage.md rename to .changeset/archived/mellow-mice-forage.md diff --git a/.changeset/mellow-tigers-gather.md b/.changeset/archived/mellow-tigers-gather.md similarity index 100% rename from .changeset/mellow-tigers-gather.md rename to .changeset/archived/mellow-tigers-gather.md diff --git a/.changeset/merry-foxes-climb.md b/.changeset/archived/merry-foxes-climb.md similarity index 100% rename from .changeset/merry-foxes-climb.md rename to .changeset/archived/merry-foxes-climb.md diff --git a/.changeset/merry-lynx-sing.md b/.changeset/archived/merry-lynx-sing.md similarity index 100% rename from .changeset/merry-lynx-sing.md rename to .changeset/archived/merry-lynx-sing.md diff --git a/.changeset/merry-lynx-wander.md b/.changeset/archived/merry-lynx-wander.md similarity index 100% rename from .changeset/merry-lynx-wander.md rename to .changeset/archived/merry-lynx-wander.md diff --git a/.changeset/merry-moles-chatter.md b/.changeset/archived/merry-moles-chatter.md similarity index 100% rename from .changeset/merry-moles-chatter.md rename to .changeset/archived/merry-moles-chatter.md diff --git a/.changeset/merry-quails-roam.md b/.changeset/archived/merry-quails-roam.md similarity index 100% rename from .changeset/merry-quails-roam.md rename to .changeset/archived/merry-quails-roam.md diff --git a/.changeset/migration-batch-10-ts.md b/.changeset/archived/migration-batch-10-ts.md similarity index 100% rename from .changeset/migration-batch-10-ts.md rename to .changeset/archived/migration-batch-10-ts.md diff --git a/.changeset/migration-batch-11-ts.md b/.changeset/archived/migration-batch-11-ts.md similarity index 100% rename from .changeset/migration-batch-11-ts.md rename to .changeset/archived/migration-batch-11-ts.md diff --git a/.changeset/migration-batch-12-ts.md b/.changeset/archived/migration-batch-12-ts.md similarity index 100% rename from .changeset/migration-batch-12-ts.md rename to .changeset/archived/migration-batch-12-ts.md diff --git a/.changeset/migration-batch-13-ts.md b/.changeset/archived/migration-batch-13-ts.md similarity index 100% rename from .changeset/migration-batch-13-ts.md rename to .changeset/archived/migration-batch-13-ts.md diff --git a/.changeset/migration-batch-14-ts.md b/.changeset/archived/migration-batch-14-ts.md similarity index 100% rename from .changeset/migration-batch-14-ts.md rename to .changeset/archived/migration-batch-14-ts.md diff --git a/.changeset/migration-batch-15-ts.md b/.changeset/archived/migration-batch-15-ts.md similarity index 100% rename from .changeset/migration-batch-15-ts.md rename to .changeset/archived/migration-batch-15-ts.md diff --git a/.changeset/migration-batch-2-ts.md b/.changeset/archived/migration-batch-2-ts.md similarity index 100% rename from .changeset/migration-batch-2-ts.md rename to .changeset/archived/migration-batch-2-ts.md diff --git a/.changeset/migration-batch-3-ts.md b/.changeset/archived/migration-batch-3-ts.md similarity index 100% rename from .changeset/migration-batch-3-ts.md rename to .changeset/archived/migration-batch-3-ts.md diff --git a/.changeset/migration-batch-4-ts.md b/.changeset/archived/migration-batch-4-ts.md similarity index 100% rename from .changeset/migration-batch-4-ts.md rename to .changeset/archived/migration-batch-4-ts.md diff --git a/.changeset/migration-batch-5-ts.md b/.changeset/archived/migration-batch-5-ts.md similarity index 100% rename from .changeset/migration-batch-5-ts.md rename to .changeset/archived/migration-batch-5-ts.md diff --git a/.changeset/migration-batch-6-ts.md b/.changeset/archived/migration-batch-6-ts.md similarity index 100% rename from .changeset/migration-batch-6-ts.md rename to .changeset/archived/migration-batch-6-ts.md diff --git a/.changeset/migration-batch-7-ts.md b/.changeset/archived/migration-batch-7-ts.md similarity index 100% rename from .changeset/migration-batch-7-ts.md rename to .changeset/archived/migration-batch-7-ts.md diff --git a/.changeset/migration-batch-8-ts.md b/.changeset/archived/migration-batch-8-ts.md similarity index 100% rename from .changeset/migration-batch-8-ts.md rename to .changeset/archived/migration-batch-8-ts.md diff --git a/.changeset/migration-core-ts.md b/.changeset/archived/migration-core-ts.md similarity index 100% rename from .changeset/migration-core-ts.md rename to .changeset/archived/migration-core-ts.md diff --git a/.changeset/migration-finalize-ts.md b/.changeset/archived/migration-finalize-ts.md similarity index 100% rename from .changeset/migration-finalize-ts.md rename to .changeset/archived/migration-finalize-ts.md diff --git a/.changeset/migration-milestone-ts.md b/.changeset/archived/migration-milestone-ts.md similarity index 100% rename from .changeset/migration-milestone-ts.md rename to .changeset/archived/migration-milestone-ts.md diff --git a/.changeset/mvp-concept-cleanup-canary-prep.md b/.changeset/archived/mvp-concept-cleanup-canary-prep.md similarity index 100% rename from .changeset/mvp-concept-cleanup-canary-prep.md rename to .changeset/archived/mvp-concept-cleanup-canary-prep.md diff --git a/.changeset/mvp-resolution-verbs-and-fix-sdk-mode.md b/.changeset/archived/mvp-resolution-verbs-and-fix-sdk-mode.md similarity index 100% rename from .changeset/mvp-resolution-verbs-and-fix-sdk-mode.md rename to .changeset/archived/mvp-resolution-verbs-and-fix-sdk-mode.md diff --git a/.changeset/new-project-agent-diagnostics.md b/.changeset/archived/new-project-agent-diagnostics.md similarity index 100% rename from .changeset/new-project-agent-diagnostics.md rename to .changeset/archived/new-project-agent-diagnostics.md diff --git a/.changeset/nimble-deer-chatter.md b/.changeset/archived/nimble-deer-chatter.md similarity index 100% rename from .changeset/nimble-deer-chatter.md rename to .changeset/archived/nimble-deer-chatter.md diff --git a/.changeset/nimble-eagles-romp.md b/.changeset/archived/nimble-eagles-romp.md similarity index 100% rename from .changeset/nimble-eagles-romp.md rename to .changeset/archived/nimble-eagles-romp.md diff --git a/.changeset/nimble-lynx-tumble.md b/.changeset/archived/nimble-lynx-tumble.md similarity index 100% rename from .changeset/nimble-lynx-tumble.md rename to .changeset/archived/nimble-lynx-tumble.md diff --git a/.changeset/nimble-seals-munch.md b/.changeset/archived/nimble-seals-munch.md similarity index 100% rename from .changeset/nimble-seals-munch.md rename to .changeset/archived/nimble-seals-munch.md diff --git a/.changeset/nimble-sloths-zip.md b/.changeset/archived/nimble-sloths-zip.md similarity index 100% rename from .changeset/nimble-sloths-zip.md rename to .changeset/archived/nimble-sloths-zip.md diff --git a/.changeset/nimble-wolves-dart.md b/.changeset/archived/nimble-wolves-dart.md similarity index 100% rename from .changeset/nimble-wolves-dart.md rename to .changeset/archived/nimble-wolves-dart.md diff --git a/.changeset/noble-badgers-roar.md b/.changeset/archived/noble-badgers-roar.md similarity index 100% rename from .changeset/noble-badgers-roar.md rename to .changeset/archived/noble-badgers-roar.md diff --git a/.changeset/noble-jaguars-squeak.md b/.changeset/archived/noble-jaguars-squeak.md similarity index 100% rename from .changeset/noble-jaguars-squeak.md rename to .changeset/archived/noble-jaguars-squeak.md diff --git a/.changeset/noble-mice-squeak.md b/.changeset/archived/noble-mice-squeak.md similarity index 100% rename from .changeset/noble-mice-squeak.md rename to .changeset/archived/noble-mice-squeak.md diff --git a/.changeset/noble-otters-hop.md b/.changeset/archived/noble-otters-hop.md similarity index 100% rename from .changeset/noble-otters-hop.md rename to .changeset/archived/noble-otters-hop.md diff --git a/.changeset/noble-tigers-parade.md b/.changeset/archived/noble-tigers-parade.md similarity index 100% rename from .changeset/noble-tigers-parade.md rename to .changeset/archived/noble-tigers-parade.md diff --git a/.changeset/noble-yaks-zip.md b/.changeset/archived/noble-yaks-zip.md similarity index 100% rename from .changeset/noble-yaks-zip.md rename to .changeset/archived/noble-yaks-zip.md diff --git a/.changeset/opengsd-org-rename.md b/.changeset/archived/opengsd-org-rename.md similarity index 100% rename from .changeset/opengsd-org-rename.md rename to .changeset/archived/opengsd-org-rename.md diff --git a/.changeset/patient-ibex-gather.md b/.changeset/archived/patient-ibex-gather.md similarity index 100% rename from .changeset/patient-ibex-gather.md rename to .changeset/archived/patient-ibex-gather.md diff --git a/.changeset/patient-ibex-wake.md b/.changeset/archived/patient-ibex-wake.md similarity index 100% rename from .changeset/patient-ibex-wake.md rename to .changeset/archived/patient-ibex-wake.md diff --git a/.changeset/patient-lemurs-sing.md b/.changeset/archived/patient-lemurs-sing.md similarity index 100% rename from .changeset/patient-lemurs-sing.md rename to .changeset/archived/patient-lemurs-sing.md diff --git a/.changeset/patient-voles-swim.md b/.changeset/archived/patient-voles-swim.md similarity index 100% rename from .changeset/patient-voles-swim.md rename to .changeset/archived/patient-voles-swim.md diff --git a/.changeset/per-phase-type-models.md b/.changeset/archived/per-phase-type-models.md similarity index 100% rename from .changeset/per-phase-type-models.md rename to .changeset/archived/per-phase-type-models.md diff --git a/.changeset/phase-five-installer-migration-guardrails.md b/.changeset/archived/phase-five-installer-migration-guardrails.md similarity index 100% rename from .changeset/phase-five-installer-migration-guardrails.md rename to .changeset/archived/phase-five-installer-migration-guardrails.md diff --git a/.changeset/plucky-cats-purr.md b/.changeset/archived/plucky-cats-purr.md similarity index 100% rename from .changeset/plucky-cats-purr.md rename to .changeset/archived/plucky-cats-purr.md diff --git a/.changeset/plucky-eagles-roar.md b/.changeset/archived/plucky-eagles-roar.md similarity index 100% rename from .changeset/plucky-eagles-roar.md rename to .changeset/archived/plucky-eagles-roar.md diff --git a/.changeset/plucky-herons-sing.md b/.changeset/archived/plucky-herons-sing.md similarity index 100% rename from .changeset/plucky-herons-sing.md rename to .changeset/archived/plucky-herons-sing.md diff --git a/.changeset/plucky-ibex-gather.md b/.changeset/archived/plucky-ibex-gather.md similarity index 100% rename from .changeset/plucky-ibex-gather.md rename to .changeset/archived/plucky-ibex-gather.md diff --git a/.changeset/plucky-lemurs-dance.md b/.changeset/archived/plucky-lemurs-dance.md similarity index 100% rename from .changeset/plucky-lemurs-dance.md rename to .changeset/archived/plucky-lemurs-dance.md diff --git a/.changeset/plucky-moles-roam.md b/.changeset/archived/plucky-moles-roam.md similarity index 100% rename from .changeset/plucky-moles-roam.md rename to .changeset/archived/plucky-moles-roam.md diff --git a/.changeset/plucky-otters-roam.md b/.changeset/archived/plucky-otters-roam.md similarity index 100% rename from .changeset/plucky-otters-roam.md rename to .changeset/archived/plucky-otters-roam.md diff --git a/.changeset/plucky-pandas-sprint.md b/.changeset/archived/plucky-pandas-sprint.md similarity index 100% rename from .changeset/plucky-pandas-sprint.md rename to .changeset/archived/plucky-pandas-sprint.md diff --git a/.changeset/plucky-pumas-tumble.md b/.changeset/archived/plucky-pumas-tumble.md similarity index 100% rename from .changeset/plucky-pumas-tumble.md rename to .changeset/archived/plucky-pumas-tumble.md diff --git a/.changeset/plucky-rams-climb.md b/.changeset/archived/plucky-rams-climb.md similarity index 100% rename from .changeset/plucky-rams-climb.md rename to .changeset/archived/plucky-rams-climb.md diff --git a/.changeset/plucky-tunas-howl.md b/.changeset/archived/plucky-tunas-howl.md similarity index 100% rename from .changeset/plucky-tunas-howl.md rename to .changeset/archived/plucky-tunas-howl.md diff --git a/.changeset/plucky-yaks-forage.md b/.changeset/archived/plucky-yaks-forage.md similarity index 100% rename from .changeset/plucky-yaks-forage.md rename to .changeset/archived/plucky-yaks-forage.md diff --git a/.changeset/portable-bash-shebang-hooks.md b/.changeset/archived/portable-bash-shebang-hooks.md similarity index 100% rename from .changeset/portable-bash-shebang-hooks.md rename to .changeset/archived/portable-bash-shebang-hooks.md diff --git a/.changeset/pr-3112-release-note.md b/.changeset/archived/pr-3112-release-note.md similarity index 100% rename from .changeset/pr-3112-release-note.md rename to .changeset/archived/pr-3112-release-note.md diff --git a/.changeset/pr-3113-release-note.md b/.changeset/archived/pr-3113-release-note.md similarity index 100% rename from .changeset/pr-3113-release-note.md rename to .changeset/archived/pr-3113-release-note.md diff --git a/.changeset/pr-3115-release-note.md b/.changeset/archived/pr-3115-release-note.md similarity index 100% rename from .changeset/pr-3115-release-note.md rename to .changeset/archived/pr-3115-release-note.md diff --git a/.changeset/pr-3116-release-note.md b/.changeset/archived/pr-3116-release-note.md similarity index 100% rename from .changeset/pr-3116-release-note.md rename to .changeset/archived/pr-3116-release-note.md diff --git a/.changeset/pr-3118-release-note.md b/.changeset/archived/pr-3118-release-note.md similarity index 100% rename from .changeset/pr-3118-release-note.md rename to .changeset/archived/pr-3118-release-note.md diff --git a/.changeset/pr-3123-release-note.md b/.changeset/archived/pr-3123-release-note.md similarity index 100% rename from .changeset/pr-3123-release-note.md rename to .changeset/archived/pr-3123-release-note.md diff --git a/.changeset/pr-3124-release-note.md b/.changeset/archived/pr-3124-release-note.md similarity index 100% rename from .changeset/pr-3124-release-note.md rename to .changeset/archived/pr-3124-release-note.md diff --git a/.changeset/pr-3125-release-note.md b/.changeset/archived/pr-3125-release-note.md similarity index 100% rename from .changeset/pr-3125-release-note.md rename to .changeset/archived/pr-3125-release-note.md diff --git a/.changeset/proud-elks-snooze.md b/.changeset/archived/proud-elks-snooze.md similarity index 100% rename from .changeset/proud-elks-snooze.md rename to .changeset/archived/proud-elks-snooze.md diff --git a/.changeset/proud-sloths-rally.md b/.changeset/archived/proud-sloths-rally.md similarity index 100% rename from .changeset/proud-sloths-rally.md rename to .changeset/archived/proud-sloths-rally.md diff --git a/.changeset/quick-cranes-purr.md b/.changeset/archived/quick-cranes-purr.md similarity index 100% rename from .changeset/quick-cranes-purr.md rename to .changeset/archived/quick-cranes-purr.md diff --git a/.changeset/quick-deer-squeak.md b/.changeset/archived/quick-deer-squeak.md similarity index 100% rename from .changeset/quick-deer-squeak.md rename to .changeset/archived/quick-deer-squeak.md diff --git a/.changeset/quick-geese-hum.md b/.changeset/archived/quick-geese-hum.md similarity index 100% rename from .changeset/quick-geese-hum.md rename to .changeset/archived/quick-geese-hum.md diff --git a/.changeset/quick-otters-run.md b/.changeset/archived/quick-otters-run.md similarity index 100% rename from .changeset/quick-otters-run.md rename to .changeset/archived/quick-otters-run.md diff --git a/.changeset/quick-pumas-fly.md b/.changeset/archived/quick-pumas-fly.md similarity index 100% rename from .changeset/quick-pumas-fly.md rename to .changeset/archived/quick-pumas-fly.md diff --git a/.changeset/quick-quails-wake.md b/.changeset/archived/quick-quails-wake.md similarity index 100% rename from .changeset/quick-quails-wake.md rename to .changeset/archived/quick-quails-wake.md diff --git a/.changeset/quick-rams-wave.md b/.changeset/archived/quick-rams-wave.md similarity index 100% rename from .changeset/quick-rams-wave.md rename to .changeset/archived/quick-rams-wave.md diff --git a/.changeset/quick-voles-sprint.md b/.changeset/archived/quick-voles-sprint.md similarity index 100% rename from .changeset/quick-voles-sprint.md rename to .changeset/archived/quick-voles-sprint.md diff --git a/.changeset/quiet-geckos-switch.md b/.changeset/archived/quiet-geckos-switch.md similarity index 100% rename from .changeset/quiet-geckos-switch.md rename to .changeset/archived/quiet-geckos-switch.md diff --git a/.changeset/rapid-dogs-run.md b/.changeset/archived/rapid-dogs-run.md similarity index 100% rename from .changeset/rapid-dogs-run.md rename to .changeset/archived/rapid-dogs-run.md diff --git a/.changeset/rapid-goats-munch.md b/.changeset/archived/rapid-goats-munch.md similarity index 100% rename from .changeset/rapid-goats-munch.md rename to .changeset/archived/rapid-goats-munch.md diff --git a/.changeset/rapid-voles-hop.md b/.changeset/archived/rapid-voles-hop.md similarity index 100% rename from .changeset/rapid-voles-hop.md rename to .changeset/archived/rapid-voles-hop.md diff --git a/.changeset/research-flag-and-stale-refs.md b/.changeset/archived/research-flag-and-stale-refs.md similarity index 100% rename from .changeset/research-flag-and-stale-refs.md rename to .changeset/archived/research-flag-and-stale-refs.md diff --git a/.changeset/rewire-orphaned-workflows-3131.md b/.changeset/archived/rewire-orphaned-workflows-3131.md similarity index 100% rename from .changeset/rewire-orphaned-workflows-3131.md rename to .changeset/archived/rewire-orphaned-workflows-3131.md diff --git a/.changeset/scrub-stale-command-routes.md b/.changeset/archived/scrub-stale-command-routes.md similarity index 100% rename from .changeset/scrub-stale-command-routes.md rename to .changeset/archived/scrub-stale-command-routes.md diff --git a/.changeset/sdk-init-phase-flags.md b/.changeset/archived/sdk-init-phase-flags.md similarity index 100% rename from .changeset/sdk-init-phase-flags.md rename to .changeset/archived/sdk-init-phase-flags.md diff --git a/.changeset/serene-pandas-zip.md b/.changeset/archived/serene-pandas-zip.md similarity index 100% rename from .changeset/serene-pandas-zip.md rename to .changeset/archived/serene-pandas-zip.md diff --git a/.changeset/sharp-badgers-squeak.md b/.changeset/archived/sharp-badgers-squeak.md similarity index 100% rename from .changeset/sharp-badgers-squeak.md rename to .changeset/archived/sharp-badgers-squeak.md diff --git a/.changeset/sharp-quails-leap.md b/.changeset/archived/sharp-quails-leap.md similarity index 100% rename from .changeset/sharp-quails-leap.md rename to .changeset/archived/sharp-quails-leap.md diff --git a/.changeset/sharp-yaks-climb.md b/.changeset/archived/sharp-yaks-climb.md similarity index 100% rename from .changeset/sharp-yaks-climb.md rename to .changeset/archived/sharp-yaks-climb.md diff --git a/.changeset/shell-projection-cleanup.md b/.changeset/archived/shell-projection-cleanup.md similarity index 100% rename from .changeset/shell-projection-cleanup.md rename to .changeset/archived/shell-projection-cleanup.md diff --git a/.changeset/shell-projection-fs-migration.md b/.changeset/archived/shell-projection-fs-migration.md similarity index 100% rename from .changeset/shell-projection-fs-migration.md rename to .changeset/archived/shell-projection-fs-migration.md diff --git a/.changeset/shell-projection-io-seam.md b/.changeset/archived/shell-projection-io-seam.md similarity index 100% rename from .changeset/shell-projection-io-seam.md rename to .changeset/archived/shell-projection-io-seam.md diff --git a/.changeset/shell-projection-subprocess-migration.md b/.changeset/archived/shell-projection-subprocess-migration.md similarity index 100% rename from .changeset/shell-projection-subprocess-migration.md rename to .changeset/archived/shell-projection-subprocess-migration.md diff --git a/.changeset/ship-verification-actionable.md b/.changeset/archived/ship-verification-actionable.md similarity index 100% rename from .changeset/ship-verification-actionable.md rename to .changeset/archived/ship-verification-actionable.md diff --git a/.changeset/silly-badgers-frolic.md b/.changeset/archived/silly-badgers-frolic.md similarity index 100% rename from .changeset/silly-badgers-frolic.md rename to .changeset/archived/silly-badgers-frolic.md diff --git a/.changeset/silly-finches-travel.md b/.changeset/archived/silly-finches-travel.md similarity index 100% rename from .changeset/silly-finches-travel.md rename to .changeset/archived/silly-finches-travel.md diff --git a/.changeset/silly-foxes-sing.md b/.changeset/archived/silly-foxes-sing.md similarity index 100% rename from .changeset/silly-foxes-sing.md rename to .changeset/archived/silly-foxes-sing.md diff --git a/.changeset/silly-foxes-wander.md b/.changeset/archived/silly-foxes-wander.md similarity index 100% rename from .changeset/silly-foxes-wander.md rename to .changeset/archived/silly-foxes-wander.md diff --git a/.changeset/silly-jaguars-sing.md b/.changeset/archived/silly-jaguars-sing.md similarity index 100% rename from .changeset/silly-jaguars-sing.md rename to .changeset/archived/silly-jaguars-sing.md diff --git a/.changeset/silly-jaguars-swim.md b/.changeset/archived/silly-jaguars-swim.md similarity index 100% rename from .changeset/silly-jaguars-swim.md rename to .changeset/archived/silly-jaguars-swim.md diff --git a/.changeset/silly-newts-swim.md b/.changeset/archived/silly-newts-swim.md similarity index 100% rename from .changeset/silly-newts-swim.md rename to .changeset/archived/silly-newts-swim.md diff --git a/.changeset/silly-orcas-dance.md b/.changeset/archived/silly-orcas-dance.md similarity index 100% rename from .changeset/silly-orcas-dance.md rename to .changeset/archived/silly-orcas-dance.md diff --git a/.changeset/silly-seals-parade.md b/.changeset/archived/silly-seals-parade.md similarity index 100% rename from .changeset/silly-seals-parade.md rename to .changeset/archived/silly-seals-parade.md diff --git a/.changeset/silly-yaks-parade.md b/.changeset/archived/silly-yaks-parade.md similarity index 100% rename from .changeset/silly-yaks-parade.md rename to .changeset/archived/silly-yaks-parade.md diff --git a/.changeset/steady-badgers-rest.md b/.changeset/archived/steady-badgers-rest.md similarity index 100% rename from .changeset/steady-badgers-rest.md rename to .changeset/archived/steady-badgers-rest.md diff --git a/.changeset/steady-bears-purr.md b/.changeset/archived/steady-bears-purr.md similarity index 100% rename from .changeset/steady-bears-purr.md rename to .changeset/archived/steady-bears-purr.md diff --git a/.changeset/steady-geese-hum.md b/.changeset/archived/steady-geese-hum.md similarity index 100% rename from .changeset/steady-geese-hum.md rename to .changeset/archived/steady-geese-hum.md diff --git a/.changeset/steady-jays-click.md b/.changeset/archived/steady-jays-click.md similarity index 100% rename from .changeset/steady-jays-click.md rename to .changeset/archived/steady-jays-click.md diff --git a/.changeset/steady-jays-sing.md b/.changeset/archived/steady-jays-sing.md similarity index 100% rename from .changeset/steady-jays-sing.md rename to .changeset/archived/steady-jays-sing.md diff --git a/.changeset/steady-pandas-purr.md b/.changeset/archived/steady-pandas-purr.md similarity index 100% rename from .changeset/steady-pandas-purr.md rename to .changeset/archived/steady-pandas-purr.md diff --git a/.changeset/steady-ravens-shape.md b/.changeset/archived/steady-ravens-shape.md similarity index 100% rename from .changeset/steady-ravens-shape.md rename to .changeset/archived/steady-ravens-shape.md diff --git a/.changeset/steady-shell-projection.md b/.changeset/archived/steady-shell-projection.md similarity index 100% rename from .changeset/steady-shell-projection.md rename to .changeset/archived/steady-shell-projection.md diff --git a/.changeset/steady-yaks-caper.md b/.changeset/archived/steady-yaks-caper.md similarity index 100% rename from .changeset/steady-yaks-caper.md rename to .changeset/archived/steady-yaks-caper.md diff --git a/.changeset/steady-zebras-click.md b/.changeset/archived/steady-zebras-click.md similarity index 100% rename from .changeset/steady-zebras-click.md rename to .changeset/archived/steady-zebras-click.md diff --git a/.changeset/sturdy-finches-fly.md b/.changeset/archived/sturdy-finches-fly.md similarity index 100% rename from .changeset/sturdy-finches-fly.md rename to .changeset/archived/sturdy-finches-fly.md diff --git a/.changeset/sturdy-finches-sprint.md b/.changeset/archived/sturdy-finches-sprint.md similarity index 100% rename from .changeset/sturdy-finches-sprint.md rename to .changeset/archived/sturdy-finches-sprint.md diff --git a/.changeset/sturdy-geese-glide.md b/.changeset/archived/sturdy-geese-glide.md similarity index 100% rename from .changeset/sturdy-geese-glide.md rename to .changeset/archived/sturdy-geese-glide.md diff --git a/.changeset/sturdy-geese-roam.md b/.changeset/archived/sturdy-geese-roam.md similarity index 100% rename from .changeset/sturdy-geese-roam.md rename to .changeset/archived/sturdy-geese-roam.md diff --git a/.changeset/sturdy-jays-glide.md b/.changeset/archived/sturdy-jays-glide.md similarity index 100% rename from .changeset/sturdy-jays-glide.md rename to .changeset/archived/sturdy-jays-glide.md diff --git a/.changeset/sturdy-lynx-forage.md b/.changeset/archived/sturdy-lynx-forage.md similarity index 100% rename from .changeset/sturdy-lynx-forage.md rename to .changeset/archived/sturdy-lynx-forage.md diff --git a/.changeset/sturdy-lynx-march.md b/.changeset/archived/sturdy-lynx-march.md similarity index 100% rename from .changeset/sturdy-lynx-march.md rename to .changeset/archived/sturdy-lynx-march.md diff --git a/.changeset/sturdy-moles-bark.md b/.changeset/archived/sturdy-moles-bark.md similarity index 100% rename from .changeset/sturdy-moles-bark.md rename to .changeset/archived/sturdy-moles-bark.md diff --git a/.changeset/sturdy-otters-purr.md b/.changeset/archived/sturdy-otters-purr.md similarity index 100% rename from .changeset/sturdy-otters-purr.md rename to .changeset/archived/sturdy-otters-purr.md diff --git a/.changeset/sturdy-pandas-rest.md b/.changeset/archived/sturdy-pandas-rest.md similarity index 100% rename from .changeset/sturdy-pandas-rest.md rename to .changeset/archived/sturdy-pandas-rest.md diff --git a/.changeset/sturdy-pumas-sing.md b/.changeset/archived/sturdy-pumas-sing.md similarity index 100% rename from .changeset/sturdy-pumas-sing.md rename to .changeset/archived/sturdy-pumas-sing.md diff --git a/.changeset/sturdy-rams-caper.md b/.changeset/archived/sturdy-rams-caper.md similarity index 100% rename from .changeset/sturdy-rams-caper.md rename to .changeset/archived/sturdy-rams-caper.md diff --git a/.changeset/sturdy-rams-forage.md b/.changeset/archived/sturdy-rams-forage.md similarity index 100% rename from .changeset/sturdy-rams-forage.md rename to .changeset/archived/sturdy-rams-forage.md diff --git a/.changeset/sturdy-sloths-hum.md b/.changeset/archived/sturdy-sloths-hum.md similarity index 100% rename from .changeset/sturdy-sloths-hum.md rename to .changeset/archived/sturdy-sloths-hum.md diff --git a/.changeset/sturdy-wolves-frolic.md b/.changeset/archived/sturdy-wolves-frolic.md similarity index 100% rename from .changeset/sturdy-wolves-frolic.md rename to .changeset/archived/sturdy-wolves-frolic.md diff --git a/.changeset/sturdy-wolves-glide.md b/.changeset/archived/sturdy-wolves-glide.md similarity index 100% rename from .changeset/sturdy-wolves-glide.md rename to .changeset/archived/sturdy-wolves-glide.md diff --git a/.changeset/sturdy-writers-survive.md b/.changeset/archived/sturdy-writers-survive.md similarity index 100% rename from .changeset/sturdy-writers-survive.md rename to .changeset/archived/sturdy-writers-survive.md diff --git a/.changeset/sunny-dogs-frolic.md b/.changeset/archived/sunny-dogs-frolic.md similarity index 100% rename from .changeset/sunny-dogs-frolic.md rename to .changeset/archived/sunny-dogs-frolic.md diff --git a/.changeset/sunny-herons-bark.md b/.changeset/archived/sunny-herons-bark.md similarity index 100% rename from .changeset/sunny-herons-bark.md rename to .changeset/archived/sunny-herons-bark.md diff --git a/.changeset/sunny-ibex-wave.md b/.changeset/archived/sunny-ibex-wave.md similarity index 100% rename from .changeset/sunny-ibex-wave.md rename to .changeset/archived/sunny-ibex-wave.md diff --git a/.changeset/sunny-lynx-rally.md b/.changeset/archived/sunny-lynx-rally.md similarity index 100% rename from .changeset/sunny-lynx-rally.md rename to .changeset/archived/sunny-lynx-rally.md diff --git a/.changeset/sunny-pandas-dance.md b/.changeset/archived/sunny-pandas-dance.md similarity index 100% rename from .changeset/sunny-pandas-dance.md rename to .changeset/archived/sunny-pandas-dance.md diff --git a/.changeset/sunny-quails-swim.md b/.changeset/archived/sunny-quails-swim.md similarity index 100% rename from .changeset/sunny-quails-swim.md rename to .changeset/archived/sunny-quails-swim.md diff --git a/.changeset/swift-coyotes-document.md b/.changeset/archived/swift-coyotes-document.md similarity index 100% rename from .changeset/swift-coyotes-document.md rename to .changeset/archived/swift-coyotes-document.md diff --git a/.changeset/swift-otter-hum.md b/.changeset/archived/swift-otter-hum.md similarity index 100% rename from .changeset/swift-otter-hum.md rename to .changeset/archived/swift-otter-hum.md diff --git a/.changeset/swift-otter-pebble.md b/.changeset/archived/swift-otter-pebble.md similarity index 100% rename from .changeset/swift-otter-pebble.md rename to .changeset/archived/swift-otter-pebble.md diff --git a/.changeset/swift-sets-dedupe.md b/.changeset/archived/swift-sets-dedupe.md similarity index 100% rename from .changeset/swift-sets-dedupe.md rename to .changeset/archived/swift-sets-dedupe.md diff --git a/.changeset/tidy-finches-caper.md b/.changeset/archived/tidy-finches-caper.md similarity index 100% rename from .changeset/tidy-finches-caper.md rename to .changeset/archived/tidy-finches-caper.md diff --git a/.changeset/tidy-goats-cheer.md b/.changeset/archived/tidy-goats-cheer.md similarity index 100% rename from .changeset/tidy-goats-cheer.md rename to .changeset/archived/tidy-goats-cheer.md diff --git a/.changeset/tidy-herons-dance.md b/.changeset/archived/tidy-herons-dance.md similarity index 100% rename from .changeset/tidy-herons-dance.md rename to .changeset/archived/tidy-herons-dance.md diff --git a/.changeset/tidy-moles-rest.md b/.changeset/archived/tidy-moles-rest.md similarity index 100% rename from .changeset/tidy-moles-rest.md rename to .changeset/archived/tidy-moles-rest.md diff --git a/.changeset/tidy-orcas-romp.md b/.changeset/archived/tidy-orcas-romp.md similarity index 100% rename from .changeset/tidy-orcas-romp.md rename to .changeset/archived/tidy-orcas-romp.md diff --git a/.changeset/tidy-tigers-dance.md b/.changeset/archived/tidy-tigers-dance.md similarity index 100% rename from .changeset/tidy-tigers-dance.md rename to .changeset/archived/tidy-tigers-dance.md diff --git a/.changeset/tidy-tunas-zip.md b/.changeset/archived/tidy-tunas-zip.md similarity index 100% rename from .changeset/tidy-tunas-zip.md rename to .changeset/archived/tidy-tunas-zip.md diff --git a/.changeset/tidy-voles-rally.md b/.changeset/archived/tidy-voles-rally.md similarity index 100% rename from .changeset/tidy-voles-rally.md rename to .changeset/archived/tidy-voles-rally.md diff --git a/.changeset/typed-json-surfaces-455.md b/.changeset/archived/typed-json-surfaces-455.md similarity index 100% rename from .changeset/typed-json-surfaces-455.md rename to .changeset/archived/typed-json-surfaces-455.md diff --git a/.changeset/typed-rivers-flow.md b/.changeset/archived/typed-rivers-flow.md similarity index 100% rename from .changeset/typed-rivers-flow.md rename to .changeset/archived/typed-rivers-flow.md diff --git a/.changeset/update-banner-opt-in.md b/.changeset/archived/update-banner-opt-in.md similarity index 100% rename from .changeset/update-banner-opt-in.md rename to .changeset/archived/update-banner-opt-in.md diff --git a/.changeset/verifier-debt-gate.md b/.changeset/archived/verifier-debt-gate.md similarity index 100% rename from .changeset/verifier-debt-gate.md rename to .changeset/archived/verifier-debt-gate.md diff --git a/.changeset/vivid-eagles-climb.md b/.changeset/archived/vivid-eagles-climb.md similarity index 100% rename from .changeset/vivid-eagles-climb.md rename to .changeset/archived/vivid-eagles-climb.md diff --git a/.changeset/vivid-foxes-romp.md b/.changeset/archived/vivid-foxes-romp.md similarity index 100% rename from .changeset/vivid-foxes-romp.md rename to .changeset/archived/vivid-foxes-romp.md diff --git a/.changeset/vivid-jaguars-tumble.md b/.changeset/archived/vivid-jaguars-tumble.md similarity index 100% rename from .changeset/vivid-jaguars-tumble.md rename to .changeset/archived/vivid-jaguars-tumble.md diff --git a/.changeset/vivid-tunas-sing.md b/.changeset/archived/vivid-tunas-sing.md similarity index 100% rename from .changeset/vivid-tunas-sing.md rename to .changeset/archived/vivid-tunas-sing.md diff --git a/.changeset/w005-w006-i001-generator-migration.md b/.changeset/archived/w005-w006-i001-generator-migration.md similarity index 100% rename from .changeset/w005-w006-i001-generator-migration.md rename to .changeset/archived/w005-w006-i001-generator-migration.md diff --git a/.changeset/windows-npm-shell-fix.md b/.changeset/archived/windows-npm-shell-fix.md similarity index 100% rename from .changeset/windows-npm-shell-fix.md rename to .changeset/archived/windows-npm-shell-fix.md diff --git a/.changeset/wise-foxes-romp.md b/.changeset/archived/wise-foxes-romp.md similarity index 100% rename from .changeset/wise-foxes-romp.md rename to .changeset/archived/wise-foxes-romp.md diff --git a/.changeset/wise-hawks-bark.md b/.changeset/archived/wise-hawks-bark.md similarity index 100% rename from .changeset/wise-hawks-bark.md rename to .changeset/archived/wise-hawks-bark.md diff --git a/.changeset/wise-koalas-climb.md b/.changeset/archived/wise-koalas-climb.md similarity index 100% rename from .changeset/wise-koalas-climb.md rename to .changeset/archived/wise-koalas-climb.md diff --git a/.changeset/wise-mice-cheer.md b/.changeset/archived/wise-mice-cheer.md similarity index 100% rename from .changeset/wise-mice-cheer.md rename to .changeset/archived/wise-mice-cheer.md diff --git a/.changeset/wise-pumas-glide.md b/.changeset/archived/wise-pumas-glide.md similarity index 100% rename from .changeset/wise-pumas-glide.md rename to .changeset/archived/wise-pumas-glide.md diff --git a/.changeset/wise-pumas-march.md b/.changeset/archived/wise-pumas-march.md similarity index 100% rename from .changeset/wise-pumas-march.md rename to .changeset/archived/wise-pumas-march.md diff --git a/.changeset/wise-rams-gather.md b/.changeset/archived/wise-rams-gather.md similarity index 100% rename from .changeset/wise-rams-gather.md rename to .changeset/archived/wise-rams-gather.md diff --git a/.changeset/wise-yaks-run.md b/.changeset/archived/wise-yaks-run.md similarity index 100% rename from .changeset/wise-yaks-run.md rename to .changeset/archived/wise-yaks-run.md diff --git a/.changeset/witty-bears-climb.md b/.changeset/archived/witty-bears-climb.md similarity index 100% rename from .changeset/witty-bears-climb.md rename to .changeset/archived/witty-bears-climb.md diff --git a/.changeset/witty-geese-purr.md b/.changeset/archived/witty-geese-purr.md similarity index 100% rename from .changeset/witty-geese-purr.md rename to .changeset/archived/witty-geese-purr.md diff --git a/.changeset/witty-hawks-jump.md b/.changeset/archived/witty-hawks-jump.md similarity index 100% rename from .changeset/witty-hawks-jump.md rename to .changeset/archived/witty-hawks-jump.md diff --git a/.changeset/witty-newts-greet.md b/.changeset/archived/witty-newts-greet.md similarity index 100% rename from .changeset/witty-newts-greet.md rename to .changeset/archived/witty-newts-greet.md diff --git a/.changeset/witty-wasps-hum.md b/.changeset/archived/witty-wasps-hum.md similarity index 100% rename from .changeset/witty-wasps-hum.md rename to .changeset/archived/witty-wasps-hum.md diff --git a/.changeset/yaml-echo-colon-escape.md b/.changeset/archived/yaml-echo-colon-escape.md similarity index 100% rename from .changeset/yaml-echo-colon-escape.md rename to .changeset/archived/yaml-echo-colon-escape.md diff --git a/.changeset/zesty-eagles-fly.md b/.changeset/archived/zesty-eagles-fly.md similarity index 100% rename from .changeset/zesty-eagles-fly.md rename to .changeset/archived/zesty-eagles-fly.md diff --git a/.changeset/zesty-goats-dart.md b/.changeset/archived/zesty-goats-dart.md similarity index 100% rename from .changeset/zesty-goats-dart.md rename to .changeset/archived/zesty-goats-dart.md diff --git a/.changeset/zesty-jays-wake.md b/.changeset/archived/zesty-jays-wake.md similarity index 100% rename from .changeset/zesty-jays-wake.md rename to .changeset/archived/zesty-jays-wake.md diff --git a/.changeset/zesty-moles-forage.md b/.changeset/archived/zesty-moles-forage.md similarity index 100% rename from .changeset/zesty-moles-forage.md rename to .changeset/archived/zesty-moles-forage.md diff --git a/.changeset/zesty-quails-wave.md b/.changeset/archived/zesty-quails-wave.md similarity index 100% rename from .changeset/zesty-quails-wave.md rename to .changeset/archived/zesty-quails-wave.md diff --git a/.changeset/zesty-ravens-rest.md b/.changeset/archived/zesty-ravens-rest.md similarity index 100% rename from .changeset/zesty-ravens-rest.md rename to .changeset/archived/zesty-ravens-rest.md diff --git a/.changeset/zesty-voles-roar.md b/.changeset/archived/zesty-voles-roar.md similarity index 100% rename from .changeset/zesty-voles-roar.md rename to .changeset/archived/zesty-voles-roar.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 000000000..bcc39f7a6 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,23 @@ +{ + "name": "gsd-core", + "displayName": "GSD Core", + "version": "1.4.0", + "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.", + "author": { + "name": "open-gsd", + "url": "https://github.com/open-gsd" + }, + "homepage": "https://github.com/open-gsd/gsd-core", + "repository": "https://github.com/open-gsd/gsd-core", + "license": "MIT", + "keywords": [ + "spec-driven-development", + "planning", + "workflow", + "context-engineering", + "claude-code", + "gsd" + ], + "commands": "./commands/gsd/", + "hooks": "./hooks/hooks.json" +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c1623c685..f1bfa3727 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -13,6 +13,14 @@ body: > 2. Redact usernames, paths, and API keys (e.g., replace `/Users/yourname/` with `/Users/REDACTED/`) > 3. Or run your logs through an anonymizer — we recommend **[presidio-anonymizer](https://microsoft.github.io/presidio/)** (open-source, local-only) or **[scrub](https://github.com/dssg/scrub)** before pasting + - type: checkboxes + id: preflight + attributes: + label: Pre-submission checklist + options: + - label: I have searched existing issues and this bug has not already been reported + required: true + - type: input id: version attributes: diff --git a/.github/ISSUE_TEMPLATE/docs_issue.yml b/.github/ISSUE_TEMPLATE/docs_issue.yml index b40577b30..ce1e324a4 100644 --- a/.github/ISSUE_TEMPLATE/docs_issue.yml +++ b/.github/ISSUE_TEMPLATE/docs_issue.yml @@ -8,6 +8,14 @@ body: value: | Help us improve the docs. Point us to what's wrong or missing. + - type: checkboxes + id: preflight + attributes: + label: Pre-submission checklist + options: + - label: I have searched existing issues and this documentation problem has not already been reported + required: true + - type: dropdown id: type attributes: diff --git a/.github/workflows/close-draft-prs-sweep.yml b/.github/workflows/close-draft-prs-sweep.yml new file mode 100644 index 000000000..160bea5f7 --- /dev/null +++ b/.github/workflows/close-draft-prs-sweep.yml @@ -0,0 +1,112 @@ +name: Close Draft PRs (sweep) + +# Companion to close-draft-prs.yml. That workflow runs per-PR on +# pull_request_target and is the fast path. GitHub does NOT dispatch +# pull_request_target for fork branches whose names look like a Git SHA, so a +# fork draft PR on a SHA-named branch can never be closed by any PR-triggered +# event (a pull_request run from a fork gets a read-only token). This scheduled +# sweep runs in the base-repo context with a write-capable token and enforces +# the identical policy on a timer, catching that evasion. See issue #761. + +on: + schedule: + - cron: '0 */6 * * *' + workflow_dispatch: + +concurrency: + group: close-draft-prs-sweep + cancel-in-progress: false + +permissions: + pull-requests: write + +jobs: + sweep-draft-prs: + name: Sweep open draft PRs + runs-on: ubuntu-latest + steps: + - name: Close non-maintainer draft PRs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + // Maintainers may use draft PRs for internal coordination — same + // carve-out as close-draft-prs.yml. A scheduled run has no + // github.event.pull_request, so the carve-out is applied here as a + // Set membership test over author_association. + const MAINTAINER_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']); + const repoUrl = context.repo.owner + '/' + context.repo.repo; + + const commentBody = [ + '## Draft PRs are not accepted', + '', + 'This project only accepts completed pull requests. Draft PRs are automatically closed.', + '', + '**Why?** GSD requires all PRs to be ready for review when opened \u2014 with tests passing, the correct PR template used, and a linked approved issue. Draft PRs bypass these quality gates and create review overhead.', + '', + '### What to do instead', + '', + '1. Finish your implementation locally', + '2. Run `npm run test:coverage` and confirm all tests pass', + '3. Open a **non-draft** PR using the [correct template](https://github.com/' + repoUrl + '/blob/main/CONTRIBUTING.md#pull-request-guidelines)', + '', + 'See [CONTRIBUTING.md](https://github.com/' + repoUrl + '/blob/main/CONTRIBUTING.md) for the full process.', + ].join('\n'); + + const isEnforceableDraft = (pr) => + !!pr && pr.state === 'open' && pr.draft === true && + !MAINTAINER_ASSOCIATIONS.has(pr.author_association); + + const openPulls = await github.paginate(github.rest.pulls.list, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100, + }); + + const candidates = openPulls.filter(isEnforceableDraft); + core.info('Sweep found ' + candidates.length + ' non-maintainer draft PR(s) of ' + openPulls.length + ' open.'); + + const failures = []; + + for (const candidate of candidates) { + try { + // Re-fetch immediately before mutating: the contributor may have + // marked the PR ready (or it may have closed) since pagination. + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: candidate.number, + }); + + if (!isEnforceableDraft(pr)) { + core.info('Skipping PR #' + candidate.number + ' — no longer an open non-maintainer draft.'); + continue; + } + + // Close FIRST so enforcement (the primary action) is never gated + // on the explanatory comment. A closed PR is never revisited by a + // later sweep, so this also prevents duplicate comments. + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + state: 'closed' + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: commentBody + }); + + core.info('Closed draft PR #' + pr.number + ': ' + pr.title); + } catch (err) { + failures.push(candidate.number); + core.warning('Failed to process draft PR #' + candidate.number + ': ' + err.message); + } + } + + if (failures.length > 0) { + core.setFailed('Sweep encountered errors on ' + failures.length + ' draft PR(s): ' + failures.join(', ')); + } diff --git a/.github/workflows/close-draft-prs.yml b/.github/workflows/close-draft-prs.yml index f2d361e89..6f8eda5cf 100644 --- a/.github/workflows/close-draft-prs.yml +++ b/.github/workflows/close-draft-prs.yml @@ -1,7 +1,18 @@ name: Close Draft PRs +# pull_request_target (not pull_request) so the job runs in the base-repo +# context with a write-capable token even for PRs from forks. Without this, +# fork PRs from first-time/external contributors get a read-only GITHUB_TOKEN +# and the close/comment API calls 403 — letting them bypass the auto-close. +# Safe because this job only reads event metadata and never checks out or +# executes PR-supplied code. +# Residual platform limitation: GitHub deliberately does NOT trigger +# pull_request_target for fork branches whose names look like a Git SHA, so a +# contributor could still evade this by naming their head branch like a commit +# hash. Fully closing that gap needs a scheduled base-context sweep (separate +# concern); a draft PR that evades this still cannot merge and still fails the other gates. on: - pull_request: + pull_request_target: types: [opened, reopened, converted_to_draft] concurrency: diff --git a/.github/workflows/discord-changelog.yml b/.github/workflows/discord-changelog.yml index 30ffbba78..768fb4881 100644 --- a/.github/workflows/discord-changelog.yml +++ b/.github/workflows/discord-changelog.yml @@ -19,131 +19,23 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Post release changelog to Discord - uses: actions/github-script@v7 env: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK }} - RELEASE_TAG: ${{ inputs.release_tag }} - with: - script: | - const webhookUrl = process.env.DISCORD_WEBHOOK_URL; - - if (!webhookUrl) { - core.setFailed("Missing DISCORD_CHANGELOG_WEBHOOK secret."); - return; - } - - if (context.eventName !== "release" && context.eventName !== "workflow_dispatch") { - core.info(`Skipping ${context.eventName}; this workflow only posts releases.`); - return; - } - - const release = await getRelease(); - const messages = buildReleaseMessages(release); - - for (const content of messages) { - const response = await fetch(webhookUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: JSON.stringify({ - username: "Repo Changelog", - content, - allowed_mentions: { - parse: [], - }, - }), - }); - - if (!response.ok) { - core.setFailed(`Discord webhook failed with ${response.status}: ${await response.text()}`); - return; - } - } - - async function getRelease() { - if (context.eventName === "release") { - return context.payload.release; - } - - const releaseTag = process.env.RELEASE_TAG; - const response = releaseTag - ? await github.rest.repos.getReleaseByTag({ - owner: context.repo.owner, - repo: context.repo.repo, - tag: releaseTag, - }) - : await github.rest.repos.getLatestRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - }); - - return response.data; - } - - function buildReleaseMessages(release) { - const tag = release.tag_name || release.name || "release"; - const title = release.name || tag; - const notes = formatReleaseNotes(release.body || "") || "No release notes provided."; - const fullMessage = `**${title} Released**\n\n${notes}`; - - return splitDiscordMessage(fullMessage, 1900); - } - - function formatReleaseNotes(value) { - return truncate( - value - .replace(/\r\n/g, "\n") - .replace(/https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/pull\/(\d+)/g, "#$1") - .replace(/https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/issues\/(\d+)/g, "#$1") - .replace(/https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/commit\/([0-9a-f]{7})[0-9a-f]*/gi, "$1") - .replace(/https:\/\/github\.com\/\S+/g, "") - .trim() - .split("\n") - .map((line) => { - const heading = line.match(/^#{1,6}\s+(.+)$/); - return heading ? `**${heading[1].trim()}**` : line; - }) - .join("\n") - .replace(/\n{3,}/g, "\n\n") - .trim(), - 3900, - ); - } - - function splitDiscordMessage(value, maxLength) { - if (value.length <= maxLength) { - return [value]; - } - - const chunks = []; - let remaining = value; - - while (remaining.length > maxLength) { - let splitAt = remaining.lastIndexOf("\n", maxLength); - - if (splitAt < 500) { - splitAt = remaining.lastIndexOf(" ", maxLength); - } - - if (splitAt < 500) { - splitAt = maxLength; - } - - chunks.push(remaining.slice(0, splitAt).trim()); - remaining = remaining.slice(splitAt).trim(); - } - - if (remaining) { - chunks.push(remaining); - } - - return chunks.map((chunk, index) => - index === 0 ? chunk : `**Changelog continued**\n\n${chunk}`, - ); - } - - function truncate(value, maxLength) { - return value.length <= maxLength ? value : `${value.slice(0, maxLength - 3)}...`; - } + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }} + run: | + set -euo pipefail + if [ -n "${RELEASE_TAG:-}" ]; then + node scripts/release-notes/discord-release-summary.cjs \ + --tag "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --post + else + node scripts/release-notes/discord-release-summary.cjs \ + --latest \ + --repo "$GITHUB_REPOSITORY" \ + --post + fi diff --git a/.github/workflows/docs-required.yml b/.github/workflows/docs-required.yml index a6c8a213a..4961d90d5 100644 --- a/.github/workflows/docs-required.yml +++ b/.github/workflows/docs-required.yml @@ -30,3 +30,18 @@ jobs: env: GITHUB_BASE_REF: ${{ github.base_ref }} run: node scripts/lint-docs-required.cjs + + - name: Detect docs/ changes + id: docs-changed + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + if git diff --name-only "origin/${BASE_REF}...HEAD" | grep -q '^docs/'; then + echo "docs_changed=true" >> "$GITHUB_OUTPUT" + else + echo "docs_changed=false" >> "$GITHUB_OUTPUT" + fi + + - name: Docs parity — live registry check + if: steps.docs-changed.outputs.docs_changed == 'true' + run: node --test tests/docs-parity-live-registry.test.cjs diff --git a/.github/workflows/duplicate-check.yml b/.github/workflows/duplicate-check.yml new file mode 100644 index 000000000..0931c4575 --- /dev/null +++ b/.github/workflows/duplicate-check.yml @@ -0,0 +1,54 @@ +name: Duplicate check + +on: + issues: + types: [opened] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: true + +permissions: + issues: write + contents: read + +jobs: + detect: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const dedupe = require(`${process.env.GITHUB_WORKSPACE}/scripts/issue-dedupe.cjs`); + const issue = context.payload.issue; + if (issue.pull_request) return; + const existing = (issue.labels || []).map((l) => (typeof l === 'string' ? l : l.name)); + if (existing.includes(dedupe.POSSIBLE_DUPLICATE_LABEL)) return; + const open = await github.paginate(github.rest.issues.listForRepo, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100, + }); + const candidates = open + .filter((i) => !i.pull_request && i.number !== issue.number) + .map((i) => ({ number: i.number, title: i.title })); + const matches = dedupe.scoreCandidates(issue.title, candidates, { excludeNumber: issue.number }); + if (!matches.length) { + core.info('No similar open issues found.'); + return; + } + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: dedupe.renderChallengeComment(matches, { windowHours: dedupe.DEFAULT_WINDOW_HOURS }), + }); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [dedupe.POSSIBLE_DUPLICATE_LABEL], + }); + core.info(`Flagged #${issue.number} as possible duplicate of: ${matches.map((m) => '#' + m.number).join(', ')}`); diff --git a/.github/workflows/duplicate-sweep.yml b/.github/workflows/duplicate-sweep.yml new file mode 100644 index 000000000..083c32118 --- /dev/null +++ b/.github/workflows/duplicate-sweep.yml @@ -0,0 +1,91 @@ +name: Duplicate auto-close sweep + +on: + schedule: + - cron: '0 7 * * *' + workflow_dispatch: + +permissions: + issues: write + contents: read + +jobs: + sweep: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const dedupe = require(`${process.env.GITHUB_WORKSPACE}/scripts/issue-dedupe.cjs`); + const { owner, repo } = context.repo; + const issues = await github.paginate(github.rest.issues.listForRepo, { + owner, + repo, + state: 'open', + labels: dedupe.POSSIBLE_DUPLICATE_LABEL, + per_page: 100, + }); + const now = Date.now(); + for (const issue of issues) { + if (issue.pull_request) continue; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: issue.number, + per_page: 100, + }); + const challenges = comments.filter((c) => dedupe.isChallengeComment(c.body)); + const challenge = challenges[challenges.length - 1]; + let challengeComment = null; + let laterUserComments = 0; + if (challenge) { + const challengeAt = new Date(challenge.created_at).getTime(); + laterUserComments = comments.filter( + (c) => new Date(c.created_at).getTime() > challengeAt && c.user && c.user.type !== 'Bot', + ).length; + const reactions = await github.paginate(github.rest.reactions.listForIssueComment, { + owner, + repo, + comment_id: challenge.id, + per_page: 100, + }); + const downvoted = reactions.some((r) => r.content === '-1'); + challengeComment = { createdAt: challenge.created_at, downvoted }; + } + const decision = dedupe.shouldClose({ + now, + labels: issue.labels, + challengeComment, + laterUserComments, + windowHours: dedupe.DEFAULT_WINDOW_HOURS, + }); + core.info(`#${issue.number}: ${decision.reason}`); + if (!decision.close) continue; + const fresh = await github.rest.issues.get({ owner, repo, issue_number: issue.number }); + if (fresh.data.state !== 'open') continue; + const freshLabels = (fresh.data.labels || []).map((l) => (typeof l === 'string' ? l : l.name)); + if (!freshLabels.includes(dedupe.POSSIBLE_DUPLICATE_LABEL)) { + core.info(`#${issue.number}: possible-duplicate cleared since snapshot, skipping close`); + continue; + } + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issue.number, + body: `Closing as a likely duplicate — no response within ${dedupe.DEFAULT_WINDOW_HOURS}h of the duplicate check. If this was a mistake, comment and a maintainer will reopen it.`, + }); + await github.rest.issues.update({ + owner, + repo, + issue_number: issue.number, + state: 'closed', + state_reason: 'duplicate', + }); + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: issue.number, + name: dedupe.POSSIBLE_DUPLICATE_LABEL, + }).catch((e) => core.info(`removeLabel after close: ${e.message}`)); + } diff --git a/.github/workflows/hotfix.yml b/.github/workflows/hotfix.yml deleted file mode 100644 index f40090446..000000000 --- a/.github/workflows/hotfix.yml +++ /dev/null @@ -1,447 +0,0 @@ -name: Hotfix Release - -# Hotfix flow for X.YY.Z patch releases (Z > 0). -# -# create: -# - Branches hotfix/X.YY.Z from the highest existing vX.YY.* tag (1.27.2 from -# v1.27.1, 1.27.1 from v1.27.0). The base IS the cumulative-fix anchor for -# the previous patch. -# - Auto-cherry-picks every fix:/chore: commit on origin/main that isn't -# already in the base, oldest-first. Patch-equivalents (already applied) -# are skipped via `git cherry`. feat:/refactor: are NEVER auto-included. -# - Conflicts fail the workflow with the offending SHA so the operator can -# resolve manually on the branch and re-run finalize with auto_cherry_pick=false. -# - Step summary lists every included SHA so the eventual vX.YY.Z tag -# self-documents what shipped. -# -# finalize: -# - install-smoke gate (cross-platform parity with release.yml) -# - Publishes to @latest, tags vX.YY.Z, re-points @next → vX.YY.Z, opens -# merge-back PR. - -on: - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: true - type: choice - options: - - create - - finalize - version: - description: 'Patch version (e.g., 1.27.1)' - required: true - type: string - auto_cherry_pick: - description: 'Auto-cherry-pick fix:/chore: commits from origin/next (fallback origin/main) since base tag (create only)' - required: false - type: boolean - default: true - dry_run: - description: 'Dry run (skip npm publish, tagging, and push)' - required: false - type: boolean - default: false - -concurrency: - group: hotfix-${{ inputs.version }} - cancel-in-progress: false - -env: - NODE_VERSION: 24 - -jobs: - validate-version: - runs-on: ubuntu-latest - timeout-minutes: 2 - permissions: - contents: read - outputs: - base_tag: ${{ steps.validate.outputs.base_tag }} - branch: ${{ steps.validate.outputs.branch }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Validate version format - id: validate - env: - VERSION: ${{ inputs.version }} - run: | - # Must be X.Y.Z where Z > 0 (patch release) - if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[1-9][0-9]*$'; then - echo "::error::Version must be a patch release (e.g., 1.27.1, not 1.28.0)" - exit 1 - fi - MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1-2) - TARGET_TAG="v${VERSION}" - BRANCH="hotfix/${VERSION}" - # Append TARGET_TAG to the candidate list, then sort -V, then walk the - # sorted list and print whatever immediately precedes TARGET_TAG. This - # is semver-correct for multi-digit patches (v1.27.10 > v1.27.9) where - # a plain `awk '$1 < target'` lexicographic compare would mis-order. - BASE_TAG=$( ( git tag -l "v${MAJOR_MINOR}.*" | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$"; echo "$TARGET_TAG" ) \ - | sort -V \ - | awk -v target="$TARGET_TAG" '$1 == target { print prev; exit } { prev = $1 }') - if [ -z "$BASE_TAG" ]; then - echo "::error::No prior stable tag found for ${MAJOR_MINOR}.x before $TARGET_TAG" - exit 1 - fi - echo "base_tag=$BASE_TAG" >> "$GITHUB_OUTPUT" - echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" - - create: - needs: validate-version - if: inputs.action == 'create' - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Check branch doesn't already exist - env: - BRANCH: ${{ needs.validate-version.outputs.branch }} - run: | - if git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null 2>&1; then - echo "::error::Branch $BRANCH already exists. Delete it first or use finalize." - exit 1 - fi - - - name: Configure git identity - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - - name: Create hotfix branch from base tag and push (skeleton) - env: - BRANCH: ${{ needs.validate-version.outputs.branch }} - BASE_TAG: ${{ needs.validate-version.outputs.base_tag }} - DRY_RUN: ${{ inputs.dry_run }} - run: | - set -euo pipefail - git checkout -b "$BRANCH" "$BASE_TAG" - # Push the skeleton branch up-front so any subsequent cherry-pick - # conflict leaves a remote artefact the operator can fetch, resolve, - # and re-push. Skipped on dry-run — local checkout still exercises - # the same cherry-pick + bump flow so conflicts are caught. - if [ "$DRY_RUN" != "true" ]; then - git push -u origin "$BRANCH" - fi - - - name: Cherry-pick fix/chore commits from origin/next since base tag - if: ${{ inputs.auto_cherry_pick }} - env: - BRANCH: ${{ needs.validate-version.outputs.branch }} - BASE_TAG: ${{ needs.validate-version.outputs.base_tag }} - DRY_RUN: ${{ inputs.dry_run }} - run: | - set -euo pipefail - - # Under the next-branch model, day-to-day fixes land on `next` first - # and only reach `main` via release back-merge. So `next` is the - # canonical cherry-pick source. Fall back to `main` if `next` doesn't - # exist yet (legacy single-branch repos) or as a transition guard. - if git ls-remote --exit-code origin next >/dev/null 2>&1; then - git fetch origin next:refs/remotes/origin/next - SOURCE="origin/next" - else - git fetch origin main:refs/remotes/origin/main - SOURCE="origin/main" - fi - echo "Cherry-pick source: $SOURCE" - - # `git cherry $BASE_TAG $SOURCE` lists every commit on the source not - # patch-equivalent in BASE_TAG. + means needs picking, - means - # already applied (skipped silently). - CANDIDATES=$(git cherry "$BASE_TAG" "$SOURCE" | awk '/^\+ / {print $2}') - - if [ -z "$CANDIDATES" ]; then - echo "No commits on $SOURCE beyond $BASE_TAG." - echo "## Cherry-pick summary" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Base: \`$BASE_TAG\` (source: \`$SOURCE\`) — no commits to consider." >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - - # Re-order chronologically (oldest first) for predictable application. - ORDERED=$(git log --reverse --format='%H' "$BASE_TAG..$SOURCE" \ - | grep -F -f <(echo "$CANDIDATES") || true) - - INCLUDED="" - SKIPPED="" - while IFS= read -r SHA; do - [ -z "$SHA" ] && continue - SUBJECT=$(git log -1 --format='%s' "$SHA") - # fix: or chore:, optional scope, optional ! breaking marker - if echo "$SUBJECT" | grep -qE '^(fix|chore)(\([^)]+\))?!?: '; then - echo "→ cherry-picking $SHA $SUBJECT" - if ! git cherry-pick -x "$SHA"; then - # Abort restores HEAD to the last successful pick. On real - # runs, push that state so the operator can fetch, resolve - # $SHA manually, and finalize with auto_cherry_pick=false. - git cherry-pick --abort || true - if [ "$DRY_RUN" != "true" ]; then - git push --force-with-lease origin "$BRANCH" || git push origin "$BRANCH" || true - fi - { - echo "## Cherry-pick conflict" - echo "" - echo "Failed at: \`${SHA}\` — \`${SUBJECT}\`" - echo "" - if [ "$DRY_RUN" = "true" ]; then - echo "**Dry run:** branch was not pushed, so the picks below were discarded with the runner." - if [ -n "$INCLUDED" ]; then - echo "" - echo "Already-applied picks (lost — must be re-applied before resolving \`${SHA}\`):" - echo "" - echo "$INCLUDED" - fi - echo "" - echo "**To resolve:** re-run \`create\` with \`auto_cherry_pick=true\` (real, not dry-run) to materialize the partial branch on origin, then resolve \`${SHA}\` manually. Re-running with \`auto_cherry_pick=false\` would recreate the branch from \`${BASE_TAG}\` and lose every pick listed above." - else - echo "Branch \`${BRANCH}\` was pushed with picks applied up to (but not including) the conflicting commit." - echo "" - echo "**To resolve:** \`git fetch origin && git checkout ${BRANCH} && git cherry-pick -x ${SHA}\`, fix the conflict, push, then re-run \`finalize\` with \`auto_cherry_pick=false\`." - fi - } >> "$GITHUB_STEP_SUMMARY" - echo "::error::Cherry-pick of $SHA failed. See summary." - exit 1 - fi - INCLUDED="${INCLUDED}- \`${SHA}\` ${SUBJECT}"$'\n' - else - echo " skip $SHA $SUBJECT (not fix/chore)" - SKIPPED="${SKIPPED}- \`${SHA}\` ${SUBJECT}"$'\n' - fi - done <<< "$ORDERED" - - { - echo "## Cherry-pick summary" - echo "" - echo "Base: \`$BASE_TAG\`" - echo "" - if [ -n "$INCLUDED" ]; then - echo "### Included (fix/chore)" - echo "" - echo "$INCLUDED" - else - echo "_No fix/chore commits to include._" - echo "" - fi - if [ -n "$SKIPPED" ]; then - echo "### Skipped (feat/refactor/etc — not auto-included)" - echo "" - echo "$SKIPPED" - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Bump version and push - env: - BRANCH: ${{ needs.validate-version.outputs.branch }} - BASE_TAG: ${{ needs.validate-version.outputs.base_tag }} - VERSION: ${{ inputs.version }} - DRY_RUN: ${{ inputs.dry_run }} - run: | - set -euo pipefail - npm version "$VERSION" --no-git-tag-version - git add package.json package-lock.json - git commit -m "chore: bump version to $VERSION for hotfix" - if [ "$DRY_RUN" != "true" ]; then - git push origin "$BRANCH" - else - echo "DRY RUN — branch not pushed. Local checkout exercised the cherry-pick and bump flow." - fi - { - echo "## Hotfix branch created" - echo "" - echo "- Branch: \`$BRANCH\`" - echo "- Based on: \`$BASE_TAG\`" - echo "- Apply additional manual fixes if needed, then run \`finalize\`." - } >> "$GITHUB_STEP_SUMMARY" - - install-smoke: - needs: validate-version - if: inputs.action == 'finalize' - permissions: - contents: read - uses: ./.github/workflows/install-smoke.yml - with: - ref: ${{ needs.validate-version.outputs.branch }} - - finalize: - needs: [validate-version, install-smoke] - if: inputs.action == 'finalize' - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: write - pull-requests: write - id-token: write - environment: npm-publish - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ needs.validate-version.outputs.branch }} - fetch-depth: 0 - - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: ${{ env.NODE_VERSION }} - registry-url: 'https://registry.npmjs.org' - cache: 'npm' - - - name: Configure git identity - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - - name: Detect prior publish (reconciliation mode) - id: prior_publish - env: - VERSION: ${{ inputs.version }} - run: | - EXISTING=$(npm view @opengsd/gsd-core@"$VERSION" version 2>/dev/null || true) - if [ -n "$EXISTING" ]; then - echo "::warning::@opengsd/gsd-core@${VERSION} is already on the registry — entering reconciliation mode (skip publish, continue with tag/release/PR/dist-tag)." - echo "skip_publish=true" >> "$GITHUB_OUTPUT" - else - echo "skip_publish=false" >> "$GITHUB_OUTPUT" - fi - - - name: Install and test - run: | - npm ci - npm run test:coverage:unit - - - - name: Dry-run publish validation - env: - NODE_AUTH_TOKEN: ${{ secrets.GETSHITDONEREDUXNPMTOKEN }} - run: npm publish --dry-run --tag latest - - - name: Tag and push - if: ${{ !inputs.dry_run }} - env: - VERSION: ${{ inputs.version }} - run: | - if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then - EXISTING_SHA=$(git rev-parse "refs/tags/v${VERSION}") - HEAD_SHA=$(git rev-parse HEAD) - if [ "$EXISTING_SHA" != "$HEAD_SHA" ]; then - echo "::error::Tag v${VERSION} already exists pointing to different commit" - exit 1 - fi - echo "Tag v${VERSION} already exists on current commit; skipping" - else - git tag "v${VERSION}" - git push origin "v${VERSION}" - fi - - - name: Publish to npm (latest) - if: ${{ !inputs.dry_run && steps.prior_publish.outputs.skip_publish != 'true' }} - env: - NODE_AUTH_TOKEN: ${{ secrets.GETSHITDONEREDUXNPMTOKEN }} - run: npm publish --provenance --access public --tag latest - - - name: Re-point next dist-tag at this hotfix - if: ${{ !inputs.dry_run }} - env: - VERSION: ${{ inputs.version }} - NODE_AUTH_TOKEN: ${{ secrets.GETSHITDONEREDUXNPMTOKEN }} - run: | - npm dist-tag add "@opengsd/gsd-core@${VERSION}" next - echo "✅ next dist-tag re-pointed to v${VERSION} (matches latest)" - - - name: Create GitHub Release (idempotent) - if: ${{ !inputs.dry_run }} - env: - GH_TOKEN: ${{ github.token }} - VERSION: ${{ inputs.version }} - run: | - if gh release view "v${VERSION}" >/dev/null 2>&1; then - echo "GitHub Release v${VERSION} already exists; ensuring --latest flag is set" - gh release edit "v${VERSION}" --latest || true - else - gh release create "v${VERSION}" \ - --title "v${VERSION} (hotfix)" \ - --generate-notes \ - --latest - fi - # Reformat the auto-generated notes into the curated - # Install + Feature/Enhancement/Fix format. - node scripts/release-notes/format-github-release-notes.cjs \ - --tag "v${VERSION}" --latest --apply - - - name: Create PR to merge hotfix back to main - if: ${{ !inputs.dry_run }} - env: - GH_TOKEN: ${{ github.token }} - BRANCH: ${{ needs.validate-version.outputs.branch }} - VERSION: ${{ inputs.version }} - run: | - EXISTING_PR=$(gh pr list --base main --head "$BRANCH" --state open --json number --jq '.[0].number') - if [ -n "$EXISTING_PR" ]; then - gh pr edit "$EXISTING_PR" \ - --title "chore: merge hotfix v${VERSION} back to main" \ - --body "Merge hotfix changes back to main after v${VERSION} release." - else - gh pr create \ - --base main \ - --head "$BRANCH" \ - --title "chore: merge hotfix v${VERSION} back to main" \ - --body "Merge hotfix changes back to main after v${VERSION} release." - fi - - - name: Verify publish landed on registry - if: ${{ !inputs.dry_run }} - env: - VERSION: ${{ inputs.version }} - run: | - PUBLISHED="NOT_FOUND" - for delay in 5 10 20 30 45; do - PUBLISHED=$(npm view @opengsd/gsd-core@"$VERSION" version 2>/dev/null || echo "NOT_FOUND") - if [ "$PUBLISHED" = "$VERSION" ]; then - break - fi - echo "Waiting ${delay}s for registry to catch up (saw: $PUBLISHED)..." - sleep "$delay" - done - if [ "$PUBLISHED" != "$VERSION" ]; then - echo "::error::Version $VERSION did not appear on the registry within timeout" - exit 1 - fi - LATEST_VER=$(npm view @opengsd/gsd-core dist-tags.latest 2>/dev/null || echo "NOT_FOUND") - if [ "$LATEST_VER" != "$VERSION" ]; then - echo "::error::dist-tag 'latest' resolves to '$LATEST_VER', expected '$VERSION'" - exit 1 - fi - echo "✓ Verified: @opengsd/gsd-core@$VERSION is live on @latest" - - - name: Summary - env: - VERSION: ${{ inputs.version }} - BASE_TAG: ${{ needs.validate-version.outputs.base_tag }} - DRY_RUN: ${{ inputs.dry_run }} - run: | - { - echo "## Hotfix v${VERSION}" - echo "" - echo "- Base (cumulative-fix anchor): \`${BASE_TAG}\`" - if [ "$DRY_RUN" = "true" ]; then - echo "- **DRY RUN** — npm publish, tagging, and push skipped" - else - echo "- Published to npm as \`latest\`" - echo "- \`next\` dist-tag re-pointed to v${VERSION}" - echo "- Tagged \`v${VERSION}\` (anchor for the next hotfix's cherry-pick base)" - echo "- Merge-back PR opened against main" - fi - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 4368a0ef5..0b62bd48c 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -30,7 +30,6 @@ on: - 'tests/release-tarball-smoke.install.test.cjs' - '.github/workflows/install-smoke.yml' - '.github/workflows/release.yml' - - '.github/workflows/hotfix.yml' push: branches: - main @@ -49,6 +48,9 @@ concurrency: group: install-smoke-${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: + contents: read + jobs: # --------------------------------------------------------------------------- # Job 1: tarball install (existing canonical path) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d04647649..19867304a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,9 +12,14 @@ on: - rc - finalize version: - description: 'Version (e.g., 1.28.0 or 2.0.0)' + description: 'Version: X.Y.0 (minor/major) or X.Y.Z with Z>0 (hotfix/patch)' required: true type: string + auto_cherry_pick: + description: 'Hotfix create only: auto-cherry-pick fix:/chore: commits from origin/next (fallback origin/main) since base tag' + required: false + type: boolean + default: true dry_run: description: 'Dry run (skip npm publish, tagging, and push)' required: false @@ -37,6 +42,8 @@ jobs: outputs: branch: ${{ steps.validate.outputs.branch }} is_major: ${{ steps.validate.outputs.is_major }} + is_hotfix: ${{ steps.validate.outputs.is_hotfix }} + base_tag: ${{ steps.validate.outputs.base_tag }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -46,20 +53,44 @@ jobs: id: validate env: VERSION: ${{ inputs.version }} + ACTION: ${{ inputs.action }} run: | - # Must be X.Y.0 (minor or major release, not patch), no leading zeros in any segment - if ! echo "$VERSION" | grep -qE '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.0$'; then - echo "::error::Version '$VERSION' is invalid. Must be X.Y.0 with no leading zeros (e.g., 1.28.0 or 2.0.0). Use hotfix workflow for patch releases." - exit 1 - fi - BRANCH="release/${VERSION}" - # Detect major (X.0.0) + set -euo pipefail + IS_HOTFIX="false" IS_MAJOR="false" - if echo "$VERSION" | grep -qE '^(0|[1-9][0-9]*)\.0\.0$'; then - IS_MAJOR="true" + BASE_TAG="" + if echo "$VERSION" | grep -qE '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.0$'; then + # Minor or major release (X.Y.0) + BRANCH="release/${VERSION}" + if echo "$VERSION" | grep -qE '^(0|[1-9][0-9]*)\.0\.0$'; then + IS_MAJOR="true" + fi + elif echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[1-9][0-9]*$'; then + # Patch / hotfix release (X.Y.Z, Z>0) + IS_HOTFIX="true" + if [ "$ACTION" = "rc" ]; then + echo "::error::Hotfix (patch) releases skip the rc action — run create, then finalize." + exit 1 + fi + BRANCH="hotfix/${VERSION}" + MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1-2) + TARGET_TAG="v${VERSION}" + # semver-correct base tag: highest vMAJOR_MINOR.* strictly below TARGET_TAG + BASE_TAG=$( ( git tag -l "v${MAJOR_MINOR}.*" | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$"; echo "$TARGET_TAG" ) \ + | sort -V \ + | awk -v target="$TARGET_TAG" '$1 == target { print prev; exit } { prev = $1 }') + if [ -z "$BASE_TAG" ]; then + echo "::error::No prior stable tag found for ${MAJOR_MINOR}.x before $TARGET_TAG" + exit 1 + fi + else + echo "::error::Version '$VERSION' is invalid. Use X.Y.0 (minor/major) or X.Y.Z with Z>0 (hotfix), no leading zeros." + exit 1 fi echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" echo "is_major=$IS_MAJOR" >> "$GITHUB_OUTPUT" + echo "is_hotfix=$IS_HOTFIX" >> "$GITHUB_OUTPUT" + echo "base_tag=$BASE_TAG" >> "$GITHUB_OUTPUT" - name: Reject already-published versions env: @@ -103,6 +134,7 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Create release branch + if: needs.validate-version.outputs.is_hotfix != 'true' env: BRANCH: ${{ needs.validate-version.outputs.branch }} VERSION: ${{ inputs.version }} @@ -124,6 +156,152 @@ jobs: echo "" >> "$GITHUB_STEP_SUMMARY" echo "Next: run this workflow with \`rc\` action to publish a pre-release to \`next\`" >> "$GITHUB_STEP_SUMMARY" + - name: Create hotfix branch from base tag (skeleton) + if: needs.validate-version.outputs.is_hotfix == 'true' + env: + BRANCH: ${{ needs.validate-version.outputs.branch }} + BASE_TAG: ${{ needs.validate-version.outputs.base_tag }} + DRY_RUN: ${{ inputs.dry_run }} + run: | + set -euo pipefail + git checkout -b "$BRANCH" "$BASE_TAG" + # Push the skeleton up-front so a later cherry-pick conflict leaves a + # remote artefact the operator can fetch, resolve, and re-push. + if [ "$DRY_RUN" != "true" ]; then + git push -u origin "$BRANCH" + fi + + - name: Cherry-pick fix/chore commits from origin/next since base tag + if: ${{ needs.validate-version.outputs.is_hotfix == 'true' && inputs.auto_cherry_pick }} + env: + BRANCH: ${{ needs.validate-version.outputs.branch }} + BASE_TAG: ${{ needs.validate-version.outputs.base_tag }} + DRY_RUN: ${{ inputs.dry_run }} + run: | + set -euo pipefail + + # Under the next-branch model, day-to-day fixes land on `next` first + # and only reach `main` via release back-merge. So `next` is the + # canonical cherry-pick source. Fall back to `main` if `next` doesn't + # exist yet (legacy single-branch repos) or as a transition guard. + if git ls-remote --exit-code origin next >/dev/null 2>&1; then + git fetch origin next:refs/remotes/origin/next + SOURCE="origin/next" + else + git fetch origin main:refs/remotes/origin/main + SOURCE="origin/main" + fi + echo "Cherry-pick source: $SOURCE" + + # `git cherry $BASE_TAG $SOURCE` lists every commit on the source not + # patch-equivalent in BASE_TAG. + means needs picking, - means + # already applied (skipped silently). + CANDIDATES=$(git cherry "$BASE_TAG" "$SOURCE" | awk '/^\+ / {print $2}') + + if [ -z "$CANDIDATES" ]; then + echo "No commits on $SOURCE beyond $BASE_TAG." + echo "## Cherry-pick summary" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Base: \`$BASE_TAG\` (source: \`$SOURCE\`) — no commits to consider." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + # Re-order chronologically (oldest first) for predictable application. + ORDERED=$(git log --reverse --format='%H' "$BASE_TAG..$SOURCE" \ + | grep -F -f <(echo "$CANDIDATES") || true) + + INCLUDED="" + SKIPPED="" + while IFS= read -r SHA; do + [ -z "$SHA" ] && continue + SUBJECT=$(git log -1 --format='%s' "$SHA") + # fix: or chore:, optional scope, optional ! breaking marker + if echo "$SUBJECT" | grep -qE '^(fix|chore)(\([^)]+\))?!?: '; then + echo "→ cherry-picking $SHA $SUBJECT" + if ! git cherry-pick -x "$SHA"; then + # Abort restores HEAD to the last successful pick. On real + # runs, push that state so the operator can fetch, resolve + # $SHA manually, and finalize with auto_cherry_pick=false. + git cherry-pick --abort || true + if [ "$DRY_RUN" != "true" ]; then + git push --force-with-lease origin "$BRANCH" || git push origin "$BRANCH" || true + fi + { + echo "## Cherry-pick conflict" + echo "" + echo "Failed at: \`${SHA}\` — \`${SUBJECT}\`" + echo "" + if [ "$DRY_RUN" = "true" ]; then + echo "**Dry run:** branch was not pushed, so the picks below were discarded with the runner." + if [ -n "$INCLUDED" ]; then + echo "" + echo "Already-applied picks (lost — must be re-applied before resolving \`${SHA}\`):" + echo "" + echo "$INCLUDED" + fi + echo "" + echo "**To resolve:** re-run \`create\` with \`auto_cherry_pick=true\` (real, not dry-run) to materialize the partial branch on origin, then resolve \`${SHA}\` manually. Re-running with \`auto_cherry_pick=false\` would recreate the branch from \`${BASE_TAG}\` and lose every pick listed above." + else + echo "Branch \`${BRANCH}\` was pushed with picks applied up to (but not including) the conflicting commit." + echo "" + echo "**To resolve:** \`git fetch origin && git checkout ${BRANCH} && git cherry-pick -x ${SHA}\`, fix the conflict, push, then re-run \`finalize\` with \`auto_cherry_pick=false\`." + fi + } >> "$GITHUB_STEP_SUMMARY" + echo "::error::Cherry-pick of $SHA failed. See summary." + exit 1 + fi + INCLUDED="${INCLUDED}- \`${SHA}\` ${SUBJECT}"$'\n' + else + echo " skip $SHA $SUBJECT (not fix/chore)" + SKIPPED="${SKIPPED}- \`${SHA}\` ${SUBJECT}"$'\n' + fi + done <<< "$ORDERED" + + { + echo "## Cherry-pick summary" + echo "" + echo "Base: \`$BASE_TAG\`" + echo "" + if [ -n "$INCLUDED" ]; then + echo "### Included (fix/chore)" + echo "" + echo "$INCLUDED" + else + echo "_No fix/chore commits to include._" + echo "" + fi + if [ -n "$SKIPPED" ]; then + echo "### Skipped (feat/refactor/etc — not auto-included)" + echo "" + echo "$SKIPPED" + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Bump hotfix version and push + if: needs.validate-version.outputs.is_hotfix == 'true' + env: + BRANCH: ${{ needs.validate-version.outputs.branch }} + BASE_TAG: ${{ needs.validate-version.outputs.base_tag }} + VERSION: ${{ inputs.version }} + DRY_RUN: ${{ inputs.dry_run }} + run: | + set -euo pipefail + npm version "$VERSION" --no-git-tag-version + git add package.json package-lock.json + git commit -m "chore: bump version to $VERSION for hotfix" + if [ "$DRY_RUN" != "true" ]; then + git push origin "$BRANCH" + else + echo "DRY RUN — branch not pushed." + fi + { + echo "## Hotfix branch created" + echo "" + echo "- Branch: \`$BRANCH\`" + echo "- Based on: \`$BASE_TAG\`" + echo "- Apply additional manual fixes if needed, then run \`finalize\`." + } >> "$GITHUB_STEP_SUMMARY" + install-smoke-rc: needs: validate-version if: inputs.action == 'rc' @@ -192,6 +370,29 @@ jobs: node scripts/check-npm-integrity.cjs npm run test:coverage:unit + - name: Preview CHANGELOG (non-destructive) + env: + VERSION: ${{ inputs.version }} + run: | + # Non-destructive CHANGELOG preview for the version under test (#759): + # renders the curated section finalize will promote, without writing + # CHANGELOG.md or consuming .changeset fragments. Surfaced in the job + # summary so RC testers see the upcoming release notes. + # + # Render to a file as a standalone command so a non-zero exit (e.g. a + # malformed fragment) fails the step. The default GitHub Linux shell + # is `bash -e` without pipefail, so a `node | tee` pipeline would mask + # a node failure behind tee's exit 0. + PREVIEW_FILE="${RUNNER_TEMP:-/tmp}/changelog-preview.md" + node scripts/changeset/cli.cjs render \ + --version "$VERSION" --date "$(date -u +%F)" --preview > "$PREVIEW_FILE" + { + echo "### CHANGELOG preview for v${VERSION} (not yet promoted)" + echo '' + cat "$PREVIEW_FILE" + } >> "${GITHUB_STEP_SUMMARY:-/dev/null}" + cat "$PREVIEW_FILE" + - name: Commit pre-release version bump env: PRE_VERSION: ${{ steps.prerelease.outputs.pre_version }} @@ -243,6 +444,19 @@ jobs: node scripts/release-notes/format-github-release-notes.cjs \ --tag "v${PRE_VERSION}" --prerelease --apply + - name: Post Discord pre-release announcement + if: ${{ !inputs.dry_run }} + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK }} + GH_TOKEN: ${{ github.token }} + PRE_VERSION: ${{ steps.prerelease.outputs.pre_version }} + run: | + node scripts/release-notes/discord-release-summary.cjs \ + --tag "v${PRE_VERSION}" \ + --repo "$GITHUB_REPOSITORY" \ + --post \ + --allow-missing-webhook + - name: Verify publish if: ${{ !inputs.dry_run }} env: @@ -317,6 +531,30 @@ jobs: node scripts/check-npm-integrity.cjs npm run test:coverage:unit + - name: Promote CHANGELOG (render fragments) + env: + VERSION: ${{ inputs.version }} + run: | + node scripts/changeset/cli.cjs render \ + --version "$VERSION" --date "$(date -u +%F)" --allow-empty + git add -A .changeset CHANGELOG.md + # Diff-preview guard: surface exactly what was promoted, in the log and + # the job summary, before the commit lands. + { + echo "### CHANGELOG promotion for v${VERSION}" + echo '```diff' + git diff --cached -- CHANGELOG.md + echo '```' + } >> "${GITHUB_STEP_SUMMARY:-/dev/null}" + git --no-pager diff --cached -- CHANGELOG.md + git diff --cached --quiet || git commit -m "chore: promote CHANGELOG for v${VERSION}" + + - name: Verify CHANGELOG promoted + env: + VERSION: ${{ inputs.version }} + run: | + node scripts/changeset/cli.cjs verify --version "$VERSION" --changelog CHANGELOG.md + # npm bundled with Node 24 (pinned via setup-node) already supports trusted publishing (#318) - name: Dry-run publish validation @@ -326,7 +564,7 @@ jobs: if: ${{ !inputs.dry_run }} continue-on-error: true env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.GSD_BOT_PR_TOKEN || secrets.GITHUB_TOKEN }} BRANCH: ${{ needs.validate-version.outputs.branch }} VERSION: ${{ inputs.version }} run: | @@ -391,6 +629,19 @@ jobs: node scripts/release-notes/format-github-release-notes.cjs \ --tag "v${VERSION}" --latest --apply + - name: Post Discord release announcement + if: ${{ !inputs.dry_run }} + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK }} + GH_TOKEN: ${{ github.token }} + VERSION: ${{ inputs.version }} + run: | + node scripts/release-notes/discord-release-summary.cjs \ + --tag "v${VERSION}" \ + --repo "$GITHUB_REPOSITORY" \ + --post \ + --allow-missing-webhook + - name: Clean up next dist-tag if: ${{ !inputs.dry_run }} env: diff --git a/.github/workflows/remove-duplicate-label.yml b/.github/workflows/remove-duplicate-label.yml new file mode 100644 index 000000000..af71abd9e --- /dev/null +++ b/.github/workflows/remove-duplicate-label.yml @@ -0,0 +1,39 @@ +name: Clear possible-duplicate on response + +on: + issue_comment: + types: [created] + +permissions: + issues: write + contents: read + +jobs: + clear: + if: ${{ !github.event.issue.pull_request }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const dedupe = require(`${process.env.GITHUB_WORKSPACE}/scripts/issue-dedupe.cjs`); + const { owner, repo } = context.repo; + const comment = context.payload.comment; + const issue = context.payload.issue; + if (comment.user && comment.user.type === 'Bot') return; + const existing = (issue.labels || []).map((l) => (typeof l === 'string' ? l : l.name)); + if (!existing.includes(dedupe.POSSIBLE_DUPLICATE_LABEL)) return; + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: issue.number, + name: dedupe.POSSIBLE_DUPLICATE_LABEL, + }).catch((e) => core.info(`removeLabel: ${e.message}`)); + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: issue.number, + labels: [dedupe.HUMAN_REVIEW_LABEL], + }); + core.info(`Cleared possible-duplicate on #${issue.number}; routed to ${dedupe.HUMAN_REVIEW_LABEL}.`); diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index b687079c5..d25c3c9e4 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -22,6 +22,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: + contents: read + jobs: security: runs-on: ubuntu-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68c46eb4e..d186c6d65 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,7 @@ jobs: outputs: code_changed: ${{ steps.scope.outputs.code_changed }} full_matrix: ${{ steps.scope.outputs.full_matrix }} + product_changed: ${{ steps.scope.outputs.product_changed }} targeted_tests: ${{ steps.scope.outputs.targeted_tests }} windows_tests: ${{ steps.scope.outputs.windows_tests }} steps: @@ -49,6 +50,7 @@ jobs: if [ "$EVENT_NAME" != "pull_request" ]; then { echo "code_changed=true" + echo "product_changed=true" echo "full_matrix=true" echo "targeted_tests=" echo "windows_tests=" @@ -117,7 +119,7 @@ jobs: test: name: test (${{ matrix.os }}, ${{ matrix.node-version }}) needs: changes - if: needs.changes.outputs.code_changed == 'true' + if: needs.changes.outputs.product_changed == 'true' runs-on: ${{ matrix.os }} timeout-minutes: 15 env: @@ -214,6 +216,47 @@ jobs: if: matrix.scope == 'full' && needs.changes.outputs.full_matrix == 'true' run: npm run test:slow + test-inert: + name: test (inert CI) + needs: changes + if: needs.changes.outputs.code_changed == 'true' && needs.changes.outputs.product_changed != 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + GSD_PLUGIN_ROOT: .ci-gsd-plugin-root-disabled + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: true + token: ${{ github.token }} + - name: Guard — require GitHub-hosted runner + run: node scripts/ci-guard-runner.cjs + - name: Rebase check — merge PR base branch into PR head + if: github.event_name == 'pull_request' + env: + GITHUB_TOKEN: ${{ github.token }} + run: node scripts/ci-rebase-check.cjs + - name: Set up Node.js 22 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 22 + cache: 'npm' + - name: Environment check + run: npm run check:env + - name: Install dependencies + run: npm ci + - name: Dependency integrity gate + run: node scripts/check-npm-integrity.cjs + - name: Prepare scoped test list + env: + TEST_SCOPE: targeted + TARGETED_TESTS: ${{ needs.changes.outputs.targeted_tests }} + WINDOWS_TESTS: ${{ needs.changes.outputs.windows_tests }} + run: node scripts/ci-prepare-test-scope.cjs + - name: Run scoped tests + run: node scripts/run-tests.cjs --files-from .ci-selected-tests.txt + test-full: name: full test (${{ matrix.os }}, ${{ matrix.node-version }}) needs: changes @@ -289,7 +332,7 @@ jobs: coverage: needs: changes - if: needs.changes.outputs.code_changed == 'true' + if: needs.changes.outputs.product_changed == 'true' runs-on: ubuntu-latest timeout-minutes: 15 env: @@ -336,6 +379,7 @@ jobs: - changes - lint-tests - test + - test-inert - test-full - coverage if: always() @@ -345,17 +389,21 @@ jobs: - name: Summarize required test gate env: CODE_CHANGED: ${{ needs.changes.outputs.code_changed }} + PRODUCT_CHANGED: ${{ needs.changes.outputs.product_changed }} CHANGES_RESULT: ${{ needs.changes.result }} LINT_RESULT: ${{ needs.lint-tests.result }} TEST_RESULT: ${{ needs.test.result }} + INERT_RESULT: ${{ needs.test-inert.result }} FULL_TEST_RESULT: ${{ needs.test-full.result }} COVERAGE_RESULT: ${{ needs.coverage.result }} run: | set -euo pipefail echo "code_changed=$CODE_CHANGED" + echo "product_changed=$PRODUCT_CHANGED" echo "changes=$CHANGES_RESULT" echo "lint-tests=$LINT_RESULT" echo "test=$TEST_RESULT" + echo "test-inert=$INERT_RESULT" echo "test-full=$FULL_TEST_RESULT" echo "coverage=$COVERAGE_RESULT" @@ -374,19 +422,24 @@ jobs: exit 0 fi - if [ "$TEST_RESULT" != "success" ]; then - echo "::error::test matrix did not pass" - exit 1 - fi - - if [ "$FULL_TEST_RESULT" != "success" ] && [ "$FULL_TEST_RESULT" != "skipped" ]; then - echo "::error::full parity matrix did not pass" - exit 1 - fi - - if [ "$COVERAGE_RESULT" != "success" ]; then - echo "::error::coverage did not pass" - exit 1 + if [ "$PRODUCT_CHANGED" = "true" ]; then + if [ "$TEST_RESULT" != "success" ]; then + echo "::error::test matrix did not pass" + exit 1 + fi + if [ "$FULL_TEST_RESULT" != "success" ] && [ "$FULL_TEST_RESULT" != "skipped" ]; then + echo "::error::full parity matrix did not pass" + exit 1 + fi + if [ "$COVERAGE_RESULT" != "success" ]; then + echo "::error::coverage did not pass" + exit 1 + fi + else + if [ "$INERT_RESULT" != "success" ]; then + echo "::error::inert CI lane did not pass" + exit 1 + fi fi echo "Required test gate passed." diff --git a/.gitignore b/.gitignore index 2f8e75bce..bbc12e1b1 100644 --- a/.gitignore +++ b/.gitignore @@ -66,8 +66,12 @@ build/ # ADR-457 build-at-publish: TS-generated runtime artifacts (compiled from src/*.cts # by `npm run build:lib`). Source of truth is src/; these are emitted, never edited. # Published via prepublishOnly; built before test via pretest. Grows as modules migrate. +/gsd-core/bin/lib/research-store.cjs +/gsd-core/bin/lib/research-provider.cjs +/gsd-core/bin/lib/package-legitimacy.cjs /gsd-core/bin/lib/semver-compare.cjs /gsd-core/bin/lib/config-types.cjs +/gsd-core/bin/lib/cli-exit.cjs /gsd-core/bin/lib/code-review-flags.cjs /gsd-core/bin/lib/context-utilization.cjs /gsd-core/bin/lib/artifacts.cjs @@ -102,6 +106,8 @@ build/ /gsd-core/bin/lib/state-document.cjs /gsd-core/bin/lib/shell-command-projection.cjs /gsd-core/bin/lib/security.cjs +/gsd-core/bin/lib/verification.cjs +/gsd-core/bin/lib/verification-command-router.cjs /gsd-core/bin/lib/command-aliases.cjs /gsd-core/bin/lib/config-schema.cjs /gsd-core/bin/lib/model-profiles.cjs @@ -114,9 +120,11 @@ build/ /gsd-core/bin/lib/install-profiles.cjs /gsd-core/bin/lib/intel.cjs /gsd-core/bin/lib/installer-migrations.cjs +/gsd-core/bin/lib/worktree-base-ref.cjs /gsd-core/bin/lib/worktree-safety.cjs /gsd-core/bin/lib/planning-workspace.cjs /gsd-core/bin/lib/runtime-artifact-layout.cjs +/gsd-core/bin/lib/runtime-config-adapter-registry.cjs /gsd-core/bin/lib/command-routing-hub.cjs /gsd-core/bin/lib/core.cjs /gsd-core/bin/lib/drift.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 913a58ac8..5ce462e33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,76 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [1.4.0] - 2026-06-08 + +### Added + +- **Research is now cached, curated-first, and code-governed** — a content-addressed Research Store (per-source TTL), a single provider waterfall with confidence tiers, and registry-API package legitimacy replace the per-agent prose waterfall and the slopcheck bolt-on. (#664) Confidence is now verification-evidence-driven: provider identity alone no longer yields HIGH; HIGH requires ground-truth corroboration (e.g. `legitimacyVerdict: 'OK'`), authority alone caps at MEDIUM, and SLOP caps at LOW. (#664) +- `/gsd:plan-phase` now accepts a `--granularity ` flag to override the configured planning granularity for a single invocation. The flag takes precedence over `granularities.planning`, top-level `granularity`, and `planning.granularity` config. Invalid values are rejected. (#703) (#750) +- **gsd-core can now be installed as a native Claude Code plugin** — a new `.claude-plugin/plugin.json` manifest enables installing gsd-core via `claude plugin install` or the zero-friction `~/.claude/skills/` auto-load path (`gsd-core@skills-dir`), with slash commands auto-namespaced as `/gsd-core:` (e.g. `/gsd-core:plan-phase`) and lifecycle management via `claude plugin enable|disable|update`. gsd-core's always-on guard and update hooks are wired for the plugin path through `hooks/hooks.json` using `${CLAUDE_PLUGIN_ROOT}`. This is additive — the existing npm / file-copy installer is unchanged. (#797) +- **Installer pre-populates `permissions.allow`/`deny` for Claude Code** — fresh Claude Code installs now receive GSD's known-safe tool-call patterns (`Bash(npx gsd-core *)`, `Read(.planning/*)`, `Write(.planning/*)`, `Read(STATE.md)`, `Write(STATE.md)`) in `settings.json` out of the box, eliminating first-run approval prompts. A `deny` block for credential files (`Read(.env)`, `Read(.env.*)`, `Read(.secrets)`) is also added for defense-in-depth. The merge is additive and idempotent; existing user-set entries are preserved. Uninstall removes only GSD-owned entries. (#768) (#819) +- +Added: register newly-available Claude Code lifecycle hooks — SubagentStop, Stop, PreCompact (all wired to gsd-context-monitor for context-headroom warnings), and FileChanged (matcher: `config.json`, wired to new gsd-config-reload.js hook that hot-reloads `.planning/config.json` context mid-session). Also updates hooks/hooks.json (plugin manifest) and managed-hooks-registry for drift-guard coverage (#770). (#821) +- Gemini installs now register three additional hook events — `BeforeAgent`, `AfterAgent`, and `BeforeModel` — wired to `gsd-context-monitor.js` for per-turn context headroom tracking. Previously only `SessionStart`, `BeforeTool`, and `AfterTool` were registered. The installer also detects `hooksConfig.enabled: false` in the user's Gemini `settings.json` and emits a clear warning, surfacing the silent failure mode where all hooks are registered but never execute. (#776) (#829) +- Cross-runtime command enrichment in the installer. Gemini CLI commands now use native `{{args}}` interpolation (translated from Claude's `$ARGUMENTS`) so typed arguments interpolate into the prompt body, and `/gsd:progress` injects live project state via a fixed, injection-safe `!{cat .planning/STATE.md 2>/dev/null}` shell block. Qwen Code skills now carry a numeric `priority` field so the most-used main-loop workflows (`new-project`, `plan-phase`, `execute-phase`, …) surface first in the `/skills` list. The OpenCode per-command `model`/`agent`/`subtask` enrichment was evaluated and intentionally not implemented — `model` would reintroduce the ProviderModelNotFoundError regression that the converter deliberately guards against for non-Anthropic providers (#1156), `subtask`/`agent` change execution semantics for GSD's interactive commands, and `variant` is not in the OpenCode command schema. (#778) (#825) +- Emit native on-demand skills (`skills//SKILL.md`) for the OpenCode-family runtimes (OpenCode and Kilo) at install time, in addition to the existing flat `command/` and file-based `agents/` surfaces. OpenCode and Kilo share a config schema and both discover skills from `skills//SKILL.md`; the installer now stages each GSD command as a skill with minimal, spec-compliant frontmatter (`name` matching the directory, `description` 1–1024 chars) via a shared OpenCode-family skill writer. Skills respect the active install profile (core/minimal stage only their subset) and are removed on uninstall. (#784) (#810) +- `gsd install --cursor` now writes `.cursor/commands/gsd-.md` in addition to the existing `.cursor/skills/` surface. Cursor 1.6 introduced plain-markdown slash commands (no frontmatter) in `.cursor/commands/`; they appear in the `/` menu in the Agent input. Each command file is generated from the same source as the skill but with frontmatter stripped and Cursor-specific content transforms applied (`convertClaudeCommandToCursorCommand`). The skills surface is unchanged — both surfaces are written on every install. (#803) +- The GitHub Copilot installer now reaches lifecycle-hook and instruction parity with other first-class runtimes. It emits a self-contained `sessionStart` hook config (`.github/hooks/gsd-session.json` for local installs, `~/.copilot/hooks/gsd-session.json` for global) and writes `AGENTS.md` at the repository root (which Copilot CLI reads as primary instructions) alongside `copilot-instructions.md`. The hook is an inline `command` hook with no separate script file, so it cannot dangle. Both artifacts are removed — with user-authored content preserved — on `--uninstall`. (#786) (#804) +- Elevate the Cline runtime to hook parity. The installer now emits the Cline `.clinerules/` directory form (`.clinerules/gsd.md`) instead of a single `.clinerules` file, adds a `.clinerules/hooks/PreToolUse` lifecycle hook (Cline v3.36+ JSON stdin → `{cancel,errorMessage,contextModification}` protocol; guards `.planning/` artifacts and fails open), and merges GSD instructions into the cross-tool global `~/.agents/AGENTS.md` target on global installs. A legacy single-file `.clinerules` is migrated to the directory form in place, and `--uninstall` removes the new artifacts and strips the GSD block from `~/.agents/AGENTS.md`. (#787) (#803) +- Qwen Code installs now register three additional hook events that Qwen Code supports beyond Claude Code: `SubagentStop`, `Stop`, and `PreCompact` — all wired to `gsd-context-monitor.js` for context headroom tracking at subagent completion, model stop, and pre-compaction. These events are Qwen-only; Claude Code installs are unchanged. `UserPromptSubmit` is deferred: `gsd-prompt-guard` exits unless `tool_name` is `Write|Edit`, making it a no-op for that payload shape. (#788) (#807) +- **CodeBuddy (Tencent) installs now emit `/gsd-*` slash commands.** A `--codebuddy` install writes `commands/gsd-.md` files to `~/.codebuddy/commands/` so GSD workflows are invokable from CodeBuddy's `/` menu (`/gsd-phase`, `/gsd-ship`, etc.), matching the integration depth of other fully-elevated runtimes (#789). The existing `skills/gsd-/SKILL.md` files are now emitted with `user-invocable: false` so they stay out of the `/` menu — the commands surface is the single `/` entry point (no duplicate entries) and skills remain available for model invocation. Subagents (`~/.codebuddy/agents/`) were already emitted and are unchanged. Uninstall removes the `gsd-*` command files while preserving user-owned commands. No `mcp.json` is written — gsd ships no MCP server and CodeBuddy's `mcp.json` only registers external MCP servers. + + (#830) +- **Augment (Auggie) installs now emit slash command definitions alongside skills.** A global `--augment` install writes `commands/gsd-.md` files to `~/.augment/commands/` in addition to the existing `skills/gsd-/SKILL.md` files, matching the integration depth of other fully-elevated runtimes and allowing Auggie users to invoke GSD as slash commands (`/gsd-phase`, `/gsd-ship`, etc.) without manual configuration (#790). Content rewrites (path normalisation and Augment-specific branding) are applied at install time. Uninstall removes the `gsd-*` command files while preserving user-owned commands. `mcpServers` registration is explicitly excluded — gsd ships no MCP server and does not register third-party servers. (#801) +- Issues are now checked for duplicates when opened: a no-LLM title-similarity check posts a challenge comment and applies a `possible-duplicate` label when a new issue closely matches existing open ones. Flagged issues that go unanswered for 24h are auto-closed as duplicates (reply, or react 👎 to the bot comment, to keep one open); a reply clears the label and routes to `needs-maintainer-review`. (#836) (#843) +- Cursor now receives GSD lifecycle hooks via `.cursor/hooks.json` — a sessionStart hook injects the current workflow state as context at session start, and a postToolUse hook nudges the agent to update `.planning/` after write-class operations, bringing Cursor to baseline hook parity with Gemini and Claude Code. (#777) +- **Gemini CLI extension package** — gsd-core now ships a `gemini-extension.json` manifest (plus a `GEMINI.md` context payload) at the repository root, so Gemini CLI users can install, update, and remove GSD through Gemini's own extension lifecycle: `gemini extensions install https://github.com/open-gsd/gsd-core`, `gemini extensions update gsd-core`, `gemini extensions uninstall gsd-core`, and `gemini extensions link ` for local dev. The extension is discoverable in `gemini extensions list` and loads GSD's operating context into every session. Additive — the existing `npx gsd-core --gemini` installer (which provides the `/gsd:*` slash commands) is unchanged. (#775) (#775) +- **New `agent_skills_security.trusted_global_roots` config** — opt-in allowlist of trusted root directories so symlinked `global:` agent skills whose real path resolves outside the default skills dir (e.g. `~/.claude/skills`) are accepted; default `[]` is byte-identical and preserves the symlink-escape guard. (#754) +- Added `/gsd-update --next` (alias `--rc`) to install or refresh from the `@next` RC dist-tag (ADR #660). A new `parse_update_channel` workflow step resolves the channel from `$ARGUMENTS`; the version check and all three npx install invocations thread `$TAG` instead of hardcoding `@latest`. When `--next` is used the version-comparison output gains a `Channel: next (RC)` banner so the user knows they are leaving the stable line; omitting the flag keeps `@latest` behavior byte-for-byte unchanged. `check-latest-version.cjs` gains `ALLOWED_TAGS`, `buildViewArgs`, and `resolveTag` exports, with an allowlist guard (enforced at both the CLI and function boundary) that rejects any dist-tag other than `latest`/`next`. (#815) (#839) + +### Changed + +- `/gsd:plan-phase --research-phase ` now auto-uses an existing `RESEARCH.md` instead of prompting update/view/skip. When research already exists and neither `--research` nor `--view` is passed, it emits a one-line notice and exits cleanly, matching the promptless behavior of standard `/gsd:plan-phase `. Pass `--research` to force-refresh or `--view` to print the existing research. (#159) (#718) +- Retire the installer's one-off runtime directory helpers (`getGlobalDir`/`getOpencodeGlobalDir`/`getKiloGlobalDir`) and consolidate per-runtime global config-dir resolution onto the single canonical projection `runtime-homes:getGlobalConfigDir`, extended with the `--config-dir` override and the opencode/kilo `*_CONFIG` file-path precedence. Behavior-preserving across all 15 install runtimes. (#56) (#802) +- Make per-runtime config-mutation dispatch in the installer explicit: a new runtime config adapter registry maps each supported runtime to a typed config intent (install surface, shared-settings gate, finish-phase permission writer), and `install()`/`finishInstall()` dispatch by resolved intent instead of inline `runtime === '...'` branching. Behavior-preserving; unknown runtimes now fail loudly. (#60) (#795) +- **Verification status routing is now owned by a single queryable seam** — `ship.md` and `execute-phase.md` both consume `gsd_run query verification.status` instead of re-deriving the `passed`/`gaps_found`/`human_needed` routing independently; the query returns `next_action` and `next_command` so per-status prose no longer needs to be kept in sync across files. This also fixes the broad-grep status misread in `execute-phase.md` where a body `status:` line (in a code block or copied artifact) could concatenate with the frontmatter value and misroute a valid passed phase; a parity test fails if a new verifier status value lacks a route. (#651) (#755) +- Agent `color:` frontmatter now uses Claude Code's documented named colors (`red`/`blue`/`green`/`yellow`/`purple`/`orange`/`pink`/`cyan`) instead of hex values or the undocumented `magenta`, so the intended per-agent TUI color differentiation renders reliably across the Claude Code runtime. Display-only metadata; no behavior change. (#771) (#823) +- Codex installs now register three additional stable hook events (`SubagentStart`, `Stop`, `PostToolUse`) wired to `gsd-context-monitor.js`, matching the full event coverage available since Codex CLI stabilised these hooks. The `SessionStart` hook entry gains a `commandWindows` field on Windows installs so the `.cmd` shim is used for native execution (Git Bash/MSYS cannot POSIX-exec `node.exe` directly). Both new-event registration and uninstall paths handle the flat `{ "EventName": [...] }` and nested `{ "hooks": { "EventName": [...] } }` hooks.json shapes. `gsd-context-monitor.js` and its Windows `.cmd` sibling are added to the managed-hook allowlist so idempotent re-runs de-duplicate entries correctly. (#772) (#827) +- Codex CLI installs now emit two enrichments per agent and skill. **Agent TOML enrichment:** light-tier agents (haiku-equivalent, `routingTier: "light"` in model-catalog.json) get `service_tier = "flex"` and `model_verbosity = "low"` appended to their agent TOML, telling the Codex scheduler to use the flex tier (lower cost, background processing) and suppress verbose token output. **Skill TUI chip:** each installed `gsd-*` skill directory now receives an `agents/openai.yaml` file with `interface.display_name` and `interface.short_description`, making the skill appear in the Codex `/skills` picker with a human-readable name and description drawn from the skill's existing short-description frontmatter. Both enrichments are additive and backward-compatible with Codex CLI ≥ 0.130.0. (#774) (#828) +- **Cline global installs now emit skills, not just rules:** gsd writes skills to `~/.cline/skills//SKILL.md` for Cline ≥ v3.48.0 (see [Cline skills docs](https://docs.cline.bot/customization/skills)), in addition to the existing `.clinerules` file. Each `SKILL.md` carries `name`/`description` frontmatter (agentskills.io) with paths rewritten to the `.cline/` convention. Local installs remain `.clinerules`-only. The `.clinerules` rules file continues to be emitted for compatibility, and upgrading over an existing rules-only install emits the new skills on the next run. (#809) +- **Workflow size budget now measures bytes, not lines (#717).** `tests/workflow-size-budget.test.cjs` re-bases its tier ceilings (XL/LARGE/DEFAULT) from line counts to byte counts — deterministic, no tokenizer, and matching the unit vendors bound on (Codex's 32,768-byte project_doc_max_bytes cap). The #597 tighten-only ratchet and per-file semantics are unchanged; the budget's caching-independent quality rationale (context rot / attention budget) is now documented. (#719) +- The `gsd-verifier` agent no longer re-runs the full workspace test suite once per must-have during Step 7b spot-checks — it enumerates tests to prove existence and runs a single named test to prove a pass, invoking the full suite at most once per verification. (#753) +- **`/gsd-plan-phase`, `/gsd-execute-phase`, `/gsd-autonomous` now run in an isolated forked context on Claude Code** — `context: fork` in skill frontmatter protects the main session's context budget. These three heavy skills also declare `effort: xhigh`; quick-status skills `/gsd-progress` and `/gsd-stats` declare `effort: low`. The installer preserves both fields when converting commands to Claude SKILL.md files. Runtimes that do not recognise these fields silently ignore them — no behaviour change on non-Claude runtimes. (#769) +- **`/gsd:plan-phase` and `/gsd:execute-phase` no longer eagerly load MVP-only guidance on non-MVP runs** — the MVP planner rules, user-story template, Walking-Skeleton template, and MVP+TDD halt-report reference are now Read lazily by the planner/executor only when MVP / Walking-Skeleton / MVP+TDD mode is active, in both the workflow files and the `gsd-planner`/`gsd-executor` agent definitions, instead of being `@`-imported into every run. Behaviour is unchanged; non-MVP planning/execution simply carries less context. (#720) (#746) +- +Automated `codex exec` invocations in the review workflow now include `--ephemeral` (no session-state accumulation across automated/CI runs) and `--dangerously-bypass-hook-trust` (skip hook-trust prompts for hooks managed by gsd-core itself). These flags apply only to the non-interactive reviewer invocations in `gsd-core/workflows/review.md`. (#773) (#824) +- **Codex slash-command conversion no longer corrupts inline-wrapped `/gsd-…` file paths** — the install-time converter now identifies a real `/gsd-` mention by positive boundaries (opening delimiter + no path continuation) instead of an unbounded preceding-character denylist, closing the path-corruption class (#637 → #704) by construction while still converting legitimate backtick-wrapped mentions. (#747) +- The release pipeline now automatically runs `changeset render` during the finalize job, promoting `.changeset/` fragments into a dated `CHANGELOG.md` section before publishing — previously a manual step that was routinely skipped (leaving v1.3.0 and v1.3.1 unpromoted, #690). A new `--allow-empty` flag prevents the verify gate from hard-failing on no-change releases by emitting a dated heading with a `_No notable changes._` placeholder when there are zero fragments. (#715) + +### Fixed + +- `/gsd-review --cursor` now actually invokes the Cursor agent. Detection probes the `cursor-agent` headless binary instead of the `cursor` IDE launcher, the invocation calls the single `cursor-agent` binary in print mode (not the two-token `cursor agent`, which the IDE treats as a file path), and the review prompt is passed as a file-path argument rather than piped to stdin (which `cursor-agent -p` ignores). On failure the captured stderr is surfaced instead of a silent empty result. (#686) +- **No more "gsd-core" console-window flash on Windows.** Every gsd-core child process now passes `windowsHide: true`: the context monitor's `record-session` spawn, the `execGit` / `execNpm` / `execTool` helpers in `shell-command-projection`, the `gsd-worktree-path-guard` and `gsd-workflow-guard` hook git probes, `check-command-router`'s `git log` call, and the `roadmap-upgrade` git status/rev-parse/reset/clean calls — matching the existing `gsd-check-update` spawn. `execNpm` (which uses `shell: true` → `cmd.exe` and runs on every SessionStart, i.e. every `/clear`) and the worktree-path guard (which runs on every Edit/Write in a worktree) were the most visible offenders. No behavior change on macOS/Linux, where the flag is ignored. (#688) +- **`/gsd-review --agy` no longer hangs the whole review on large prompts.** On a big, file-path-rich prompt Antigravity's `agy -p` agentic Cascade can loop on its `code_search`/grep steps and never converge. The invocation now passes agy's own `--print-timeout` flag (its native print-mode cap) so a stalled run self-terminates through the tool's own mechanism; on a non-zero exit any partial output is discarded so the existing transcript fallback / "review failed" stub take over. (#689) +- The roadmap parser now resolves fresh phases of the current milestone in multi-milestone roadmaps. `extractCurrentMilestone()` scoped the current-milestone window to its `## Phases` checklist subsection and stopped at the milestone's own `## Milestone … (Phase Details)` heading, so the `### Phase N:` detail headers fell out of scope. Any command backed by the parser — `init.phase-op` (and therefore `/gsd:discuss-phase` and `/gsd:plan-phase`), `state`, `roadmap list`, and `validate health` (W006) — could not resolve phases of any milestone after the first until a `.planning/phases/` directory already existed, blocking discuss/plan. The parser now also includes the current milestone's `(Phase Details)` section in scope, anchored to the selected milestone's version token so sibling sub-milestones do not cross-pollinate. (#730) (#748) +- **`getGlobalSkillsBase('kilo')` now resolves to `~/.kilo/skills`** — where Kilo Code actually discovers global skills — instead of `~/.config/kilo/skills`. Per [Kilo Code docs](https://kilo.ai/docs/customize/skills), global skills live in the `.kilo` directory within HOME (`~/.kilo/skills/`), independent of the XDG-based config dir at `~/.config/kilo`. The kilo.jsonc config dir (`~/.config/kilo`) and the `command/` path used by the installer are correct and unchanged. Blast radius: this corrects the resolved skills-base path used by doctor/status checks and agent-skills-block resolution (`init.cjs`); the installer writes commands (not skills) for Kilo, so no files were previously being written to the wrong location. (#806) +- Honor the `COPILOT_HOME` environment variable when resolving the GitHub Copilot global config directory. Previously a global `--copilot` install ignored `COPILOT_HOME` and wrote all artifacts (skills, agents, `copilot-instructions.md`, the session hook) to `~/.copilot` even when the user had relocated their Copilot home, making them undiscoverable by Copilot CLI. Resolution now follows `--config-dir` > `COPILOT_CONFIG_DIR` > `COPILOT_HOME` > `~/.copilot`, mirroring the existing `CODEX_HOME` handling. Uninstall uses the same resolver and stays symmetric. (#812) (#814) +- **Release version bumps now keep runtime manifest versions in sync** — `.claude-plugin/plugin.json` and `gemini-extension.json` are stamped to match `package.json` on every `npm version`, unblocking RC/finalize releases. New version-bearing manifests must be registered in `scripts/sync-manifest-versions.cjs` (enforced by a regression test). (#845) +- **`npx @opengsd/gsd-core` upgrades no longer abort with "applied migration checksum changed"** — an already-applied installer migration whose recorded checksum drifted (e.g. a shipped body was edited) is now detected and reconciled automatically on the next install, instead of hard-failing the upgrade. Replaces the published-checksum allowlist with general self-healing recovery plus a CI baseline lock. (#675) +- **`/gsd-import`, `/gsd-plan-review-convergence`, and `/gsd-spec-phase` now run on global installs** — these workflows resolve `gsd-tools` via the runtime launcher instead of a hardcoded `$HOME` path, so they no longer falsely report the tool as "not found" (and stop short) when only a global/shim install is present and no project-local runtime exists. (#642) +- **Worktree wave-cleanup no longer fails when the phase SUMMARY is committed** — `rescueSummaryArtifacts` no longer copies an already-committed SUMMARY into the main checkout, which previously caused `git merge --no-ff` to abort with a permanent `merge_failed` (#706). (#709) +- **Phase execution no longer halts with `exit 42` (worktree base mismatch) when run on a branch diverged from the default branch (#683).** Claude Code forks worktree-isolated executors off the repository default branch (`origin/HEAD`), so running `/gsd-execute-phase` on an unmerged milestone/feature branch left every executor without the phase's plan files and tripped the `worktree-branch-check` guard (100% reproducible, all OSes). Execute-phase now detects this before dispatch and automatically degrades to sequential execution on the main working tree, recommending the permanent fix `worktree.baseRef:"head"`. Both fresh installs and upgrades of GSD Core set `worktree.baseRef:"head"` in `.claude/settings.local.json` automatically (no-clobber) when `workflow.use_worktrees` is enabled (the default); `gsd-tools worktree set-baseref` remains available for manual use (e.g. after toggling worktrees on later). The `exit 42` guard remains as a backstop. (#749) +- **Codex install no longer corrupts launcher paths** — shell path segments like `${VAR}/gsd-core/` and `$(cmd)/gsd-local-patches` are no longer rewritten into a literal `$gsd-core` token during Codex markdown conversion (#704). (#710) +- **`/gsd:surface` no longer corrupts installed skill paths** — re-surfacing (profile/enable/disable/reset) now applies the same per-runtime path rewrites as install, so SKILL.md bodies keep the correct install target instead of reverting to the converter's default `~/.claude` paths. (#817) +- **`/gsd:graphify`, `/gsd:import`, and planning agents now resolve `gsd-tools` on global/shim-only installs** — agent and command surfaces that invoked a hardcoded `$HOME/.claude/...gsd-tools.cjs` path now route through the resolved `gsd_run` launcher, so the step no longer reports the tool "not found" when there is no project-local runtime. (#707) +- **`/gsd:surface` no longer mis-names or orphans runtime command files** — re-surfacing now writes the same `gsd-`-prefixed command filenames as a fresh install for flat command dirs (Cursor, Augment, OpenCode, Kilo) and preserves user-authored command files instead of deleting them. (#822) +- **`/gsd:update` reliably previews release notes again** — promotes the 1.3.x changelog into dated `[1.3.0]`/`[1.3.1]` sections, stops deleting the temp changelog before the human-readable render (no more `(changelog unavailable)`), and adds a release gate that blocks publishing a version whose `CHANGELOG.md` section was never promoted. (#694) + +### Security + +- **`gsd-tools config-set` prototype-pollution guard hardened and regression-tested.** The guard that blocks `__proto__`, `prototype`, and `constructor` segments in dotted config keys now uses inline literal comparisons at each property-write site (instead of a pre-loop `Set` check), so CodeQL's `js/prototype-pollution-utility` analysis recognises it as a sanitising barrier and code-scanning alert #26 clears. Runtime behaviour is unchanged from #663. Added regression tests that drive schema-valid dynamic-prefix keys (`agent_skills.__proto__`, `agent_skills.constructor`, `features.__proto__`, `review.models.constructor`) all the way to the guard — these reach `setConfigValue` past the schema gate and were previously the guard's only untested attack surface. (#751) (#752) +- **Hardened roadmap-phase parsing and config writes** — resolved ReDoS in phase-heading/plan-filename regexes (validate/verify/commands/phase), blocked prototype-pollution through dotted config keys in `config-set`, and pinned `qs >= 6.15.2` (DoS advisory). (#665) + ## [1.3.1](https://www.npmjs.com/package/@opengsd/gsd-core/v/1.3.1) - 2026-06-04 ### Security diff --git a/CONTEXT.md b/CONTEXT.md index 9e6ef245f..871b64625 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -104,7 +104,7 @@ Module owning runtime identity normalization at runtime-selection seams. Canonic Module owning validation for Installer Migration Module records and planned actions. It enforces migration metadata, explicit install scopes, ownership evidence for destructive/config actions, and runtime contract citations for runtime config rewrites before a migration can enter planning or apply. ### Installer Module -Primary installer for all runtimes. Single production file: `bin/install.js` (generated). Exports: `install(isGlobal, runtime[, configDir])` → typed result `{ runtime, configDir, settingsPath, settings, statuslineCommand, updateBannerCommand }`; `uninstall(isGlobal, runtime[, configDir])`; `installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)`; `uninstallRuntimeArtifacts(runtime, configDir, scope)`; `writeManifest(configDir, runtime)`. Runtime enum: `allRuntimes` (15 values: claude, antigravity, augment, cline, codebuddy, codex, copilot, cursor, gemini, hermes, kilo, opencode, qwen, trae, windsurf). Directory helpers: `getDirName(runtime)` → local dir name; `getGlobalDir(runtime[, explicitDir])` → global path (env-var–aware per runtime); `getConfigDirFromHome(runtime, isGlobal)` → shell-quoted path fragment. Runtime-specific helpers: `resolveKiloConfigPath(configDir)`, `configureKiloPermissions(isGlobal[, explicitDir])`. Layout-driven artifact copy/removal delegates to `gsd-core/bin/lib/runtime-artifact-layout.cjs:resolveRuntimeArtifactLayout` (throws `TypeError` for unknown runtimes). Hermes uses nested `skills/gsd//` layout (prefix: ''); other skill-runtimes use flat `skills/gsd-/` layout. See Skill Surface Budget Module and Runtime Artifact Layout Module. +Primary installer for all runtimes. Single production file: `bin/install.js` (generated). Exports: `install(isGlobal, runtime[, configDir])` → typed result `{ runtime, configDir, settingsPath, settings, statuslineCommand, updateBannerCommand }`; `uninstall(isGlobal, runtime[, configDir])`; `installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)`; `uninstallRuntimeArtifacts(runtime, configDir, scope)`; `writeManifest(configDir, runtime)`. Runtime enum: `allRuntimes` (15 values: claude, antigravity, augment, cline, codebuddy, codex, copilot, cursor, gemini, hermes, kilo, opencode, qwen, trae, windsurf). Directory helpers: `getDirName(runtime)` → local dir name; `getConfigDirFromHome(runtime, isGlobal)` → shell-quoted path fragment. Per-runtime global config-dir resolution is delegated to `gsd-core/bin/lib/runtime-homes.cjs:getGlobalConfigDir(runtime[, explicitDir])` — the canonical, env-var–aware projection (`explicitDir` override + opencode/kilo `*_CONFIG` file-path precedence); the legacy in-installer `getGlobalDir`/`getOpencodeGlobalDir`/`getKiloGlobalDir` were retired into it (#56). Runtime-specific helpers: `resolveKiloConfigPath(configDir)`, `configureKiloPermissions(isGlobal[, explicitDir])`. Claude-specific permission helpers: `mergeClaudePermissions(settings)` — non-destructively appends GSD-owned allow/deny entries (see `GSD_CLAUDE_ALLOW_PERMISSIONS`, `GSD_CLAUDE_DENY_PERMISSIONS` constants) to a Claude Code settings object; called from `finishInstall` for `runtime === 'claude'` only; uninstall removes exactly these entries (#768). Layout-driven artifact copy/removal delegates to `gsd-core/bin/lib/runtime-artifact-layout.cjs:resolveRuntimeArtifactLayout` (throws `TypeError` for unknown runtimes). Hermes uses nested `skills/gsd//` layout (prefix: ''); other skill-runtimes use flat `skills/gsd-/` layout. See Skill Surface Budget Module and Runtime Artifact Layout Module. ### Package Identity Module [Planned] Single seam owning GSD's published-package coordinates so a repoint/rename is a one-line change instead of a tree-wide sweep. Source of truth is `package.json`; values are *derived*, not re-typed: `packageName` (`.name` → `@opengsd/get-shit-done-redux`), `binName` (`Object.keys(.bin)[0]` → `get-shit-done-redux`), `repoSlug` (parsed from `.repository.url` → `open-gsd/get-shit-done-redux`), plus derived `changelogRawUrl` and `manualInstallCommand({ scope, runtime })`. Generated `.cjs` per ADR-457 (generated-single-source); shipped under `gsd-core/bin/lib/`. Three consumer worlds: **Node** consumers `require()` it at runtime (worker, `check-latest-version.cjs`, `bin/install.js`); the **bash launcher** snippet receives the literal injected by `scripts/sync-runtime-launcher.cjs` at sync time; **prose/help** literals (`update.md`, installer help) carry a committed copy. A drift-guard lint (`scripts/lint-package-identity-drift.cjs`, sibling to `check:alias-drift`) fails CI on any raw package/repo literal outside `package.json`, the generated module, and the value-checked materialization sites — this is what keeps the seam real (`two adapters`, not one). Replaces the contradictory pair it consolidates: the runtime-broken `require('../package.json').name` in `hooks/gsd-check-update-worker.js` (#378, resolves to `undefined` post-install) and the hardcoded constant in `check-latest-version.cjs` (#2992). _Avoid_: "package name string", "the npm name" (when you mean the seam). See ADR-457 and Installer Module. @@ -116,11 +116,34 @@ Module owning install detection for `/gsd:update`. `resolveUpdateContext({ home, Module owning which skills and agents are written to runtime config directories at install time (Phase 1) and at runtime via cluster-level toggles (Phase 2). Phase 1: `gsd-core/bin/lib/install-profiles.cjs` defines named profiles (`core`, `standard`, `full`), computes transitive closure over `requires:` frontmatter, stages skills/agents to runtime config dirs, and persists the chosen profile in a `.gsd-profile` marker. Profile resolution precedence: explicit `--profile=` flag > `.gsd-profile` marker > `full`. `--minimal`/`--core-only` are back-compat aliases for `--profile=core`. Phase 2: `gsd-core/bin/lib/surface.cjs` implements the `/gsd:surface` slash command for cluster-level enable/disable without reinstall; cluster definitions live in `gsd-core/bin/lib/clusters.cjs`; per-runtime state persists in `/.gsd-surface.json` independent from the `.gsd-profile` marker. See ADR-0011. ### Runtime Artifact Layout Module -Module owning the per-runtime mapping from artifact kind to filesystem placement. ADR-3660 defines the typed `kinds` per runtime (`commands`, `agents`, `skills`) with destination subpath, prefix, and stage adapter (with per-runtime converters in `bin/install.js`: `convertClaudeCommandToClaudeSkill`, `…CodexSkill`, `…CopilotSkill`, `…AntigravitySkill`). Phase 1 applies this seam to the Runtime Surface Module (`surface.cjs:applySurface`). Phase 2 is planned to migrate install/uninstall in `bin/install.js` so all lifecycle sites iterate one shared layout table instead of re-encoding runtime layout logic. This design is intended to remove the #3659 class of omissions. Migrations remain under the Installer Migration Module (ADR-0008). See ADR-3660. +Module owning the per-runtime mapping from artifact kind to filesystem placement. ADR-3660 defines the typed `kinds` per runtime (`commands`, `agents`, `skills`) with destination subpath, prefix, and stage adapter (with per-runtime converters in `bin/install.js`: `convertClaudeCommandToClaudeSkill`, `…CodexSkill`, `…CopilotSkill`, `…AntigravitySkill`). Phase 1 applies this seam to the Runtime Surface Module (`surface.cjs:applySurface`); as of #813, `applySurface` applies the same per-runtime skill-body path rewrites as `installRuntimeArtifacts` for `skills` kinds — re-surfacing no longer overwrites installed SKILL.md bodies with converter-default `~/.claude` paths. The shared accessor `getInstallExports` (exported from `runtime-artifact-layout.cjs`) is the single-source seam through which `surface.cjs` reaches `computePathPrefix` and `applyRuntimeContentRewritesInPlace`; the resolved `scope` (`'local'`|`'global'`) is now carried on the `Layout` object returned by `resolveRuntimeArtifactLayout` so `applySurface` derives the same `pathPrefix` (global `$HOME` form vs. absolute) as a fresh install. Phase 2 is planned to migrate install/uninstall in `bin/install.js` so all lifecycle sites iterate one shared layout table instead of re-encoding runtime layout logic. This design is intended to remove the #3659 class of omissions. Migrations remain under the Installer Migration Module (ADR-0008). See ADR-3660. + +### Runtime Install Policy Module +Projects a pure, typed install plan for a given runtime by composing artifact placements (Runtime Artifact Layout Module), command text (Shell Command Projection Module), and per-runtime config intentions — with no filesystem IO or format-specific serialization. Runtime-specific adapters consume the plan and execute concrete file mutations and config rendering. See ADR-58. + +### Runtime Config Adapter Registry +Module owning the explicit per-runtime config-mutation dispatch table for the installer. `resolveRuntimeConfigIntent(runtime)` projects a typed config intent — `installSurface` (`settings-json` | `codex-toml` | `copilot-instructions` | `cline-rules` | `cursor-hooks-json` | `profile-marker-only`), `writesSharedSettings` (the `finishInstall` shared-settings write gate), and `finishPermissionWriter` (`opencode` | `kilo` | none) — that `bin/install.js` dispatches on instead of inline `runtime === '...'` branching. Owns adapter selection only: it performs no filesystem IO and does not execute config mutations (the install/finishInstall handlers and the per-runtime writers do that). Unknown runtimes fail loudly with a `TypeError`, guarded by an `Object.hasOwn` own-property check so prototype-chain keys (`__proto__`, `constructor`) also throw. Realizes the adapter-selection half of the Runtime Install Policy Module boundary. Source: `gsd-core/bin/lib/runtime-config-adapter-registry.cjs`. See ADR-58, #60. + +### Claude Code Plugin Manifest Module +Module owning the projection of gsd-core's artifact surfaces (`commands`, `agents`, hooks) onto the Claude Code plugin contract (`.claude-plugin/plugin.json` + `hooks/hooks.json`) — the plugin-contract sibling of the Runtime Artifact Layout Module (which projects the same surfaces onto filesystem placements). Defined mapping: `name`=`binName` (drives the `/gsd-core:` command namespace), `repository`/`homepage`=`repoUrl` (Package Identity Module), `version`/`description`/`license` from `package.json` (`version` is required for `claude plugin validate --strict`), `commands`=`./commands/gsd/`, agents via Claude Code's default `agents/` discovery (the explicit string form is schema-rejected), `hooks`=`./hooks/hooks.json`. The hook projection carries ONLY the always-on subset of the Installer Module's Claude `settings.json` wiring (check-update, context-monitor, prompt-guard, read-guard, worktree-path-guard, read-injection-scanner) via `${CLAUDE_PLUGIN_ROOT}`; config-gated opt-in hooks are excluded because a static manifest cannot honor per-project config gates, and plugin-shipped agents cannot carry hook frontmatter (so all plugin-path hook wiring lives in hooks.json). `hooks.json` covers all seven Claude Code lifecycle events: SessionStart, PreToolUse, PostToolUse, SubagentStop, Stop, PreCompact (all wired to context-monitor for context-headroom awareness), and FileChanged (matcher: `config.json` → config-reload, injects `additionalContext` when `.planning/config.json` changes mid-session). Additive — the file-copy path (Runtime Artifact Layout / Install Policy / Installer Modules) is unchanged. Conformance is validated by `claude plugin validate --strict` plus the in-repo drift-guard `tests/issue-766-plugin-manifest.test.cjs`. _Avoid_: "the plugin API", "the plugin file" (when you mean the seam). See ADR-766 and Runtime Artifact Layout Module. + +### Gemini Extension Package +The repo-root `gemini-extension.json` + `GEMINI.md` pair that projects gsd-core onto the Gemini CLI extension contract, enabling one-step lifecycle management via `gemini extensions install ` / `update` / `remove` (and `gemini extensions link ` for dev). The Gemini-CLI sibling of the Claude Code Plugin Manifest Module — same additive idea, different runtime package format. Defined mapping: `name`=`binName` (`gsd-core`; lowercase-dashes per Gemini's extension naming rule), `version` tracks `package.json` (Gemini's `gemini extensions update` keys off the manifest `version` field), `description` (required by the manifest schema), `contextFileName`=`GEMINI.md` (the extension's context payload, loaded into every Gemini session). Intentionally minimal: no `mcpServers` (gsd-core ships no MCP server). Slash-command / agent / hook projection into the extension (which would require committing the Gemini-format TOML/agent conversions the Installer Module produces at `--gemini` install time) is deferred — the manual `npx gsd-core --gemini` path remains the way to install the `/gsd:*` commands, and is unchanged (additive, no breaking change). Conformance is guarded by the in-repo drift test `tests/issue-775-gemini-extension.test.cjs` (manifest validity, `version`↔`package.json` parity, `contextFileName` existence, `files[]` publication). _Avoid_: "the Gemini plugin" (Gemini calls them extensions, not plugins). See #775, ADR-766, Claude Code Plugin Manifest Module, and Runtime Artifact Layout Module. ### Knowledge Graph Module Module owning the graphify integration: config gate (`isGraphifyEnabled`), disabled response (`disabledResponse`), subprocess helper (`execGraphify`, typed `GRAPHIFY_REASON` enum), presence detection (`checkGraphifyInstalled`), version checking (`checkGraphifyVersion`), query surface (`graphifyQuery` — BFS seed-expand + budget trim), status surface (`graphifyStatus` — node/edge counts, mtime staleness, commit-staleness tri-state via `built_at_commit`/`commits_behind`/`commit_stale`), diff surface (`graphifyDiff` — added/removed/changed nodes+edges), build pre-flight (`graphifyBuild`), snapshot management (`writeSnapshot`). Reads `.planning/config.json:graphify.enabled` as config gate; writes to `.planning/graphs/`. Auto-update hook (`hooks/gsd-graphify-update.sh`) triggers a detached background rebuild after HEAD-advancing git operations on the default branch when `graphify.auto_update=true`. Status file `.planning/graphs/.last-build-status.json` carries `{ ts, status, exit_code, duration_ms, head_at_build, graphify_version }`. Graph IR uses `nodes[]`, `edges[]` (or `links[]` for graphify ≥0.7 compat), `hyperedges[]`, `built_at_commit`. `commit_stale` is tri-state: `false` (known fresh), `true` (stale), `null` (unknown — no git or pre-v0.7 graph). Source: `gsd-core/bin/lib/graphify.cjs`. Skill: `commands/gsd/graphify.md`. +### Research Module +The GSD-RESEARCH capability behind an L2-hybrid seam: code owns cache + provider policy + package legitimacy; MCP owns the actual fetch. Reachable via `gsd-tools query research-plan|research-store|package-legitimacy`. Source: `src/research-{store,provider}.cts` + `src/package-legitimacy.cts` (generated to `gsd-core/bin/lib/*.cjs` per ADR-457). Replaces the prose provider-waterfall duplicated across the researcher agents and the pip-install `slopcheck` bolt-on. + +- `GSD-RESEARCH.MODULE.research-store=content-addressed cache; key=sha256(ecosystem+library+version+query+kind); getResearch->{hit,stale} never throws (mirrors graphify staleness); ttlForSource curated HIGH 30d|MED 7d|web LOW 1d; tiers: curated-doc kinds -> ~/.gsd/research-cache (cross-project), web/synthesis -> project .planning/research/.cache` +- `GSD-RESEARCH.MODULE.research-provider=single source of truth PROVIDER_WATERFALL (docs Context7->Ref->Jina->websearch; web Exa->Tavily->Perplexity->Brave->websearch; scrape Firecrawl->Jina); planResearch returns cache-hits+fetch-plan; classifyConfidence stamps HIGH|MEDIUM|LOW by provider AUTHORITY + verification EVIDENCE (HIGH requires code-computed ground-truth corroboration e.g. legitimacyVerdict OK; provider authority alone caps at MEDIUM; SLOP caps at LOW); Firecrawl is scrape-only (not in docs/web discovery)` +- `GSD-RESEARCH.MODULE.package-legitimacy=registry-API verdicts (npm/PyPI/crates.io injectable adapters) computed from thresholds {minAgeDays:30,minWeeklyDownloads:1000,requireRepo:true}; verdict OK|SUS|SLOP per package; slopcheck=optional adapter that can only escalate, never the install-or-degrade gate` +- `GSD-RESEARCH.INTEGRATION.L2-hybrid=code owns cache+legitimacy+confidence+provider-pick (gsd-tools query research-plan/research-store/package-legitimacy); MCP owns the fetch; agent returns RESEARCH.md path, never raw fetches` +- `GSD-RESEARCH.PROVIDER.availability=config flags brave_search/exa_search/firecrawl/tavily_search/ref_search/perplexity/jina (env _API_KEY or ~/.gsd/_api_key); context7/jina/websearch always available; planResearch falls through waterfall to websearch terminal` +- `GSD-RESEARCH.CONTEXT-DISCIPLINE=less-context levers: subagent isolation + compact provider output + fetches-to-disk + cache-returns-digest; API clear_tool_uses/memory tool are the conceptual model, not a Claude Code harness knob` +- `DEFECT.RESEARCH-PROVIDER-PROSE-DRIFT=provider waterfall duplicated across N researcher agent .md files drifts independently (META.RULE.brief-no-paraphrase); fix-forward=research-provider.cjs single source of truth + generated agents (#657)` + ### MVP Mode Phase-level planning mode that frames work as a vertical slice (UI → API → DB) of one user-visible capability instead of horizontal layers. Resolved at workflow init via the precedence chain: `--mvp` CLI flag → ROADMAP.md `**Mode:** mvp` field → `workflow.mvp_mode` config → false. All-or-nothing per phase (PRD #2826 Q1). Surfaced as `MVP_MODE=true|false` to the planner, executor, verifier, and discovery surfaces (progress, stats, graphify). Canonical parser: `roadmap.cjs` `**Mode:**` field; canonical resolution chain documented in `workflows/plan-phase.md`. Concept index: `references/mvp-concepts.md`. @@ -184,7 +207,7 @@ The canonical lint infrastructure adopted in ADR 452 (`docs/adr/452-eslint-lint- `RULESET.TESTS.eslint-harness=ADR 452 (2026-05-28): ESLint flat config + typescript-eslint + eslint-plugin-n + eslint-plugin-no-only-tests + local plugin at scripts/eslint-rules/; replaces scripts/lint-*.cjs regex scanners; three test-rigor rules (local/no-source-grep, local/no-magic-sleep-in-tests, local/no-elapsed-assertion) ship at warn, promoted to error after #453 cleanup sweep merges` `RULESET.WORKFLOW_MARKDOWN.FENCES=preserve opening language fence when editing shell snippets in workflow markdown; malformed fence creates fresh CR threads (MD040)` -`RULESET.WORKFLOW_SIZE_BUDGET=workflow-size-budget can fail otherwise-valid review fixes; XL workflows <=1800 lines or trim prose before final checks` +`RULESET.WORKFLOW_SIZE_BUDGET=workflow-size-budget (#717) measures BYTES not lines; tiers XL<=90000 / LARGE<=54000 / DEFAULT<=38000 bytes, discuss-phase<30000; can fail otherwise-valid review fixes — trim prose or extract LAZILY-loaded content (eager @-imports don't reduce loaded context) before final checks` `RULESET.WORKFLOW_FILE_NAMES=workflow files use hyphens; XML attributes must match (extract-learnings not extract_learnings); tests should pin exact hyphenated name` `RULESET.WORKFLOW_EXECUTION_CONTEXT=@-ref in commands/gsd/*.md must resolve to an existing file on disk; regression test in tests/bug-3135-capture-backlog-workflow.test.cjs; INVENTORY.md row + INVENTORY-MANIFEST.json families.workflows must stay in sync; "Invoked by" attribution must move when a flag absorbs a micro-skill` `RULESET.WORKFLOW_EXECUTE_END_TO_END=ADR-0002 standard for single-workflow commands is "Execute end-to-end." (no bolded **Follow the X workflow** fragments); flag-dispatch routing uses "execute the X workflow end-to-end." in routing bullets` @@ -227,7 +250,6 @@ The canonical lint infrastructure adopted in ADR 452 (`docs/adr/452-eslint-lint- `RULESET.CODERABBIT.GUARD.SCOPE=if a new @me open PR appears during final list, include it in the same guard pass before declaring all-open-PRs complete` `RULESET.TESTS.CODERABBIT_FIX=prefer exported-function behavioral tests over source-grep; lint-no-source-grep rejects readFileSync source assertions without allow-test-rule` `RULESET.WORKFLOW_MARKDOWN.FENCES=when editing shell snippets inside workflow markdown, preserve the opening language fence; malformed fence can create fresh CodeRabbit threads` -`RULESET.WORKFLOW_SIZE_BUDGET=workflow-size-budget can fail otherwise-valid review fixes; keep XL workflows <=1800 lines or trim prose in same PR before final checks` `RULESET.GEMINI.TOOLS.ask_user=Gemini CLI has no ask_user tool; filter both AskUserQuestion and lowercase ask_user from tools frontmatter and neutralize both names in Gemini body text` `RULESET.GEMINI.TEST_SENTINEL=convertClaudeToGeminiAgent regression should assert tools excludes ask_user, body excludes AskUserQuestion/ask_user, and Read still maps to read_file` @@ -516,6 +538,15 @@ The canonical lint infrastructure adopted in ADR 452 (`docs/adr/452-eslint-lint- `DEFECT.GENERATIVE-FIX=for any new constant/array/parser shared between two parallel surfaces (two workflow surfaces, or a generated artifact and its hand-authored source), the same commit MUST add a parity assertion that fails when the two diverge` `DEFECT.GENERATIVE-EXEMPLAR=tests/runtime-launcher-parity.test.cjs (asserts every workflow bash block uses the canonical gsd_run launcher — the in-repo pattern for enforcing equality across parallel surfaces)` +`DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.symptom=a YAML-frontmatter scalar (e.g. VERIFICATION.md status) read with grep "^key:" over the WHOLE markdown report instead of the frontmatter block; a key: line in the body (code block, copied artifact, example) returns extra matches that concatenate after cut|tr into a value matching no expected token, so a valid state is misrouted` +`DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.examples=#586/PR #650 ship.md verification gate — grep "^status:" also matched body status: lines, yielding passed+gaps_found+human_needed instead of passed and blocking a passed phase; the same broad-grep still lives in execute-phase.md (consolidation tracked by #651)` +`DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.detect=grep "^:" on a *.md whose result is compared to exact tokens, with no frontmatter scoping and no -m1; one body line beginning : is enough to break it` +`DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.fix-forward=scope to the leading frontmatter block and take the first match: sed -n '/^---$/,/^---$/p' "$f" | grep -m1 "^:" | cut -d: -f2 | tr -d ' '; fix every parallel copy in the same change or consolidate behind one queryable seam (#651)` +`DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.symptom=a test that parses a workflow bash block out of a *.md and runs it via execFileSync('bash',...) breaks on Windows two ways: the fence regex uses a literal \n after the bash fence that will not match CRLF and trips windows-test-parity-guard (fenceRegexLiteralNewline); and git-bash exists so a bash-presence probe is true, but an os.tmpdir() Windows path (C:\...) is un-globbable in bash so the pipeline returns empty and assertions fail` +`DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.examples=#586/PR #650 tests/ship-586-verification-routing.test.cjs — the fence \n offender failed ubuntu-24/macos/coverage, then the Windows tmpdir-path glob failed full test (windows-latest,22) at fail 3; both were invisible to file-scoped gsd-test-both runs because the parity guard is only scanned by the full suite` +`DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.detect=test does readFileSync(md).match for a bash fence with literal \n, OR execFileSync('bash',...) gated only on a bash-presence probe; also verifying a new test with a file-scoped run instead of the full suite hides repo-wide static guards` +`DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.fix-forward=match the fence with \r?\n and normalize the captured block to LF; gate pipeline execution on process.platform !== 'win32' && hasBash since the extraction LOGIC is platform-independent and POSIX coverage suffices; run the full suite (or the parity/lint guards) before push when adding a test file` + --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef458d327..2b0caebf8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -133,7 +133,7 @@ and `next` (integration for the upcoming release). **Almost every PR targets | `chore/`, `docs/`, `refactor/`, `test/`, `perf/`, `ci/`, `revert/` | `next` | All routine work | | `fix/critical-NNN-slug` | `main` | Production-down emergencies only; auto-back-merges to `next` | | `release/X.Y.0` | `main` | Created by `release.yml` — don't make these by hand | -| `hotfix/X.Y.Z` | `main` | Created by `hotfix.yml` — don't make these by hand | +| `hotfix/X.Y.Z` | `main` | Created by `release.yml` (dispatch with a patch version X.Y.Z) — don't make these by hand | | Stabilization PR for an in-flight release | `release/X.Y.0` | Fix a regression found during the RC cycle | **Day-to-day commands:** diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 000000000..0d298557d --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,53 @@ +# GSD Core — Gemini CLI context + +This context is loaded by the **gsd-core Gemini CLI extension**. It gives Gemini +the operating context for [GSD Core](https://github.com/open-gsd/gsd-core), a +meta-prompting, context-engineering, and spec-driven development system for AI +coding agents. + +## What GSD is + +GSD turns a vague goal into shipped software through an explicit, +resumable workflow: **explore → plan → execute → verify → ship**. Work is +organised into milestones and phases under a `.planning/` directory, with each +phase carrying a SPEC, a PLAN, and verification criteria. The system favours +small, atomic, test-backed commits and keeps durable context in version-tracked +files rather than in the conversation. + +## The slash commands (installed separately) + +> **This extension ships only the context above — not the slash commands.** It +> loads gsd's operating context into your Gemini sessions and is managed through +> `gemini extensions list / update / uninstall`. To install the `/gsd:*` command +> set, agents, and hooks into `~/.gemini/`, run the dedicated installer: +> +> ```bash +> npx gsd-core --gemini --global +> ``` +> +> The two paths are complementary and the manual installer remains fully +> supported. The commands below are available only once that installer has run. + +If you have installed the gsd commands, the workflow is driven by these `/gsd:*` +slash commands (Gemini registers gsd's commands under the `gsd` namespace, so the +colon form is canonical): + +- `/gsd:new-project` — initialise a project and gather deep context. +- `/gsd:progress` — the unified situational command: check progress, advance the + workflow, or dispatch a freeform intent. +- `/gsd:plan-phase ` — produce a detailed phase plan with a verification loop. +- `/gsd:execute-phase ` — execute a phase's plans with wave-based parallelism. +- `/gsd:verify-work` — validate built features through conversational UAT. +- `/gsd:ship` — open a PR, run review, and prepare for merge. +- `/gsd:help` — list every available command. + +## Working with GSD + +- Treat `.planning/` as the source of truth for project state — read it before + acting, and keep it current as work progresses. +- Prefer the smallest change that satisfies the phase's verification criteria. +- Run the project's tests and linters before declaring a phase done. +- When unsure what to do next, and the gsd commands are installed, `/gsd:progress` + is the situational entry point. + +Learn more: diff --git a/VERSIONING.md b/VERSIONING.md index cb9de4e96..7c3a53493 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -71,35 +71,24 @@ For fixes that need to ship without waiting for the next minor. A hotfix `vX.YY.Z` cumulatively includes everything in `vX.YY.{Z-1}` plus every `fix:`/`chore:` commit landed on `main` since that base. The base tag is the anchor — `git cherry $BASE_TAG main` reveals exactly which commits are still unshipped, and the new `vX.YY.Z` tag becomes the next hotfix's base, so the cycle is self-documenting. -#### Two paths +#### How to dispatch a hotfix -**Path A — `hotfix.yml` (canonical, two-step):** +Hotfixes are dispatched via the **Release workflow (`release.yml`)** with a patch version (X.Y.Z). There is no separate hotfix workflow. -1. Trigger `hotfix.yml` with `action=create`, `version=1.27.1`, `auto_cherry_pick=true` (default). +1. Trigger `release.yml` with `action=create`, `version=1.27.1`, `auto_cherry_pick=true` (default). - Workflow detects `BASE_TAG` = highest `v1.27.*` < `v1.27.1` (so `1.27.1` branches from `v1.27.0`; `1.27.2` would branch from `v1.27.1`). - Branches `hotfix/1.27.1` from `BASE_TAG`. - Auto-cherry-picks every `fix:`/`chore:` commit on `origin/main` not already in the base, oldest-first. Patch-equivalents are skipped via `git cherry`. `feat:`/`refactor:` are **never** auto-included. - On conflict the workflow halts with the offending SHA. Resolve manually on the branch, then re-run finalize with `auto_cherry_pick=false`. - Bumps `package.json` (and `sdk/package.json`), pushes the branch, and lists every included SHA in the run summary. 2. (Optional) push additional manual commits to `hotfix/1.27.1`. -3. Trigger `hotfix.yml` with `action=finalize`. The workflow: +3. Trigger `release.yml` with `action=finalize`. The workflow: - Runs `install-smoke` cross-platform gate. - Runs full test suite + coverage. - - Builds SDK, bundles `sdk-bundle/gsd-sdk.tgz` inside the CC tarball (parity with `release-sdk.yml`). + - Builds SDK, bundles `sdk-bundle/gsd-sdk.tgz` inside the CC tarball. - Tags `v1.27.1`, publishes to `@latest`, re-points `@next → v1.27.1`. - Opens merge-back PR against `main`. -**Path B — `release-sdk.yml` (stopgap, one-shot):** - -Active while the `@opengsd/gsd-sdk` npm token is unavailable; bundles the SDK inside the CC tarball. - -1. Trigger `release-sdk.yml` with `action=hotfix`, `version=1.27.1`, `auto_cherry_pick=true`. - - The `prepare` job creates the branch and cherry-picks (same logic as Path A). - - `install-smoke` runs against the new branch. - - The `release` job tags, publishes to `@latest`, re-points `@next`, opens merge-back PR. - - Idempotent: if `hotfix/1.27.1` already exists (e.g. you ran `hotfix.yml create` first), the prepare job checks it out and re-runs cherry-pick as a no-op. -2. `dry_run=true` exercises the full pipeline without pushing the branch or publishing. - ### Minor Release (Standard Cycle) For accumulated fixes and enhancements. @@ -135,6 +124,24 @@ Branch names map to commit types: | `docs/` | `docs:` | none | | `refactor/` | `refactor:` | none | +## Manifest Version Sync + +Certain runtime-integration manifests carry a `version` field that must always +match `package.json`: + +- `.claude-plugin/plugin.json` — Claude Code plugin manifest (issue #766) +- `gemini-extension.json` — Gemini CLI extension manifest (issue #775) + +The `version` npm lifecycle script (`scripts/sync-manifest-versions.cjs --stage`) +stamps these files automatically on every `npm version` call, and stages them so +they are included in the release commit alongside `package.json`. + +To add a new manifest that must track the package version, register its path in +the `VERSIONED_MANIFESTS` array in `scripts/sync-manifest-versions.cjs`. A +regression test (`tests/issue-844-manifest-version-sync.test.cjs`) enforces this: +it scans all committed JSON files for a matching `version` field and fails if any +are missing from the registry. + ## Publishing Commands (Reference) ```bash diff --git a/agents/gsd-advisor-researcher.md b/agents/gsd-advisor-researcher.md index 0a7b27f9a..9218b97b8 100644 --- a/agents/gsd-advisor-researcher.md +++ b/agents/gsd-advisor-researcher.md @@ -18,26 +18,7 @@ Spawned by `discuss-phase` via `Task()`. You do NOT present output directly to t -When you need library or framework documentation, check in this order: - -1. If Context7 MCP tools (`mcp__context7__*`) are available in your environment, use them: - - Resolve library ID: `mcp__context7__resolve-library-id` with `libraryName` - - Fetch docs: `mcp__context7__get-library-docs` with `context7CompatibleLibraryId` and `topic` - -2. If Context7 MCP is not available (upstream bug anthropics/claude-code#13898 strips MCP - tools from agents with a `tools:` frontmatter restriction), use the CLI fallback via Bash: - - Step 1 — Resolve library ID: - ```bash - npx --yes ctx7@latest library "" - ``` - Step 2 — Fetch documentation: - ```bash - npx --yes ctx7@latest docs "" - ``` - -Do not skip documentation lookups because MCP tools are unavailable — the CLI fallback -works via Bash and produces equivalent output. +@~/.claude/gsd-core/references/research-documentation-lookup.md diff --git a/agents/gsd-ai-researcher.md b/agents/gsd-ai-researcher.md index ac9261fa3..3d1c58ea8 100644 --- a/agents/gsd-ai-researcher.md +++ b/agents/gsd-ai-researcher.md @@ -2,7 +2,7 @@ name: gsd-ai-researcher description: Researches a chosen AI framework's official docs to produce implementation-ready guidance — best practices, syntax, core patterns, and pitfalls distilled for the specific use case. Writes the Framework Quick Reference and Implementation Guidance sections of AI-SPEC.md. Spawned by /gsd:ai-integration-phase orchestrator. tools: Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, mcp__context7__* -color: "#34D399" +color: green # hooks: # PostToolUse: # - matcher: "Write|Edit" @@ -17,26 +17,7 @@ Write Sections 3–4b of AI-SPEC.md: framework quick reference, implementation g -When you need library or framework documentation, check in this order: - -1. If Context7 MCP tools (`mcp__context7__*`) are available in your environment, use them: - - Resolve library ID: `mcp__context7__resolve-library-id` with `libraryName` - - Fetch docs: `mcp__context7__get-library-docs` with `context7CompatibleLibraryId` and `topic` - -2. If Context7 MCP is not available (upstream bug anthropics/claude-code#13898 strips MCP - tools from agents with a `tools:` frontmatter restriction), use the CLI fallback via Bash: - - Step 1 — Resolve library ID: - ```bash - npx --yes ctx7@latest library "" - ``` - Step 2 — Fetch documentation: - ```bash - npx --yes ctx7@latest docs "" - ``` - -Do not skip documentation lookups because MCP tools are unavailable — the CLI fallback -works via Bash and produces equivalent output. +@~/.claude/gsd-core/references/research-documentation-lookup.md diff --git a/agents/gsd-code-fixer.md b/agents/gsd-code-fixer.md index f7e04266b..841e8b435 100644 --- a/agents/gsd-code-fixer.md +++ b/agents/gsd-code-fixer.md @@ -2,7 +2,7 @@ name: gsd-code-fixer description: Applies fixes to code review findings from REVIEW.md. Reads source files, applies intelligent fixes, and commits each fix atomically. Spawned by /gsd:code-review --fix. tools: Read, Edit, Write, Bash, Grep, Glob -color: "#10B981" +color: green # hooks: # - before_write --- diff --git a/agents/gsd-code-reviewer.md b/agents/gsd-code-reviewer.md index 17a01abec..772c22aeb 100644 --- a/agents/gsd-code-reviewer.md +++ b/agents/gsd-code-reviewer.md @@ -2,7 +2,7 @@ name: gsd-code-reviewer description: Reviews source files for bugs, security issues, and code quality problems. Produces structured REVIEW.md with severity-classified findings. Spawned by /gsd:code-review. tools: Read, Write, Bash, Grep, Glob -color: "#F59E0B" +color: orange # hooks: # - before_write --- diff --git a/agents/gsd-domain-researcher.md b/agents/gsd-domain-researcher.md index 7144fb026..8b55f686d 100644 --- a/agents/gsd-domain-researcher.md +++ b/agents/gsd-domain-researcher.md @@ -2,7 +2,7 @@ name: gsd-domain-researcher description: Researches the business domain and real-world application context of the AI system being built. Surfaces domain expert evaluation criteria, industry-specific failure modes, regulatory context, and what "good" looks like for practitioners in this field — before the eval-planner turns it into measurable rubrics. Spawned by /gsd:ai-integration-phase orchestrator. tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__* -color: "#A78BFA" +color: purple # hooks: # PostToolUse: # - matcher: "Write|Edit" @@ -17,26 +17,7 @@ Research the business domain — not the technical framework. Write Section 1b o -When you need library or framework documentation, check in this order: - -1. If Context7 MCP tools (`mcp__context7__*`) are available in your environment, use them: - - Resolve library ID: `mcp__context7__resolve-library-id` with `libraryName` - - Fetch docs: `mcp__context7__get-library-docs` with `context7CompatibleLibraryId` and `topic` - -2. If Context7 MCP is not available (upstream bug anthropics/claude-code#13898 strips MCP - tools from agents with a `tools:` frontmatter restriction), use the CLI fallback via Bash: - - Step 1 — Resolve library ID: - ```bash - npx --yes ctx7@latest library "" - ``` - Step 2 — Fetch documentation: - ```bash - npx --yes ctx7@latest docs "" - ``` - -Do not skip documentation lookups because MCP tools are unavailable — the CLI fallback -works via Bash and produces equivalent output. +@~/.claude/gsd-core/references/research-documentation-lookup.md diff --git a/agents/gsd-eval-auditor.md b/agents/gsd-eval-auditor.md index d23a6c838..d55503831 100644 --- a/agents/gsd-eval-auditor.md +++ b/agents/gsd-eval-auditor.md @@ -2,7 +2,7 @@ name: gsd-eval-auditor description: Retroactive audit of an implemented AI phase's evaluation coverage. Checks implementation against the AI-SPEC.md evaluation plan. Scores each eval dimension as COVERED/PARTIAL/MISSING. Produces a scored EVAL-REVIEW.md with findings, gaps, and remediation guidance. Spawned by /gsd:eval-review orchestrator. tools: Read, Write, Bash, Grep, Glob -color: "#EF4444" +color: red # hooks: # PostToolUse: # - matcher: "Write|Edit" diff --git a/agents/gsd-eval-planner.md b/agents/gsd-eval-planner.md index 25f61b4b6..5bad98c4a 100644 --- a/agents/gsd-eval-planner.md +++ b/agents/gsd-eval-planner.md @@ -2,7 +2,7 @@ name: gsd-eval-planner description: Designs a structured evaluation strategy for an AI phase. Identifies critical failure modes, selects eval dimensions with rubrics, recommends tooling, and specifies the reference dataset. Writes the Evaluation Strategy, Guardrails, and Production Monitoring sections of AI-SPEC.md. Spawned by /gsd:ai-integration-phase orchestrator. tools: Read, Write, Edit, Bash, Grep, Glob, AskUserQuestion -color: "#F59E0B" +color: orange # hooks: # PostToolUse: # - matcher: "Write|Edit" diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index 1deac8ca3..2a70dbea5 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -385,7 +385,7 @@ If RED or GREEN gate commits are missing, add a warning to SUMMARY.md under a `# ## MVP+TDD Gate -**When the orchestrator passes both `MVP_MODE=true` and `TDD_MODE=true`:** Before running the implementation step of any task with `tdd="true"`, run the runtime gate from `@~/.claude/gsd-core/references/execute-mvp-tdd.md`. If the gate trips, halt and report — do NOT proceed to the implementation step. +**When the orchestrator passes both `MVP_MODE=true` and `TDD_MODE=true`:** Before running the implementation step of any task with `tdd="true"`, run the runtime gate from `~/.claude/gsd-core/references/execute-mvp-tdd.md` (Read it). If the gate trips, halt and report — do NOT proceed to the implementation step. **Halt-and-report protocol:** diff --git a/agents/gsd-framework-selector.md b/agents/gsd-framework-selector.md index c14a4fc02..b9c9d5d99 100644 --- a/agents/gsd-framework-selector.md +++ b/agents/gsd-framework-selector.md @@ -2,7 +2,7 @@ name: gsd-framework-selector description: Presents an interactive decision matrix to surface the right AI/LLM framework for the user's specific use case. Produces a scored recommendation with rationale. Spawned by /gsd:ai-integration-phase and /gsd-select-framework orchestrators. tools: Read, Bash, Grep, Glob, WebSearch, AskUserQuestion -color: "#38BDF8" +color: cyan --- diff --git a/agents/gsd-nyquist-auditor.md b/agents/gsd-nyquist-auditor.md index 86d5352a3..35279b10b 100644 --- a/agents/gsd-nyquist-auditor.md +++ b/agents/gsd-nyquist-auditor.md @@ -8,7 +8,7 @@ tools: - Bash - Glob - Grep -color: "#8B5CF6" +color: purple --- diff --git a/agents/gsd-pattern-mapper.md b/agents/gsd-pattern-mapper.md index 9b83144f2..c07288049 100644 --- a/agents/gsd-pattern-mapper.md +++ b/agents/gsd-pattern-mapper.md @@ -2,7 +2,7 @@ name: gsd-pattern-mapper description: Analyzes codebase for existing patterns and produces PATTERNS.md mapping new files to closest analogs. Read-only codebase analysis spawned by /gsd:plan-phase orchestrator before planning. tools: Read, Bash, Glob, Grep, Write -color: magenta +color: purple # hooks: # PostToolUse: # - matcher: "Write|Edit" diff --git a/agents/gsd-phase-researcher.md b/agents/gsd-phase-researcher.md index 8c3368436..8df612a10 100644 --- a/agents/gsd-phase-researcher.md +++ b/agents/gsd-phase-researcher.md @@ -1,7 +1,7 @@ --- name: gsd-phase-researcher description: Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator. -tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__* +tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__* color: cyan # hooks: # PostToolUse: @@ -30,41 +30,13 @@ Spawned by `/gsd:plan-phase` (integrated) or `/gsd:plan-phase --research-phase < - `[CITED: docs.example.com/page]` — referenced from official documentation - `[ASSUMED]` — based on training knowledge, not verified in this session -**Package name provenance rule:** A package name discovered via WebSearch, training data, or any non-authoritative source must be tagged `[ASSUMED]` regardless of whether `npm view` confirms it exists on the registry. Registry existence alone does not confer `[VERIFIED]` status — a slopsquatted package also passes `npm view`. Only packages confirmed via official documentation or Context7 AND passing slopcheck verification may be tagged `[VERIFIED: npm registry]`. +**Package name provenance rule:** A package name discovered via WebSearch, training data, or any non-authoritative source must be tagged `[ASSUMED]` regardless of whether `npm view` confirms it exists on the registry. Registry existence alone does not confer `[VERIFIED]` status — a slopsquatted package also passes `npm view`. Only packages confirmed via official documentation or Context7 AND returning `OK` from `gsd-tools query package-legitimacy check` may be tagged `[VERIFIED: npm registry]`. Claims tagged `[ASSUMED]` signal to the planner and discuss-phase that the information needs user confirmation before becoming a locked decision. Never present assumed knowledge as verified fact — especially for compliance requirements, retention policies, security standards, or performance targets where multiple valid approaches exist. -When you need library or framework documentation, check in this order: - -1. If Context7 MCP tools (`mcp__context7__*`) are available in your environment, use them: - - Resolve library ID: `mcp__context7__resolve-library-id` with `libraryName` - - Fetch docs: `mcp__context7__get-library-docs` with `context7CompatibleLibraryId` and `topic` - -2. If Context7 MCP is not available (upstream bug anthropics/claude-code#13898 strips MCP - tools from agents with a `tools:` frontmatter restriction), use the CLI fallback via Bash: - - Step 1 — Resolve library ID: - ```bash - if command -v ctx7 &>/dev/null; then - ctx7 library "" - else - echo "ctx7 not found — install with: npm install -g ctx7 (verify at npmjs.com/package/ctx7 first)" - fi - ``` - Step 2 — Fetch documentation: - ```bash - if command -v ctx7 &>/dev/null; then - ctx7 docs "" - else - echo "ctx7 not found — install with: npm install -g ctx7 (verify at npmjs.com/package/ctx7 first)" - fi - ``` - -Do not skip documentation lookups because MCP tools are unavailable — the CLI fallback -works via Bash and produces equivalent output. Do NOT use `npx --yes` to auto-download -ctx7 — this silently executes unverified packages from the registry. +@~/.claude/gsd-core/references/research-documentation-lookup.md @@ -109,153 +81,106 @@ Your RESEARCH.md is consumed by `gsd-planner`: - -## Claude's Training as Hypothesis - -Training data is 6-18 months stale. Treat pre-existing knowledge as hypothesis, not fact. - -**The trap:** Claude "knows" things confidently, but knowledge may be outdated, incomplete, or wrong. - -**The discipline:** -1. **Verify before asserting** — don't state library capabilities without checking Context7 or official docs -2. **Date your knowledge** — "As of my training" is a warning flag -3. **Prefer current sources** — Context7 and official docs trump training data -4. **Flag uncertainty** — LOW confidence when only training data supports a claim - -## Honest Reporting - -Research value comes from accuracy, not completeness theater. - -**Report honestly:** -- "I couldn't find X" is valuable (now we know to investigate differently) -- "This is LOW confidence" is valuable (flags for validation) -- "Sources contradict" is valuable (surfaces real ambiguity) - -**Avoid:** Padding findings, stating unverified claims as facts, hiding uncertainty behind confident language. - -## Research is Investigation, Not Confirmation - -**Bad research:** Start with hypothesis, find evidence to support it -**Good research:** Gather evidence, form conclusions from evidence - -When researching "best library for X": find what the ecosystem actually uses, document tradeoffs honestly, let evidence drive recommendation. - +@~/.claude/gsd-core/references/research-philosophy.md -## Tool Priority +## Research Plan via Code Seam -| Priority | Tool | Use For | Trust Level | -|----------|------|---------|-------------| -| 1st | Context7 | Library APIs, features, configuration, versions | HIGH | -| 2nd | WebFetch | Official docs/READMEs not in Context7, changelogs | HIGH-MEDIUM | -| 3rd | WebSearch | Ecosystem discovery, community patterns, pitfalls | Needs verification | +The agent decides **what** to research (the questions). The seam decides **which provider** to use and manages caching. -**Context7 flow:** -1. `mcp__context7__resolve-library-id` with libraryName -2. `mcp__context7__query-docs` with resolved ID + specific query +### Step A — Build a research-plan input file -**WebSearch tips:** Use multiple query variations. Cross-verify with authoritative sources. Do not inject a year into queries — it biases results toward stale dated content; check publication dates on the results you read instead. +Construct a JSON file at a temp path (e.g. `/tmp/research-plan-input.json`): -## Enhanced Web Search (Brave API) +```json +{ + "ecosystem": "", + "config": { "exa_search": true/false, "brave_search": true/false, "firecrawl": true/false, "tavily_search": true/false }, + "questions": [ + { "text": "How does X work?", "kind": "docs", "library": "x", "version": "1.2.3" }, + { "text": "Best practices for Y?", "kind": "web" } + ] +} +``` -Check `brave_search` from init context. If `true`, use Brave Search for higher quality results: +`config` comes from the init context (availability flags). `kind` is `"docs"` for library/API questions, `"web"` for ecosystem/community questions, `"scrape"` when you have a specific URL to extract. + +### Step B — Obtain the fetch plan ```bash -gsd-tools query websearch "your query" --limit 10 +gsd-tools query research-plan --input /tmp/research-plan-input.json ``` -**Options:** -- `--limit N` — Number of results (default: 10) -- `--freshness day|week|month` — Restrict to recent content +Returns `{ "items": [ { "question": "...", "key": "", "cache": { "hit": true/false, "stale": false }, "fetch": { "provider": "context7", "query": "..." } } ] }`. -If `brave_search: false` (or not set), use built-in WebSearch tool instead. +- `cache.hit && !cache.stale` → reuse the cached digest; no fetch needed. +- `cache.hit && cache.stale` → fetch anyway to refresh; the old entry is returned as a fallback. +- no `cache` field → cache miss; must fetch. -Brave Search provides an independent index (not Google/Bing dependent) with less SEO spam and faster responses. +### Step C — Execute the indicated fetch -### Exa Semantic Search (MCP) +For each item where `fetch` is present, invoke the MCP tool matching `fetch.provider`: -Check `exa_search` from init context. If `true`, use Exa for semantic, research-heavy queries: +| provider id | MCP tool / built-in | +|-------------|---------------------| +| `context7` | `mcp__context7__resolve-library-id` then `mcp__context7__query-docs` | +| `ref` | `mcp__ref__*` (use the appropriate ref MCP tool for the query) | +| `jina` | `mcp__jina__*` (use the appropriate jina MCP tool for the query) | +| `exa` | `mcp__exa__web_search_exa` with `fetch.query` | +| `tavily` | `mcp__tavily__search` with `fetch.query` | +| `perplexity` | `mcp__perplexity__*` (use the appropriate perplexity MCP tool for the query) | +| `brave` | `gsd-tools query websearch ""` (Brave-backed) or built-in `WebSearch` | +| `firecrawl` | `mcp__firecrawl__scrape` with url (scrape kind) or `mcp__firecrawl__search` | +| `websearch` | built-in `WebSearch` tool | +| `webfetch` | built-in `WebFetch` tool | -``` -mcp__exa__web_search_exa with query: "your semantic query" +For any other provider id `X` not listed above: use `mcp__X__*` if available, else fall back to `WebSearch`. + +**WebSearch tip:** Do not inject a year into queries — it biases results toward stale dated content; check publication dates on the results you read instead. + +### Step D — Cache each digest + +After digesting a source, persist it so future runs can reuse it: + +```bash +gsd-tools query research-store put \ + --content "" \ + --source \ + --provider \ + --confidence \ + --kind ``` -**Best for:** Research questions where keyword search fails — "best approaches to X", finding technical/academic content, discovering niche libraries. Returns semantically relevant results. - -If `exa_search: false` (or not set), fall back to WebSearch or Brave Search. - -### Firecrawl Deep Scraping (MCP) - -Check `firecrawl` from init context. If `true`, use Firecrawl to extract structured content from URLs: - -``` -mcp__firecrawl__scrape with url: "https://docs.example.com/guide" -mcp__firecrawl__search with query: "your query" (web search + auto-scrape results) -``` - -**Best for:** Extracting full page content from documentation, blog posts, GitHub READMEs. Use after finding a URL from Exa, WebSearch, or known docs. Returns clean markdown. - -If `firecrawl: false` (or not set), fall back to WebFetch. - -## Verification Protocol - -**Verify every WebSearch finding:** - -``` -For each WebSearch finding: -1. Can I verify with Context7? → YES: HIGH confidence -2. Can I verify with official docs? → YES: MEDIUM confidence -3. Do multiple sources agree? → YES: Increase one level -4. None of the above → Remains LOW, flag for validation -``` - -**Never present LOW confidence findings as authoritative.** +`key` comes from the `research-plan` item. `confidence` comes from the classify-confidence seam (see ``). -| Level | Sources | Use | -|-------|---------|-----| -| HIGH | Context7, official docs, official releases | State as fact | -| MEDIUM | WebSearch verified with official source, multiple credible sources | State with attribution | -| LOW | WebSearch only, single source, unverified | Flag as needing validation | +Obtain the confidence tier from code — do not hard-code tiers in your reasoning: -Priority: Context7 > Exa (verified) > Firecrawl (official docs) > Official GitHub > Brave/WebSearch (verified) > WebSearch (unverified) +```bash +gsd-tools query classify-confidence --provider +# for cross-checked findings, add --verified: +gsd-tools query classify-confidence --provider --verified +``` + +Returns `HIGH`, `MEDIUM`, or `LOW`. Use that value when tagging claims and when calling `research-store put --confidence `. + +Keep using the provenance tags in RESEARCH.md: +- `[VERIFIED: source]` — confirmed via tool AND from an authoritative source (HIGH confidence) +- `[CITED: url]` — referenced from official documentation (MEDIUM confidence) +- `[ASSUMED]` — training knowledge, not verified this session (LOW confidence) + +**Never present LOW confidence findings as authoritative.** +@~/.claude/gsd-core/references/research-verification-protocol.md -## Known Pitfalls - -### Configuration Scope Blindness -**Trap:** Assuming global configuration means no project-scoping exists -**Prevention:** Verify ALL configuration scopes (global, project, local, workspace) - -### Deprecated Features -**Trap:** Finding old documentation and concluding feature doesn't exist -**Prevention:** Check current official docs, review changelog, verify version numbers and dates - -### Negative Claims Without Evidence -**Trap:** Making definitive "X is not possible" statements without official verification -**Prevention:** For any negative claim — is it verified by official docs? Have you checked recent updates? Are you confusing "didn't find it" with "doesn't exist"? - -### Single Source Reliance -**Trap:** Relying on a single source for critical claims -**Prevention:** Require multiple sources: official docs (primary), release notes (currency), additional source (verification) - -## Pre-Submission Checklist - -- [ ] All domains investigated (stack, patterns, pitfalls) -- [ ] Negative claims verified with official docs -- [ ] Multiple sources cross-referenced for critical claims -- [ ] URLs provided for authoritative sources -- [ ] Publication dates checked (prefer recent/current) -- [ ] Confidence levels assigned honestly -- [ ] "What might I have missed?" review completed - [ ] **If rename/refactor phase:** Runtime State Inventory completed — all 5 categories answered explicitly (not left blank) - [ ] Security domain included (or `security_enforcement: false` confirmed) - [ ] ASVS categories verified against phase tech stack @@ -269,30 +194,30 @@ Priority: Context7 > Exa (verified) > Firecrawl (official docs) > Official GitHu Every phase that installs external packages **must** run the following verification before emitting the `## Package Legitimacy Audit` section in RESEARCH.md. -### Step 1 — Install slopcheck (best-effort) +### Step 1 — Run legitimacy check via seam ```bash -pip install slopcheck --break-system-packages 2>/dev/null || pip install slopcheck 2>/dev/null || true +gsd-tools query package-legitimacy check --ecosystem ... ``` -### Step 2 — Run legitimacy check +Returns a JSON array of per-package verdicts: -```bash -if command -v slopcheck &>/dev/null; then - slopcheck install ... --json -else - echo "slopcheck not available — marking all packages [ASSUMED]" -fi +```json +[ + { "name": "pkg1", "verdict": "OK", "signals": { ... }, "reasons": [] }, + { "name": "pkg2", "verdict": "SUS", "signals": { ... }, "reasons": ["low downloads"] }, + { "name": "pkg3", "verdict": "SLOP", "signals": { ... }, "reasons": ["not found on registry"] } +] ``` -**Interpreting results:** -- `[SLOP]` — hallucinated or dangerously new package. **Remove entirely** from all RESEARCH.md recommendations. List in audit table under `Disposition: REMOVED`. -- `[SUS]` — suspicious (new, low-downloads, or no source repo). **Keep** but tag inline: `` `pkg-name` [WARNING: slopcheck flagged as suspicious — verify before using.] `` -- `[OK]` — clean. Proceed normally. +**Interpreting verdicts:** +- `SLOP` — hallucinated or dangerously new package. **Remove entirely** from all RESEARCH.md recommendations. List in audit table under `Disposition: REMOVED`. +- `SUS` — suspicious (new, low-downloads, or no source repo). **Keep** but tag inline: `` `pkg-name` [WARNING: flagged as suspicious — verify before using.] `` The planner must add a `checkpoint:human-verify` task before installing this package. +- `OK` — clean. Proceed normally. -**Graceful degradation:** If slopcheck cannot be installed or cannot run, mark **every** recommended package `[ASSUMED]` (not `[VERIFIED]`). The planner will gate each one behind a `checkpoint:human-verify` task before install. This is strictly safer than the current baseline — never a hard failure. +Packages discovered via WebSearch or training data and not yet verified must be tagged `[ASSUMED]` regardless of registry existence (a slopsquatted package also passes registry lookup). -### Step 3 — Ecosystem-specific registry verification +### Step 2 — Ecosystem-specific registry verification Run the appropriate command for the phase's primary language: @@ -310,14 +235,14 @@ cargo search Cross-ecosystem confusion (a Python package name that exists on npm but not PyPI) is a documented hallucination vector (~9% rate). Always verify on the correct ecosystem registry. -### Step 4 — Check for suspicious postinstall scripts (Node.js phases) +### Step 3 — Check for suspicious postinstall scripts (Node.js phases) ```bash npm view scripts.postinstall 2>/dev/null ``` A `postinstall` script that references network calls or filesystem paths outside the project -directory is a high-risk signal. Flag such packages `[SUS]` even if slopcheck rates them `[OK]`. +directory is a high-risk signal. Flag such packages `[SUS]` even if the seam rates them `[OK]`. @@ -380,16 +305,16 @@ Document the verified version and publish date. Training data versions may be mo > **Required** whenever this phase installs external packages. Run the Package Legitimacy Gate protocol before completing this section. -| Package | Registry | Age | Downloads | Source Repo | slopcheck | Disposition | -|---------|----------|-----|-----------|-------------|-----------|-------------| +| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition | +|---------|----------|-----|-----------|-------------|---------|-------------| | [name] | npm/PyPI/crates | [e.g., 8 yrs] | [e.g., 50M/wk] | [github.com/org/repo or "none"] | [OK] | Approved | | [name] | npm | [e.g., 3 days] | [e.g., 0] | none | [SLOP] | REMOVED | | [name] | npm | [e.g., 2 mo] | [e.g., 800/wk] | [github.com/…] | [SUS] | Flagged — planner must add checkpoint | -**Packages removed due to slopcheck [SLOP] verdict:** [list, or "none"] +**Packages removed due to [SLOP] verdict:** [list, or "none"] **Packages flagged as suspicious [SUS]:** [list — planner inserts checkpoint:human-verify before each install] -*If slopcheck was unavailable at research time, all packages above are tagged `[ASSUMED]` and the planner must gate each install behind a `checkpoint:human-verify` task.* +*Packages discovered via WebSearch or training data that have not been verified against an authoritative source are tagged `[ASSUMED]` and the planner must gate each install behind a `checkpoint:human-verify` task.* ## Architecture Patterns @@ -632,7 +557,8 @@ ls .planning/graphs/graph.json 2>/dev/null If graph.json exists, check freshness: ```bash -node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify status +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi +gsd_run graphify status ``` If the status response has `stale: true`, note for later: "Graph is {age_hours}h old -- treat semantic relationships as approximate." Include this annotation inline with any graph context injected below. @@ -640,7 +566,7 @@ If the status response has `stale: true`, note for later: "Graph is {age_hours}h Query the graph for each major capability in the phase scope (2-3 queries per D-05, discovery-focused): ```bash -node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify query "" --budget 1500 +gsd_run graphify query "" --budget 1500 ``` Derive query terms from the phase goal and requirement descriptions. Examples: @@ -777,7 +703,7 @@ docker info 2>/dev/null | head -3 ## Step 3: Execute Research Protocol -For each domain: Context7 first → Official docs → WebSearch → Cross-verify. Document findings with confidence levels as you go. +For each domain, use the `` seam (Steps A–D): build questions JSON, call `gsd-tools query research-plan`, run the indicated provider per item, then cache each digest. Document findings with confidence levels as you go (use `gsd-tools query classify-confidence --provider ` to obtain the tier). ## Step 4: Validation Architecture Research (if nyquist_validation enabled) @@ -924,7 +850,7 @@ Research is complete when: - [ ] Common pitfalls catalogued - [ ] Environment availability audited (or skipped with reason) - [ ] Code examples provided -- [ ] Source hierarchy followed (Context7 → Official → WebSearch) +- [ ] Source hierarchy followed (research-plan seam determines provider order; classify-confidence seam determines tiers) - [ ] All findings have confidence levels - [ ] RESEARCH.md created in correct format - [ ] RESEARCH.md committed to git diff --git a/agents/gsd-planner.md b/agents/gsd-planner.md index 135ae1494..af3d0daf8 100644 --- a/agents/gsd-planner.md +++ b/agents/gsd-planner.md @@ -309,7 +309,7 @@ Exceptions where `tdd="true"` is not needed: `type="checkpoint:*"` tasks, config ## MVP Mode Detection -**When `MVP_MODE` is enabled (passed by the plan-phase orchestrator):** Decompose tasks as **vertical feature slices**, not horizontal layers. Required reading: `@~/.claude/gsd-core/references/planner-mvp-mode.md` (loaded conditionally by the orchestrator). +**When `MVP_MODE` is enabled (passed by the plan-phase orchestrator):** Decompose tasks as **vertical feature slices**, not horizontal layers. Required reading: Read `~/.claude/gsd-core/references/planner-mvp-mode.md` for the vertical-slice rules (lazy — only on MVP runs). **Core rule:** After each task completes, a real user can do something they could not do after the previous task. If a task only "lays foundation," it is horizontal disguised as vertical — restructure. @@ -323,7 +323,7 @@ Exceptions where `tdd="true"` is not needed: `type="checkpoint:*"` tasks, config **As a** [user role], **I want to** [capability], **so that** [outcome]. ``` - Format rules from `@~/.claude/gsd-core/references/user-story-template.md`: + Format rules (Read `~/.claude/gsd-core/references/user-story-template.md`): - All three slots required. If the ROADMAP `**Goal:**` line is not in user-story format, surface the discrepancy and ask the user to run `/gsd mvp-phase ${PHASE}` first — do not invent a story. - Bold the three keywords (`**As a**`, `**I want to**`, `**so that**`) when emitting to PLAN.md. The ROADMAP form does not use bolded keywords; the PLAN form does. 2. First task: failing end-to-end test for the happy path. @@ -332,7 +332,7 @@ Exceptions where `tdd="true"` is not needed: `type="checkpoint:*"` tasks, config **Mode is all-or-nothing per phase** (PRD decision Q1). Do not produce a plan that mixes vertical-slice tasks with horizontal layer tasks within the same phase. -**Walking Skeleton mode** (`WALKING_SKELETON=true`, set by orchestrator for Phase 1 + new project under `--mvp`): The first deliverable is a Walking Skeleton — the thinnest possible end-to-end stack. In addition to `PLAN.md`, produce `SKELETON.md` using the template at `@~/.claude/gsd-core/references/skeleton-template.md`. `SKELETON.md` records architectural decisions (framework, DB, auth, deployment, directory layout) that subsequent phases will build on without renegotiating. +**Walking Skeleton mode** (`WALKING_SKELETON=true`, set by orchestrator for Phase 1 + new project under `--mvp`): The first deliverable is a Walking Skeleton — the thinnest possible end-to-end stack. In addition to `PLAN.md`, produce `SKELETON.md` using the template at `~/.claude/gsd-core/references/skeleton-template.md` (Read it now). `SKELETON.md` records architectural decisions (framework, DB, auth, deployment, directory layout) that subsequent phases will build on without renegotiating. **Compatibility with TDD detection:** When both `MVP_MODE=true` and `workflow.tdd_mode=true`, every behavior-adding task uses `tdd="true"` and a `` block, AND the task ordering follows the vertical-slice structure above. The first task is always a failing end-to-end test. @@ -407,6 +407,8 @@ Plans should complete within ~50% context (not 80%). No context anxiety, quality ## Granularity Calibration +The resolved granularity is provided in the planning context as `**Granularity:** `. Read that value and apply the corresponding row below. When no explicit value is present, default to Standard. + | Granularity | Typical Plans/Phase | Tasks/Plan | |-------------|---------------------|------------| | Coarse | 1-3 | 2-3 | @@ -818,39 +820,10 @@ If exists, load relevant documents by phase type: -Check for knowledge graph: - -```bash -ls .planning/graphs/graph.json 2>/dev/null -``` - -If graph.json exists, check freshness: - -```bash -node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify status -``` - -If the status response has `stale: true`, note for later: "Graph is {age_hours}h old -- treat semantic relationships as approximate." Include this annotation inline with any graph context injected below. - -Query the graph for phase-relevant dependency context (single query per D-06): - -```bash -node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify query "" --budget 2000 -``` - -(graphify is not exposed on `gsd-tools query` yet; use `gsd-tools.cjs` for graphify only.) - -Use the keyword that best captures the phase goal. Examples: -- Phase "User Authentication" -> query term "auth" -- Phase "Payment Integration" -> query term "payment" -- Phase "Database Migration" -> query term "migration" - -If the query returns nodes and edges, incorporate as dependency context for planning: -- Which modules/files are semantically related to this phase's domain -- Which subsystems may be affected by changes in this phase -- Cross-document relationships that inform task ordering and wave structure - -If no results or graph.json absent, continue without graph context. +Read `gsd-core/references/planner-load-graph-context.md` and execute it. It checks for a +knowledge graph and, if `.planning/graphs/graph.json` exists, reads freshness and +phase-relevant dependency context via the `gsd_run` launcher and incorporates the results +into planning. If the graph is absent, skip and continue without graph context. diff --git a/agents/gsd-project-researcher.md b/agents/gsd-project-researcher.md index 75135ff99..2c79ece7a 100644 --- a/agents/gsd-project-researcher.md +++ b/agents/gsd-project-researcher.md @@ -1,7 +1,7 @@ --- name: gsd-project-researcher description: Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators. -tools: Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__* +tools: Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__* color: cyan # hooks: # PostToolUse: @@ -33,53 +33,11 @@ Your files feed the roadmap: -When you need library or framework documentation, check in this order: - -1. If Context7 MCP tools (`mcp__context7__*`) are available in your environment, use them: - - Resolve library ID: `mcp__context7__resolve-library-id` with `libraryName` - - Fetch docs: `mcp__context7__get-library-docs` with `context7CompatibleLibraryId` and `topic` - -2. If Context7 MCP is not available (upstream bug anthropics/claude-code#13898 strips MCP - tools from agents with a `tools:` frontmatter restriction), use the CLI fallback via Bash: - - Step 1 — Resolve library ID: - ```bash - npx --yes ctx7@latest library "" - ``` - Step 2 — Fetch documentation: - ```bash - npx --yes ctx7@latest docs "" - ``` - -Do not skip documentation lookups because MCP tools are unavailable — the CLI fallback -works via Bash and produces equivalent output. +@~/.claude/gsd-core/references/research-documentation-lookup.md - -## Training Data = Hypothesis - -Claude's training is 6-18 months stale. Knowledge may be outdated, incomplete, or wrong. - -**Discipline:** -1. **Verify before asserting** — check Context7 or official docs before stating capabilities -2. **Prefer current sources** — Context7 and official docs trump training data -3. **Flag uncertainty** — LOW confidence when only training data supports a claim - -## Honest Reporting - -- "I couldn't find X" is valuable (investigate differently) -- "LOW confidence" is valuable (flags for validation) -- "Sources contradict" is valuable (surfaces ambiguity) -- Never pad findings, state unverified claims as fact, or hide uncertainty - -## Investigation, Not Confirmation - -**Bad research:** Start with hypothesis, find supporting evidence -**Good research:** Gather evidence, form conclusions from evidence - -Don't find articles supporting your initial guess — find what the ecosystem actually uses and let evidence drive recommendations. - +@~/.claude/gsd-core/references/research-philosophy.md @@ -94,132 +52,95 @@ Don't find articles supporting your initial guess — find what the ecosystem ac -## Tool Priority Order +## Research Plan via Code Seam -### 1. Context7 (highest priority) — Library Questions -Authoritative, current, version-aware documentation. +The agent decides **what** to research (the questions). The seam decides **which provider** to use and manages caching. -``` -1. mcp__context7__resolve-library-id with libraryName: "[library]" -2. mcp__context7__query-docs with libraryId: [resolved ID], query: "[question]" +### Step A — Build a research-plan input file + +Construct a JSON file at a temp path (e.g. `/tmp/research-plan-input.json`): + +```json +{ + "ecosystem": "", + "config": { "exa_search": true/false, "brave_search": true/false, "firecrawl": true/false, "tavily_search": true/false }, + "questions": [ + { "text": "How does X work?", "kind": "docs", "library": "x", "version": "1.2.3" }, + { "text": "Best practices for Y?", "kind": "web" } + ] +} ``` -Resolve first (don't guess IDs). Use specific queries. Trust over training data. +`config` comes from the init context (availability flags). `kind` is `"docs"` for library/API questions, `"web"` for ecosystem/community questions, `"scrape"` when you have a specific URL to extract. -### 2. Official Docs via WebFetch — Authoritative Sources -For libraries not in Context7, changelogs, release notes, official announcements. - -Use exact URLs (not search result pages). Check publication dates. Prefer /docs/ over marketing. - -### 3. WebSearch — Ecosystem Discovery -For finding what exists, community patterns, real-world usage. - -**Query templates:** -``` -Ecosystem: "[tech] best practices", "[tech] recommended libraries" -Patterns: "how to build [type] with [tech]", "[tech] architecture patterns" -Problems: "[tech] common mistakes", "[tech] gotchas" -``` - -Use multiple query variations. Mark WebSearch-only findings as LOW confidence. Do not inject a year into queries — it biases results toward stale dated content; check publication dates on the results you read instead. - -### Enhanced Web Search (Brave API) - -Check `brave_search` from orchestrator context. If `true`, use Brave Search for higher quality results: +### Step B — Obtain the fetch plan ```bash -gsd-tools query websearch "your query" --limit 10 +gsd-tools query research-plan --input /tmp/research-plan-input.json ``` -**Options:** -- `--limit N` — Number of results (default: 10) -- `--freshness day|week|month` — Restrict to recent content +Returns `{ "items": [ { "question": "...", "key": "", "cache": { "hit": true/false, "stale": false }, "fetch": { "provider": "context7", "query": "..." } } ] }`. -If `brave_search: false` (or not set), use built-in WebSearch tool instead. +- `cache.hit && !cache.stale` → reuse the cached digest; no fetch needed. +- `cache.hit && cache.stale` → fetch anyway to refresh; the old entry is returned as a fallback. +- no `cache` field → cache miss; must fetch. -Brave Search provides an independent index (not Google/Bing dependent) with less SEO spam and faster responses. +### Step C — Execute the indicated fetch -### Exa Semantic Search (MCP) +For each item where `fetch` is present, invoke the MCP tool matching `fetch.provider`: -Check `exa_search` from orchestrator context. If `true`, use Exa for research-heavy, semantic queries: +| provider id | MCP tool / built-in | +|-------------|---------------------| +| `context7` | `mcp__context7__resolve-library-id` then `mcp__context7__query-docs` | +| `ref` | `mcp__ref__*` (use the appropriate ref MCP tool for the query) | +| `jina` | `mcp__jina__*` (use the appropriate jina MCP tool for the query) | +| `exa` | `mcp__exa__web_search_exa` with `fetch.query` | +| `tavily` | `mcp__tavily__search` with `fetch.query` | +| `perplexity` | `mcp__perplexity__*` (use the appropriate perplexity MCP tool for the query) | +| `brave` | `gsd-tools query websearch ""` (Brave-backed) or built-in `WebSearch` | +| `firecrawl` | `mcp__firecrawl__scrape` with url (scrape kind) or `mcp__firecrawl__search` | +| `websearch` | built-in `WebSearch` tool | +| `webfetch` | built-in `WebFetch` tool | -``` -mcp__exa__web_search_exa with query: "your semantic query" +For any other provider id `X` not listed above: use `mcp__X__*` if available, else fall back to `WebSearch`. + +**WebSearch tip:** Do not inject a year into queries — it biases results toward stale dated content; check publication dates on the results you read instead. + +### Step D — Cache each digest + +After digesting a source, persist it so future runs can reuse it: + +```bash +gsd-tools query research-store put \ + --content "" \ + --source \ + --provider \ + --confidence \ + --kind ``` -**Best for:** Research questions where keyword search fails — "best approaches to X", finding technical/academic content, discovering niche libraries, ecosystem exploration. Returns semantically relevant results rather than keyword matches. - -If `exa_search: false` (or not set), fall back to WebSearch or Brave Search. - -### Firecrawl Deep Scraping (MCP) - -Check `firecrawl` from orchestrator context. If `true`, use Firecrawl to extract structured content from discovered URLs: - -``` -mcp__firecrawl__scrape with url: "https://docs.example.com/guide" -mcp__firecrawl__search with query: "your query" (web search + auto-scrape results) -``` - -**Best for:** Extracting full page content from documentation, blog posts, GitHub READMEs, comparison articles. Use after finding a relevant URL from Exa, WebSearch, or known docs. Returns clean markdown instead of raw HTML. - -If `firecrawl: false` (or not set), fall back to WebFetch. - -## Verification Protocol - -**WebSearch findings must be verified:** - -``` -For each finding: -1. Verify with Context7? YES → HIGH confidence -2. Verify with official docs? YES → MEDIUM confidence -3. Multiple sources agree? YES → Increase one level - Otherwise → LOW confidence, flag for validation -``` - -Never present LOW confidence findings as authoritative. - -## Confidence Levels - -| Level | Sources | Use | -|-------|---------|-----| -| HIGH | Context7, official documentation, official releases | State as fact | -| MEDIUM | WebSearch verified with official source, multiple credible sources agree | State with attribution | -| LOW | WebSearch only, single source, unverified | Flag as needing validation | - -**Source priority:** Context7 → Exa (verified) → Firecrawl (official docs) → Official GitHub → Brave/WebSearch (verified) → WebSearch (unverified) +`key` comes from the `research-plan` item. `confidence` comes from the classify-confidence seam (see ``). + + +Obtain the confidence tier from code — do not hard-code tiers in your reasoning: + +```bash +gsd-tools query classify-confidence --provider +# for cross-checked findings, add --verified: +gsd-tools query classify-confidence --provider --verified +``` + +Returns `HIGH`, `MEDIUM`, or `LOW`. Use that value when tagging claims and when calling `research-store put --confidence `. + +**Never present LOW confidence findings as authoritative.** + + + - -## Research Pitfalls - -### Configuration Scope Blindness -**Trap:** Assuming global config means no project-scoping exists -**Prevention:** Verify ALL scopes (global, project, local, workspace) - -### Deprecated Features -**Trap:** Old docs → concluding feature doesn't exist -**Prevention:** Check current docs, changelog, version numbers - -### Negative Claims Without Evidence -**Trap:** Definitive "X is not possible" without official verification -**Prevention:** Is this in official docs? Checked recent updates? "Didn't find" ≠ "doesn't exist" - -### Single Source Reliance -**Trap:** One source for critical claims -**Prevention:** Require official docs + release notes + additional source - -## Pre-Submission Checklist - -- [ ] All domains investigated (stack, features, architecture, pitfalls) -- [ ] Negative claims verified with official docs -- [ ] Multiple sources for critical claims -- [ ] URLs provided for authoritative sources -- [ ] Publication dates checked (prefer recent/current) -- [ ] Confidence levels assigned honestly -- [ ] "What might I have missed?" review completed - +@~/.claude/gsd-core/references/research-verification-protocol.md @@ -564,7 +485,7 @@ Orchestrator provides: project name/description, research mode, project context, ## Step 3: Execute Research -For each domain: Context7 → Official Docs → WebSearch → Verify. Document with confidence levels. +For each domain, use the `` seam (Steps A–D): build questions JSON, call `gsd-tools query research-plan`, run the indicated provider per item, then cache each digest. Document findings with confidence levels as you go (use `gsd-tools query classify-confidence --provider ` to obtain the tier). ## Step 4: Quality Check @@ -678,7 +599,7 @@ Research is complete when: - [ ] Feature landscape mapped (table stakes, differentiators, anti-features) - [ ] Architecture patterns documented - [ ] Domain pitfalls catalogued -- [ ] Source hierarchy followed (Context7 → Official → WebSearch) +- [ ] Source hierarchy followed (research-plan seam determines provider order; classify-confidence seam determines tiers) - [ ] All findings have confidence levels - [ ] Output files created in `.planning/research/` - [ ] SUMMARY.md includes roadmap implications diff --git a/agents/gsd-security-auditor.md b/agents/gsd-security-auditor.md index 31847360f..209ce4683 100644 --- a/agents/gsd-security-auditor.md +++ b/agents/gsd-security-auditor.md @@ -8,7 +8,7 @@ tools: - Bash - Glob - Grep -color: "#EF4444" +color: red --- diff --git a/agents/gsd-ui-auditor.md b/agents/gsd-ui-auditor.md index 2fd1b1153..3ed6cc9e6 100644 --- a/agents/gsd-ui-auditor.md +++ b/agents/gsd-ui-auditor.md @@ -2,7 +2,7 @@ name: gsd-ui-auditor description: Retroactive 6-pillar visual audit of implemented frontend code. Produces scored UI-REVIEW.md. Spawned by /gsd:ui-review orchestrator. tools: Read, Write, Bash, Grep, Glob -color: "#F472B6" +color: pink # hooks: # PostToolUse: # - matcher: "Write|Edit" diff --git a/agents/gsd-ui-checker.md b/agents/gsd-ui-checker.md index 2ed509e78..adb0e67a3 100644 --- a/agents/gsd-ui-checker.md +++ b/agents/gsd-ui-checker.md @@ -2,7 +2,7 @@ name: gsd-ui-checker description: Validates UI-SPEC.md design contracts against 6 quality dimensions. Produces BLOCK/FLAG/PASS verdicts. Spawned by /gsd:ui-phase orchestrator. tools: Read, Bash, Glob, Grep -color: "#22D3EE" +color: cyan --- diff --git a/agents/gsd-ui-researcher.md b/agents/gsd-ui-researcher.md index d168fdebb..63a7d6f84 100644 --- a/agents/gsd-ui-researcher.md +++ b/agents/gsd-ui-researcher.md @@ -1,8 +1,8 @@ --- name: gsd-ui-researcher description: Produces UI-SPEC.md design contract for frontend phases. Reads upstream artifacts, detects design system state, asks only unanswered questions. Spawned by /gsd:ui-phase orchestrator. -tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__* -color: "#E879F9" +tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__* +color: purple # hooks: # PostToolUse: # - matcher: "Write|Edit" @@ -28,26 +28,7 @@ If the prompt contains a `` block, you MUST use the `Read` too -When you need library or framework documentation, check in this order: - -1. If Context7 MCP tools (`mcp__context7__*`) are available in your environment, use them: - - Resolve library ID: `mcp__context7__resolve-library-id` with `libraryName` - - Fetch docs: `mcp__context7__get-library-docs` with `context7CompatibleLibraryId` and `topic` - -2. If Context7 MCP is not available (upstream bug anthropics/claude-code#13898 strips MCP - tools from agents with a `tools:` frontmatter restriction), use the CLI fallback via Bash: - - Step 1 — Resolve library ID: - ```bash - npx --yes ctx7@latest library "" - ``` - Step 2 — Fetch documentation: - ```bash - npx --yes ctx7@latest docs "" - ``` - -Do not skip documentation lookups because MCP tools are unavailable — the CLI fallback -works via Bash and produces equivalent output. +@~/.claude/gsd-core/references/research-documentation-lookup.md diff --git a/agents/gsd-user-profiler.md b/agents/gsd-user-profiler.md index 6e427fe94..82188804f 100644 --- a/agents/gsd-user-profiler.md +++ b/agents/gsd-user-profiler.md @@ -2,7 +2,7 @@ name: gsd-user-profiler description: Analyzes extracted session messages across 8 behavioral dimensions to produce a scored developer profile with confidence levels and evidence. Spawned by profile orchestration workflows. tools: Read -color: magenta +color: purple --- diff --git a/agents/gsd-verifier.md b/agents/gsd-verifier.md index 07d149d2c..2b2641b4e 100644 --- a/agents/gsd-verifier.md +++ b/agents/gsd-verifier.md @@ -466,8 +466,11 @@ ls $BUILD_OUTPUT_DIR/*.{js,css} 2>/dev/null | wc -l # Module exports expected functions node -e "const m = require('$MODULE_PATH'); console.log(typeof m.$FUNCTION_NAME)" 2>/dev/null | grep -q "function" -# Test suite passes (if tests exist for this phase's code) -npm test -- --grep "$PHASE_TEST_PATTERN" 2>&1 | grep -q "passing" +# A test EXISTS (existence proof — enumerate, do NOT run the suite) +cargo test -- --list 2>/dev/null | grep -q "$PHASE_TEST_PATTERN" # pytest --collect-only -q · npx vitest list · go test -list '.*' + +# A specific test PASSES (run ONE named test, never the whole suite) +cargo test "$TEST_NAME" -- --exact # pytest -k "$TEST_NAME" · npx vitest run -t "$TEST_NAME" ``` 2. **Run each check** and record pass/fail: @@ -487,6 +490,7 @@ npm test -- --grep "$PHASE_TEST_PATTERN" 2>&1 | grep -q "passing" - Each check must complete in under 10 seconds - Do not start servers or services — only test what's already runnable - Do not modify state (no writes, no mutations, no side effects) +- **Run the full workspace test command at most once per verification.** Never filter a full run per must-have (` 2>&1 | grep X` repeated per truth) — it re-runs everything and yields no new evidence. Prove a test exists by enumeration (`--list` / `--collect-only`); prove one passes via a single named test. If a full run is genuinely required, run it once and `grep` the saved output. - If the project has no runnable entry points yet, skip with: "Step 7b: SKIPPED (no runnable entry points)" ## Step 7c: Probe Execution @@ -572,6 +576,8 @@ Classify status using this decision tree IN ORDER (most restrictive first): **passed is ONLY valid when the human verification section is empty.** If you identified items requiring human testing in Step 8, status MUST be human_needed. +> **Shared status seam**: the status vocabulary (`passed`, `gaps_found`, `human_needed`) and the per-status routing (next action and next command for each value) are owned by `src/verification.cts` via `gsd_run query verification.status`. This agent is the single emitter of the frontmatter status field; consumers (ship.md, execute-phase.md) read routing from that query instead of re-deriving it. + **Score:** `verified_truths / total_truths` ## Step 9b: Filter Deferred Items diff --git a/bin/install.js b/bin/install.js index abd7a1a35..0ae9bdcf6 100755 --- a/bin/install.js +++ b/bin/install.js @@ -30,7 +30,13 @@ const { } = require(path.join(__dirname, '..', 'scripts', 'fix-slash-commands.cjs')); const { resolveAntigravityGlobalDir, + getGlobalConfigDir, } = require('../gsd-core/bin/lib/runtime-homes.cjs'); +const { + applyWorktreeBaseRef, + readBaseRefFromSettings, +} = require('../gsd-core/bin/lib/worktree-base-ref.cjs'); +const { resolveRuntimeConfigIntent } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs'); /** * Runtimes that register hyphen-form `name:` per #2808 AND copy agent bodies @@ -91,10 +97,112 @@ function isCodexHooksFeatureKey(key) { return CODEX_HOOKS_FEATURE_ALL_KEYS.includes(key); } +// #768 \u2014 Claude Code permissions.allow / permissions.deny entries. +// Pre-populated during Claude installs to eliminate first-run approval friction +// for gsd-core's own known-safe tool calls, and to add defense-in-depth deny +// entries for common credential files. +// +// Format: each string uses Claude Code's documented permission rule syntax \u2014 +// "Tool(pattern)" e.g. "Bash(npx gsd-core *)", "Read(.planning/*)" +// "Tool" (bare tool name, no pattern) +// +// Merge policy: additive, non-destructive \u2014 existing user entries are preserved; +// GSD entries are appended only when not already present (idempotent). +const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([ + 'Bash(npx gsd-core *)', + 'Read(.planning/*)', + 'Write(.planning/*)', + 'Read(STATE.md)', + 'Write(STATE.md)', +]); +const GSD_CLAUDE_DENY_PERMISSIONS = Object.freeze([ + 'Read(.env)', + 'Read(.env.*)', + 'Read(.secrets)', +]); + +/** + * Merge GSD-owned permission entries into a Claude Code settings object. + * + * Additive and idempotent: existing allow/deny entries are preserved; GSD + * entries are appended only if not already present. No other permission sub-keys + * (ask, disableBypassPermissionsMode, etc.) are touched. + * + * Defensive: if settings is not a plain object, returns immediately without + * throwing. If permissions.allow / permissions.deny exist but are not arrays + * (malformed settings), they are replaced with valid arrays. + * + * @param {object} settings - The parsed settings.json object to mutate in-place. + */ +function mergeClaudePermissions(settings) { + if (settings === null || typeof settings !== 'object' || Array.isArray(settings)) return; + + if (!settings.permissions || typeof settings.permissions !== 'object' || Array.isArray(settings.permissions)) { + settings.permissions = {}; + } + + if (!Array.isArray(settings.permissions.allow)) { + settings.permissions.allow = []; + } + if (!Array.isArray(settings.permissions.deny)) { + settings.permissions.deny = []; + } + + for (const entry of GSD_CLAUDE_ALLOW_PERMISSIONS) { + if (!settings.permissions.allow.includes(entry)) { + settings.permissions.allow.push(entry); + } + } + for (const entry of GSD_CLAUDE_DENY_PERMISSIONS) { + if (!settings.permissions.deny.includes(entry)) { + settings.permissions.deny.push(entry); + } + } +} + // Copilot instructions marker constants const GSD_COPILOT_INSTRUCTIONS_MARKER = ''; const GSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER = ''; +// #786 \u2014 GitHub Copilot CLI lifecycle hook constants. +// Copilot reads hook configs from /hooks/*.json (repo scope: .github/hooks/, +// user scope: ~/.copilot/hooks/) with the shape { version, hooks: { : [...] } }. +// Events use camelCase (sessionStart, preToolUse, postToolUse, ...). A `command` +// hook runs an INLINE shell command (bash / powershell), so the GSD hook is fully +// self-contained \u2014 there is no separate hook script to install, and therefore +// nothing that can dangle if a script copy is skipped. See +// https://docs.github.com/en/copilot/reference/hooks-configuration +const GSD_COPILOT_HOOK_FILE = 'gsd-session.json'; +// Copilot parses a command hook's stdout as the hook-output JSON. For sessionStart +// the schema is `{ additionalContext?: string }` (the text is prepended to the +// session as context). So the hook must emit that JSON envelope — not bare text. +// The two messages contain no JSON-special characters, so they embed verbatim. +const GSD_COPILOT_SESSION_MSG_PRESENT = + 'GSD: .planning/STATE.md present - review the current phase and any blockers before acting.'; +const GSD_COPILOT_SESSION_MSG_ABSENT = + 'GSD: no .planning/ workflow found - run /gsd-new-project to start a tracked workflow.'; +const GSD_COPILOT_SESSION_HOOK_BASH = + 'if [ -f .planning/STATE.md ]; then ' + + `printf '%s' '{"additionalContext":"${GSD_COPILOT_SESSION_MSG_PRESENT}"}'; else ` + + `printf '%s' '{"additionalContext":"${GSD_COPILOT_SESSION_MSG_ABSENT}"}'; fi`; +const GSD_COPILOT_SESSION_HOOK_PWSH = + 'if (Test-Path .planning/STATE.md) ' + + `{ '{"additionalContext":"${GSD_COPILOT_SESSION_MSG_PRESENT}"}' } ` + + `else { '{"additionalContext":"${GSD_COPILOT_SESSION_MSG_ABSENT}"}' }`; + +// #777 — Cursor CLI lifecycle hook constants. +// Cursor reads hook configs from /.cursor/hooks.json (local) or +// ~/.cursor/hooks.json (global) with the shape { version: 1, hooks: { : [...] } }. +// Events use camelCase: sessionStart, postToolUse, preToolUse, etc. +// A `command` hook entry runs an external script. GSD registers two managed hooks: +// sessionStart → gsd-cursor-session-start.js (context injection) +// postToolUse → gsd-cursor-post-tool.js (STATE.md update monitor) +// Cursor docs: https://cursor.com/docs/hooks +const GSD_CURSOR_SESSION_HOOK_SCRIPT = 'gsd-cursor-session-start.js'; +const GSD_CURSOR_POST_TOOL_HOOK_SCRIPT = 'gsd-cursor-post-tool.js'; +// Marker comment embedded in managed hook entries so GSD can find+remove them. +const GSD_CURSOR_HOOK_MARKER = 'gsd-managed'; + // GSD-managed files under hooks/lib/ (helpers required by gsd-*.sh hooks). // git-cmd.js does not start with "gsd-" (shared classifier for #3129), gsd-graphify-rebuild.sh does. const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh']; @@ -403,218 +511,6 @@ function getConfigDirFromHome(runtime, isGlobal) { return "'.claude'"; } -/** - * Get the global config directory for OpenCode - * OpenCode follows XDG Base Directory spec and uses ~/.config/opencode/ - * Priority: OPENCODE_CONFIG_DIR > dirname(OPENCODE_CONFIG) > XDG_CONFIG_HOME/opencode > ~/.config/opencode - */ -function getOpencodeGlobalDir() { - // 1. Explicit OPENCODE_CONFIG_DIR env var - if (process.env.OPENCODE_CONFIG_DIR) { - return expandTilde(process.env.OPENCODE_CONFIG_DIR); - } - - // 2. OPENCODE_CONFIG env var (use its directory) - if (process.env.OPENCODE_CONFIG) { - return path.dirname(expandTilde(process.env.OPENCODE_CONFIG)); - } - - // 3. XDG_CONFIG_HOME/opencode - if (process.env.XDG_CONFIG_HOME) { - return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode'); - } - - // 4. Default: ~/.config/opencode (XDG default) - return path.join(os.homedir(), '.config', 'opencode'); -} - -/** - * Get the global config directory for Kilo - * Kilo follows XDG Base Directory spec and uses ~/.config/kilo/ - * Priority: KILO_CONFIG_DIR > dirname(KILO_CONFIG) > XDG_CONFIG_HOME/kilo > ~/.config/kilo - */ -function getKiloGlobalDir() { - // 1. Explicit KILO_CONFIG_DIR env var - if (process.env.KILO_CONFIG_DIR) { - return expandTilde(process.env.KILO_CONFIG_DIR); - } - - // 2. KILO_CONFIG env var (use its directory) - if (process.env.KILO_CONFIG) { - return path.dirname(expandTilde(process.env.KILO_CONFIG)); - } - - // 3. XDG_CONFIG_HOME/kilo - if (process.env.XDG_CONFIG_HOME) { - return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'kilo'); - } - - // 4. Default: ~/.config/kilo (XDG default) - return path.join(os.homedir(), '.config', 'kilo'); -} - -/** - * Get the global config directory for a runtime - * @param {string} runtime - 'claude', 'opencode', 'gemini', 'codex', or 'copilot' - * @param {string|null} explicitDir - Explicit directory from --config-dir flag - */ -function getGlobalDir(runtime, explicitDir = null) { - if (runtime === 'opencode') { - // For OpenCode, --config-dir overrides env vars - if (explicitDir) { - return expandTilde(explicitDir); - } - return getOpencodeGlobalDir(); - } - - if (runtime === 'kilo') { - // For Kilo, --config-dir overrides env vars - if (explicitDir) { - return expandTilde(explicitDir); - } - return getKiloGlobalDir(); - } - - if (runtime === 'gemini') { - // Gemini: --config-dir > GEMINI_CONFIG_DIR > ~/.gemini - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.GEMINI_CONFIG_DIR) { - return expandTilde(process.env.GEMINI_CONFIG_DIR); - } - return path.join(os.homedir(), '.gemini'); - } - - if (runtime === 'codex') { - // Codex: --config-dir > CODEX_HOME > ~/.codex - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.CODEX_HOME) { - return expandTilde(process.env.CODEX_HOME); - } - return path.join(os.homedir(), '.codex'); - } - - if (runtime === 'copilot') { - // Copilot: --config-dir > COPILOT_CONFIG_DIR > ~/.copilot - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.COPILOT_CONFIG_DIR) { - return expandTilde(process.env.COPILOT_CONFIG_DIR); - } - return path.join(os.homedir(), '.copilot'); - } - - if (runtime === 'antigravity') { - // Antigravity: --config-dir > ANTIGRAVITY_CONFIG_DIR > auto-detected - // ~/.gemini/{antigravity,antigravity-ide,antigravity-cli} - if (explicitDir) { - return expandTilde(explicitDir); - } - return resolveAntigravityGlobalDir(); - } - - if (runtime === 'cursor') { - // Cursor: --config-dir > CURSOR_CONFIG_DIR > ~/.cursor - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.CURSOR_CONFIG_DIR) { - return expandTilde(process.env.CURSOR_CONFIG_DIR); - } - return path.join(os.homedir(), '.cursor'); - } - - if (runtime === 'windsurf') { - // Windsurf: --config-dir > WINDSURF_CONFIG_DIR > ~/.codeium/windsurf - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.WINDSURF_CONFIG_DIR) { - return expandTilde(process.env.WINDSURF_CONFIG_DIR); - } - return path.join(os.homedir(), '.codeium', 'windsurf'); - } - - if (runtime === 'augment') { - // Augment: --config-dir > AUGMENT_CONFIG_DIR > ~/.augment - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.AUGMENT_CONFIG_DIR) { - return expandTilde(process.env.AUGMENT_CONFIG_DIR); - } - return path.join(os.homedir(), '.augment'); - } - if (runtime === 'trae') { - // Trae: --config-dir > TRAE_CONFIG_DIR > ~/.trae - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.TRAE_CONFIG_DIR) { - return expandTilde(process.env.TRAE_CONFIG_DIR); - } - return path.join(os.homedir(), '.trae'); - } - - if (runtime === 'qwen') { - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.QWEN_CONFIG_DIR) { - return expandTilde(process.env.QWEN_CONFIG_DIR); - } - return path.join(os.homedir(), '.qwen'); - } - - if (runtime === 'hermes') { - // Hermes Agent: --config-dir > HERMES_HOME > ~/.hermes - // Honors HERMES_HOME which Hermes users set for profile mode / Docker - // deploys (docs: https://hermes-agent.nousresearch.com/docs). - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.HERMES_HOME) { - return expandTilde(process.env.HERMES_HOME); - } - return path.join(os.homedir(), '.hermes'); - } - - if (runtime === 'codebuddy') { - // CodeBuddy: --config-dir > CODEBUDDY_CONFIG_DIR > ~/.codebuddy - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.CODEBUDDY_CONFIG_DIR) { - return expandTilde(process.env.CODEBUDDY_CONFIG_DIR); - } - return path.join(os.homedir(), '.codebuddy'); - } - - if (runtime === 'cline') { - // Cline: --config-dir > CLINE_CONFIG_DIR > ~/.cline - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.CLINE_CONFIG_DIR) { - return expandTilde(process.env.CLINE_CONFIG_DIR); - } - return path.join(os.homedir(), '.cline'); - } - - // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.CLAUDE_CONFIG_DIR) { - return expandTilde(process.env.CLAUDE_CONFIG_DIR); - } - return path.join(os.homedir(), '.claude'); -} - const banner = '\n' + cyan + ' ██████╗ ███████╗██████╗\n' + ' ██╔════╝ ██╔════╝██╔══██╗\n' + @@ -683,20 +579,10 @@ if (hasUninstall) { // Show help if requested if (hasHelp) { - 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}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex 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}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — 7 main-loop skills incl. phase (~130 desc tokens)\n standard — ~13 skills incl. phase, review, config (~700)\n full — all 66 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 Gemini globally${reset}\n npx ${pkg.name} --gemini --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 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 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 / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / 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`); + 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}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex 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}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — 7 main-loop skills incl. phase (~130 desc tokens)\n standard — ~13 skills incl. phase, review, config (~700)\n full — all 66 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 Gemini globally${reset}\n npx ${pkg.name} --gemini --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 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 / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / 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`); process.exit(0); } -/** - * Expand ~ to home directory (shell doesn't expand in env vars passed to node) - */ -function expandTilde(filePath) { - if (filePath && filePath.startsWith('~/')) { - return path.join(os.homedir(), filePath.slice(2)); - } - return filePath; -} - /** * Compute the path prefix used for `@file` references in installed command/skill * markdown. For global installs into a runtime config dir under $HOME, we @@ -991,9 +877,31 @@ function rewriteLegacyCodexHookBlock(content, absoluteRunner, opts) { return { content: updated, changed }; } -function reconcileCodexHooksJsonSessionStart(targetDir, opts = {}) { +/** + * Generic reconcile helper: ensure hooks.json contains exactly one managed GSD + * hook entry for `eventName`, while preserving all user-owned entries. + * + * Supports both known hooks.json shapes: + * 1) { "": [...] } + * 2) { "hooks": { "": [...] } } + * + * @param {string} targetDir - Codex config dir (e.g. ~/.codex or /.codex). + * @param {string} eventName - Codex hook event name (e.g. 'SessionStart', 'Stop'). + * @param {{ managedCommand?: string|null, commandWindows?: string|null, matcher?: string|null, timeout?: number|null }} opts + * managedCommand: POSIX hook command string to register, or null to remove. + * commandWindows: Windows .cmd shim path to emit as `commandWindows` field + * (#772). When provided, Codex uses this path on Windows and `managedCommand` + * on POSIX without needing per-platform config regeneration. + * matcher: optional Codex MatcherGroup pattern (e.g. 'Bash|Edit|Write'). + * timeout: optional timeout in seconds. + * @returns {{ changed: boolean, wrote: boolean, path: string }} + */ +function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) { const hooksJsonPath = path.join(targetDir, 'hooks.json'); const managedCommand = typeof opts.managedCommand === 'string' ? opts.managedCommand : null; + const commandWindows = typeof opts.commandWindows === 'string' ? opts.commandWindows : null; + const matcher = typeof opts.matcher === 'string' ? opts.matcher : undefined; + const timeout = typeof opts.timeout === 'number' ? opts.timeout : undefined; let parsed = {}; let currentContent = null; if (fs.existsSync(hooksJsonPath)) { @@ -1012,22 +920,22 @@ function reconcileCodexHooksJsonSessionStart(targetDir, opts = {}) { const usesNestedHooksObject = parsed.hooks && typeof parsed.hooks === 'object' && !Array.isArray(parsed.hooks); const hookTable = usesNestedHooksObject ? parsed.hooks : parsed; - const sessionStart = Array.isArray(hookTable.SessionStart) ? hookTable.SessionStart : []; + const eventEntries = Array.isArray(hookTable[eventName]) ? hookTable[eventName] : []; let removedLegacy = false; - const sanitizedSessionStart = []; - for (const entry of sessionStart) { + const sanitizedEntries = []; + for (const entry of eventEntries) { if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue; const originalHooks = Array.isArray(entry.hooks) ? entry.hooks : []; if (originalHooks.length === 0) { - sanitizedSessionStart.push(entry); + sanitizedEntries.push(entry); continue; } - const keptHooks = originalHooks.filter((hook) => { - const cmd = hook && typeof hook === 'object' ? hook.command : null; - const managed = isManagedHookCommand(cmd, { - surface: 'codex-hooks-json', - includeLegacyAliases: true, + const keptHooks = originalHooks.filter((hook) => { + const cmd = hook && typeof hook === 'object' ? hook.command : null; + const managed = isManagedHookCommand(cmd, { + surface: 'codex-hooks-json', + includeLegacyAliases: true, configDir: targetDir, }); if (managed) removedLegacy = true; @@ -1035,24 +943,26 @@ function reconcileCodexHooksJsonSessionStart(targetDir, opts = {}) { }); if (keptHooks.length === 0) continue; const nextEntry = { ...entry, hooks: keptHooks }; - sanitizedSessionStart.push(nextEntry); + sanitizedEntries.push(nextEntry); } if (managedCommand) { - sanitizedSessionStart.push({ - hooks: [ - { - type: 'command', - command: managedCommand, - }, - ], - }); + const hookEntry = { type: 'command', command: managedCommand }; + // #772: emit commandWindows so Codex picks the .cmd shim on Windows and + // the POSIX command on other platforms — without requiring per-OS config + // regeneration. Sourced from HookHandlerConfig.command_windows field in + // codex-rs/config/src/hook_config.rs (alias: commandWindows). + if (commandWindows) hookEntry.commandWindows = commandWindows; + if (timeout !== undefined) hookEntry.timeout = timeout; + const newEntry = { hooks: [hookEntry] }; + if (matcher !== undefined) newEntry.matcher = matcher; + sanitizedEntries.push(newEntry); } - if (sanitizedSessionStart.length > 0) { - hookTable.SessionStart = sanitizedSessionStart; + if (sanitizedEntries.length > 0) { + hookTable[eventName] = sanitizedEntries; } else { - delete hookTable.SessionStart; + delete hookTable[eventName]; } if (usesNestedHooksObject) parsed.hooks = hookTable; @@ -1066,6 +976,18 @@ function reconcileCodexHooksJsonSessionStart(targetDir, opts = {}) { return { changed: changed || removedLegacy, wrote: shouldWrite, path: hooksJsonPath }; } +/** + * Reconcile the GSD-managed SessionStart hook entry in hooks.json. + * Delegates to the generic reconcileCodexHooksJsonEvent helper. + * + * @param {string} targetDir + * @param {{ managedCommand?: string|null, commandWindows?: string|null }} opts + * @returns {{ changed: boolean, wrote: boolean, path: string }} + */ +function reconcileCodexHooksJsonSessionStart(targetDir, opts = {}) { + return reconcileCodexHooksJsonEvent(targetDir, 'SessionStart', opts); +} + /** * Build a typed IR for the Codex hook .cmd shim used on Windows (#3426). * @@ -1147,8 +1069,14 @@ function buildCodexHookWindowsShimIR(scriptAbsPath, absoluteRunnerToken) { * 2) { "hooks": { "SessionStart": [...] } } * * On Windows, writes a .cmd shim alongside the .js hook file and uses the - * .cmd path as the hook command to avoid the `bash.exe: cannot execute binary - * file` failure (#3426). + * .cmd shim path as the hook command to avoid the `bash.exe: cannot execute + * binary file` failure (#3426). + * + * #772: also emits `commandWindows` in the hook entry so that a + * cross-platform hooks.json works on both POSIX and Windows without + * requiring per-OS regeneration. Codex dispatches `commandWindows` on + * Windows and `command` on other platforms (HookHandlerConfig in + * codex-rs/config/src/hook_config.rs). * * @param {string} targetDir * @param {{ absoluteRunner: string|null, platform?: NodeJS.Platform }} opts @@ -1160,7 +1088,18 @@ function ensureCodexHooksJsonSessionStart(targetDir, opts = {}) { const hooksJsonPath = path.join(targetDir, 'hooks.json'); if (!absoluteRunner) return { changed: false, wrote: false, path: hooksJsonPath }; - const scriptPath = path.resolve(targetDir, 'hooks', 'gsd-check-update.js'); + // Normalize backslashes to forward slashes so isManagedHookCommand can + // match stored commands against configDir on Windows CI runners where + // path.resolve returns backslash paths but the stored command may use + // forward slashes (or vice versa). Forward-slash paths are always valid on + // Windows for both Node.js and Codex, so this normalization is safe for all + // platforms. (#772 — same fix applied to ensureCodexHooksJsonEvent.) + const scriptPath = path.resolve(targetDir, 'hooks', 'gsd-check-update.js').replace(/\\/g, '/'); + + // #772: compute the Windows .cmd shim path cross-platform so that + // `commandWindows` can be emitted in hooks.json regardless of the host OS. + // The .cmd path is always the .js script path with extension replaced. + const cmdShimPath = scriptPath.replace(/\.js$/, '.cmd'); let managedCommand; if (platform === 'win32') { @@ -1197,7 +1136,96 @@ function ensureCodexHooksJsonSessionStart(targetDir, opts = {}) { } if (!managedCommand) return { changed: false, wrote: false, path: hooksJsonPath }; - return reconcileCodexHooksJsonSessionStart(targetDir, { managedCommand }); + + // #772: emit commandWindows — the .cmd shim path — but ONLY on Windows where + // the shim was actually written. On POSIX, commandWindows is omitted to avoid + // pointing Windows Codex at a non-existent .cmd file (the shim is only present + // when install() ran natively on Windows and wrote it via buildCodexHookWindowsShimIR). + const commandWindows = platform === 'win32' + ? JSON.stringify(cmdShimPath.replace(/\\/g, '/')) + : undefined; + + return reconcileCodexHooksJsonSessionStart(targetDir, { managedCommand, commandWindows }); +} + +/** + * Ensure hooks.json contains exactly one managed GSD hook entry for the given + * Codex event, wired to gsd-context-monitor.js. Preserves user-owned entries. + * + * Used for the new Codex events added in #772: + * SubagentStart — inject context / GSD_AGENT_NAME awareness at subagent open + * Stop — post-session context headroom tracking + * PostToolUse — mirror the Claude Code PostToolUse context monitor + * + * All three events are routed through gsd-context-monitor.js — the same hook + * used for PostToolUse in the Claude Code baseline — so context-headroom + * warnings surface at these key Codex session lifecycle moments. + * + * On Windows (#3426): writes a gsd-context-monitor.cmd shim alongside the .js + * file and uses the .cmd path as the hook command — exactly the same fix as + * SessionStart uses for gsd-check-update — to avoid the bash.exe POSIX-exec + * failure when Codex's hook dispatcher tries to run node.exe through Git Bash. + * + * @param {string} targetDir + * @param {string} eventName - One of 'SubagentStart', 'Stop', 'PostToolUse'. + * @param {{ absoluteRunner: string|null, platform?: NodeJS.Platform }} opts + * @returns {{ changed: boolean, wrote: boolean, path: string }} + */ +function ensureCodexHooksJsonEvent(targetDir, eventName, opts = {}) { + const platform = opts.platform || process.platform; + const absoluteRunner = opts.absoluteRunner || null; + const hooksJsonPath = path.join(targetDir, 'hooks.json'); + if (!absoluteRunner) return { changed: false, wrote: false, path: hooksJsonPath }; + + // Normalize backslashes to forward slashes so that isManagedHookCommand can + // match the stored command against configDir on Windows. path.resolve on + // Windows returns backslash paths, but when platform is not 'win32' + // (e.g. platform: 'linux' in a test running on a Windows CI runner), + // projectManagedHookCommand does not normalize them — producing a mismatch + // between the stored command and the configDir-based hook-dir prefix used + // for deduplication. Forward-slash paths are always valid on Windows (Node.js + // and Codex both accept them), so normalizing here is safe for all platforms. + const scriptPath = path.resolve(targetDir, 'hooks', 'gsd-context-monitor.js').replace(/\\/g, '/'); + + let managedCommand; + if (platform === 'win32') { + // #3426 fix pattern: on Windows, write a .cmd shim and use its path as the + // hook command. The same bash.exe POSIX-exec failure that affects + // gsd-check-update.js also affects gsd-context-monitor.js. + const shimIR = buildCodexHookWindowsShimIR(scriptPath, absoluteRunner); + if (!shimIR) return { changed: false, wrote: false, path: hooksJsonPath }; + try { + atomicWriteFileSync(shimIR.cmdPath, shimIR.render.cmd(), 'utf8'); + } catch (shimWriteErr) { + const reason = shimWriteErr && shimWriteErr.message ? shimWriteErr.message : String(shimWriteErr); + console.warn( + ` ${yellow}⚠${reset} Codex Windows hook NOT installed — .cmd shim write failed for ${eventName}: ${reason}. ` + + `Fix the write error (permissions? disk full?) and re-run the installer.`, + ); + return { changed: false, wrote: false, path: hooksJsonPath }; + } + managedCommand = shimIR.hookCommand; + } else { + managedCommand = projectManagedHookCommand({ + absoluteRunner, + scriptPath, + runtime: 'codex', + platform, + }); + } + + if (!managedCommand) return { changed: false, wrote: false, path: hooksJsonPath }; + return reconcileCodexHooksJsonEvent(targetDir, eventName, { managedCommand, timeout: 10 }); +} + +/** + * Remove a GSD-managed event entry from hooks.json. Called during uninstall. + * + * @param {string} targetDir + * @param {string} eventName + */ +function removeCodexHooksJsonEvent(targetDir, eventName) { + return reconcileCodexHooksJsonEvent(targetDir, eventName, { managedCommand: null }); } function removeCodexHooksJsonSessionStart(targetDir) { @@ -1769,11 +1797,11 @@ function getCommitAttribution(runtime) { const resolveConfigPath = runtime === 'opencode' ? resolveOpencodeConfigPath : resolveKiloConfigPath; - const config = readSettings(resolveConfigPath(getGlobalDir(runtime, null))); + const config = readSettings(resolveConfigPath(getGlobalConfigDir(runtime, null))); result = (config && config.disable_ai_attribution === true) ? null : undefined; } else if (runtime === 'gemini') { // Gemini: check gemini settings.json for attribution config - const settings = readSettings(path.join(getGlobalDir('gemini', explicitConfigDir), 'settings.json')); + const settings = readSettings(path.join(getGlobalConfigDir('gemini', explicitConfigDir), 'settings.json')); if (!settings || !settings.attribution || settings.attribution.commit === undefined) { result = undefined; } else if (settings.attribution.commit === '') { @@ -1783,7 +1811,7 @@ function getCommitAttribution(runtime) { } } else if (runtime === 'claude') { // Claude Code - const settings = readSettings(path.join(getGlobalDir('claude', explicitConfigDir), 'settings.json')); + const settings = readSettings(path.join(getGlobalConfigDir('claude', explicitConfigDir), 'settings.json')); if (!settings || !settings.attribution || settings.attribution.commit === undefined) { result = undefined; } else if (settings.attribution.commit === '') { @@ -1997,6 +2025,8 @@ function convertCopilotToolName(claudeTool) { if (claudeToCopilotTools[claudeTool]) { return claudeToCopilotTools[claudeTool]; } + // mcp__{tavily,ref,jina,exa,firecrawl}__* use the generic MCP passthrough like exa/firecrawl; + // add explicit Copilot registry mappings when the io.github ids are confirmed (#657 follow-up) // Default: lowercase return claudeTool.toLowerCase(); } @@ -2091,6 +2121,40 @@ function skillFrontmatterName(skillDirName) { return skillDirName; } +/** + * Qwen Code skills accept an optional numeric `priority` frontmatter field. + * Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified + * #778): HIGHER values sort EARLIER in the `/skills` TUI listing (omitted ≈ 0; + * negatives sort below unset). It affects ONLY the `/skills` list order — + * slash-command completion and the `/help` view stay alphabetical. + * + * We assign descending priorities to GSD's main-loop commands so the most-used + * workflow skills surface first; utility skills are deliberately left unset + * (default 0) and sort below. + * + * NOTE: the #778 issue body proposed the INVERSE numbering (plan-phase: 10, + * utilities: 90+). The verified spec shows that would BURY the core loop below + * utilities, so we implement the spec-correct direction (core = high) instead. + * Keyed by command stem (skill dir is `gsd-`). + */ +const QWEN_SKILL_PRIORITY = Object.freeze({ + 'new-project': 100, + 'discuss-phase': 95, + 'plan-phase': 90, + 'execute-phase': 85, + progress: 80, + 'verify-work': 75, + phase: 70, + review: 65, + ship: 60, + config: 55, + surface: 50, + 'resume-work': 45, + 'pause-work': 40, + help: 35, + update: 30, +}); + /** * Convert a Claude command (.md) to a Claude skill (SKILL.md). * Claude Code is the native format, so minimal conversion needed — @@ -2113,6 +2177,10 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c const description = extractFrontmatterField(frontmatter, 'description') || ''; const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint'); const agent = extractFrontmatterField(frontmatter, 'agent'); + // #769: preserve context: and effort: from source command files so they + // are emitted into the installed SKILL.md frontmatter unchanged. + const context = extractFrontmatterField(frontmatter, 'context'); + const effort = extractFrontmatterField(frontmatter, 'effort'); // Preserve allowed-tools as YAML multiline list (Claude native format) const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m); @@ -2130,8 +2198,26 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c // Track GSD's package version so Hermes' skill_view() reports a stable // identifier per install. if (runtime === 'hermes') fm += `version: ${yamlQuote(pkg.version)}\n`; + // #778 (b) — Qwen-only numeric priority for /skills ordering. Scoped to qwen + // so Claude/Hermes skill frontmatter is unchanged (they ignore the field, but + // we keep their output byte-stable). skillName is the `gsd-` dir name. + if (runtime === 'qwen') { + const stem = typeof skillName === 'string' && skillName.startsWith('gsd-') + ? skillName.slice(4) + : skillName; + const priority = Object.prototype.hasOwnProperty.call(QWEN_SKILL_PRIORITY, stem) + ? QWEN_SKILL_PRIORITY[stem] + : undefined; + if (typeof priority === 'number') fm += `priority: ${priority}\n`; + } if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`; if (agent) fm += `agent: ${agent}\n`; + // #769: emit context: and effort: when present so the runtime can honour + // them natively (context: fork = isolated subagent window; effort: = + // token-budget tier). Fields are Claude-specific; unknown frontmatter + // fields are silently ignored by other runtimes (backward-compatible). + if (context) fm += `context: ${context}\n`; + if (effort) fm += `effort: ${effort}\n`; if (toolsBlock) fm += toolsBlock; fm += '---'; @@ -2384,6 +2470,29 @@ function convertClaudeCommandToCursorSkill(content, skillName) { return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; } +/** + * Convert a Claude Code command to a Cursor 1.6 slash command (#785). + * + * Cursor slash commands live in `.cursor/commands/.md` and are + * plain markdown — no YAML frontmatter, no adapter header. The filename + * becomes the command name (e.g. `gsd-help.md` → `/gsd-help`). + * + * Applies the same `convertClaudeToCursorMarkdown` transforms as the skill + * converter (tool renames, brand substitution, slash-command normalisation), + * then strips the YAML frontmatter block so only the prose body remains. + * + * @param {string} content raw Claude Code command markdown (may have frontmatter) + * @param {string} _commandName the target command name (unused; present for + * API symmetry with other converters so the runtime-artifact-layout stage + * function can call it uniformly) + * @returns {string} plain markdown body, no frontmatter + */ +function convertClaudeCommandToCursorCommand(content, _commandName) { + const converted = convertClaudeToCursorMarkdown(content); + const { body } = extractFrontmatterAndBody(converted); + return body.trimStart(); +} + /** * Convert Claude Code agent markdown to Cursor agent format. * Strips frontmatter fields Cursor doesn't support (color, skills), @@ -2747,7 +2856,47 @@ function convertClaudeCommandToCodebuddySkill(content, skillName) { const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; // #2876: quote so YAML flow indicators (`[BETA] …`) don't break // CodeBuddy's frontmatter parser. - return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n${body}`; + // + // #789: mark user-invocable:false so the skill is NOT shown in CodeBuddy's + // '/' menu (it defaults to true). The commands/ surface (#789) is the sole + // '/' entry point; skills remain model-invocable background knowledge, + // avoiding a duplicated /gsd-* entry per workflow. + return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n${body}`; +} + +/** + * Convert a Claude Code slash-command (.md) to a CodeBuddy slash-command (.md). + * + * CodeBuddy reads user-level slash commands from ~/.codebuddy/commands/.md + * (https://www.codebuddy.ai/docs/cli/slash-commands). The filename determines the + * command name (gsd-help.md → /gsd-help), so the Claude-specific `name: gsd:` + * frontmatter field is dropped. CodeBuddy command frontmatter supports + * `description` and `argument-hint`; both are preserved when present. The body is + * brand/path-converted via convertClaudeToCodebuddyMarkdown. + * + * @param {string} content raw Claude command markdown + * @param {string} commandName installed command name (e.g. 'gsd-help') + * @returns {string} + */ +function convertClaudeCommandToCodebuddyCommand(content, commandName) { + const converted = convertClaudeToCodebuddyMarkdown(content); + const { frontmatter, body } = extractFrontmatterAndBody(converted); + let description = `Run GSD workflow ${commandName}.`; + let argumentHint = ''; + if (frontmatter) { + const maybeDescription = extractFrontmatterField(frontmatter, 'description'); + if (maybeDescription) description = maybeDescription; + const maybeArgHint = extractFrontmatterField(frontmatter, 'argument-hint'); + if (maybeArgHint) argumentHint = maybeArgHint; + } + description = toSingleLine(description); + const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; + // #2876: quote values so YAML flow indicators (`[BETA] …`, `[name]`) don't + // break CodeBuddy's frontmatter parser. + const lines = ['---', `description: ${yamlQuote(shortDescription)}`]; + if (argumentHint) lines.push(`argument-hint: ${yamlQuote(toSingleLine(argumentHint))}`); + lines.push('---', body.trimStart()); + return lines.join('\n'); } function convertClaudeAgentToCodebuddyAgent(content) { @@ -2773,9 +2922,15 @@ function convertClaudeToCliineMarkdown(content) { converted = converted.replace(/\.\/CLAUDE\.md/g, '.clinerules'); converted = converted.replace(/`CLAUDE\.md`/g, '`.clinerules`'); converted = converted.replace(/\bCLAUDE\.md\b/g, '.clinerules'); + // Slash forms first (most specific — superset of bare forms) converted = converted.replace(/\.claude\/skills\//g, '.cline/skills/'); converted = converted.replace(/\.\/\.claude\//g, './.cline/'); converted = converted.replace(/\.claude\//g, '.cline/'); + // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite + converted = converted.replace(/~\/\.claude\b/g, '~/.cline'); + converted = converted.replace(/\$HOME\/\.claude\b/g, '$HOME/.cline'); + // Environment variable name rewrite + converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'CLINE_CONFIG_DIR'); converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); converted = converted.replace(/\bClaude Code\b/g, 'Cline'); @@ -2792,17 +2947,62 @@ function convertClaudeAgentToClineAgent(content) { return `${cleanFrontmatter}\n${body}`; } +/** + * Convert a Claude command (.md) to a Cline skill (SKILL.md). + * Emits ONLY name + description frontmatter per the Cline skills spec + * (https://docs.cline.bot/customization/skills) — no allowed-tools, + * argument-hint, agent, or other Claude-specific fields. + * Body is hyphen-normalised then converted via convertClaudeToCliineMarkdown + * (.claude/→.cline/, "Claude Code"→"Cline", etc.). + * Cline uses Claude-Code-compatible tool names, so no adapter header is needed. + * Targets ~/.cline/skills//SKILL.md for Cline >= v3.48.0. + */ +function convertClaudeCommandToClineSkill(content, skillName, runtime = null, cmdNames = null) { + const { frontmatter, body } = extractFrontmatterAndBody(content); + if (!frontmatter) return content; + + // Hyphen-normalise /gsd: → gsd- references in the body, then + // apply Cline-specific markdown rewrites (.claude/→.cline/, etc.). + const names = cmdNames || readGsdCommandNames(); + const normalizedBody = transformContentToHyphen(body, names); + const clineBody = convertClaudeToCliineMarkdown(normalizedBody); + + // Extract description; fall back to a generic string if absent. + let description = extractFrontmatterField(frontmatter, 'description'); + if (!description) description = `Run GSD workflow ${skillName}.`; + description = toSingleLine(description); + // Cline documented max is 1024 code points (not UTF-16 code units). + // Use Array.from to iterate by code point so that multibyte characters + // (e.g. emoji, astral-plane chars) are never split, which would produce + // lone surrogates and corrupt the YAML output. + const cp = Array.from(description); + const shortDescription = cp.length > 1024 + ? cp.slice(0, 1021).join('') + '...' + : description; + + const fm = `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---`; + return `${fm}\n${clineBody}`; +} + // ── End Cline converters ───────────────────────────────────────────────────── function convertSlashCommandsToCodexSkillMentions(content) { - // Convert colon-style skill invocations to Codex $ prefix + // Colon-style /gsd: never appears as a filesystem path segment, so no boundary guard is needed (unlike the hyphen-style below). let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => { return `$gsd-${String(commandName).toLowerCase()}`; }); // Convert hyphen-style command references (workflow output) to Codex $ prefix. - // Negative lookbehind excludes file paths like bin/gsd-tools.cjs where - // the slash is preceded by a word char, dot, or another slash. - converted = converted.replace(/(? { + // A real /gsd- MENTION is defined positively by two boundaries, so any + // in-path occurrence is excluded by construction (no denylist of preceding + // chars to maintain — see #712, supersedes the #637/#704 lookbehind treadmill): + // 1. Left boundary: opens at start-of-string, whitespace, or an inline-prose + // delimiter (backtick/quote/paren/bracket) — e.g. `/gsd-execute-phase`. + // 2. Right boundary: the command token is NOT followed by a path separator + // `/` (a path continues: `/gsd-core/bin/...`; a command does not). The + // `(?![a-z0-9/-])` also blocks regex backtracking to a shorter command. + // This converts backtick-wrapped MENTIONS (`/gsd-foo`) while leaving backtick- + // wrapped PATHS (`/gsd-core/workflows/update.md`) untouched (#712). + converted = converted.replace(/(?<=^|[\s`"'([])\/gsd-([a-z0-9-]+)(?![a-z0-9/-])/gi, (_, commandName) => { return `$gsd-${String(commandName).toLowerCase()}`; }); return converted; @@ -3003,6 +3203,20 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, const _renderedEffortCodex = _getGsdEffortCatalog().renderEffortForRuntime('codex', _universalEffortCodex).value; lines.push(`model_reasoning_effort = ${JSON.stringify(_renderedEffortCodex)}`); + // #774 — Emit service_tier and model_verbosity for light-tier agents. + // Light-tier agents (routingTier: "light" in model-catalog.json) are haiku-equivalent + // and benefit from Codex's "flex" service tier (lower cost, background processing) + // and "low" verbosity (reduced token output). Both fields are validated against the + // Codex ConfigProfile schema (codex-rs/config/src/profile_toml.rs): + // service_tier: Option — "flex" | "fast" (legacy) + // model_verbosity: Option — "low" | "medium" | "high" + const { AGENT_DEFAULT_TIERS: _agentTiers } = _getGsdEffortCatalog(); + const _agentRoutingTier = _agentTiers?.[resolvedName] || _agentTiers?.[agentName]; + if (_agentRoutingTier === 'light') { + lines.push(`service_tier = "flex"`); + lines.push(`model_verbosity = "low"`); + } + // Agent prompts contain raw backslashes in regexes and shell snippets. // TOML literal multiline strings preserve them without escape parsing. lines.push(`developer_instructions = '''`); @@ -3012,6 +3226,115 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, return lines.join('\n') + '\n'; } +/** + * Generate the agents/openai.yaml TUI chip metadata content for a Codex skill. + * + * This file is written alongside SKILL.md as /agents/openai.yaml. + * Codex loads it as a SkillMetadataFile (codex-rs/core-skills/src/loader.rs), + * making the skill discoverable in the /skills TUI popup with a display name + * and short description. If the file is absent, Codex silently skips it (fails open). + * + * Schema (interface section): + * display_name: short human-readable skill name (strip gsd- prefix) + * short_description: 1-2 sentence description for TUI chip, ≤180 chars + * + * @param {string} skillName - Full skill name e.g. "gsd-plan-phase" + * @param {string} shortDescription - Description text (already truncated by caller) + * @returns {string} YAML content for agents/openai.yaml + */ +function generateCodexSkillMetadataYaml(skillName, shortDescription) { + // Display name: strip "gsd-" prefix and convert hyphens to spaces for readability. + const displayName = skillName.replace(/^gsd-/, '').replace(/-/g, ' '); + // yamlQuote (= JSON.stringify) handles all YAML-unsafe chars: backslashes, + // quotes, newlines, control characters, and Unicode escapes. + return [ + 'interface:', + ` display_name: ${yamlQuote(displayName)}`, + ` short_description: ${yamlQuote(shortDescription)}`, + '', + ].join('\n'); +} + +/** + * Write agents/openai.yaml TUI chip metadata for each gsd-* skill directory. + * + * Called after layout-driven skill install for Codex. Iterates every gsd-* + * skill directory in skillsDir, reads the SKILL.md frontmatter to extract the + * short-description already emitted by convertClaudeCommandToCodexSkill, then + * writes /agents/openai.yaml using generateCodexSkillMetadataYaml. + * + * Fails open: individual skill directories that cannot be processed are silently + * skipped so a single malformed SKILL.md cannot block the whole install. + * + * User-owned skill directories (e.g. gsd-dev-preferences) are explicitly + * skipped so existing user-authored agents/openai.yaml files are never + * overwritten. These dirs are listed in the same USER_OWNED_SKILL_DIRS + * constant used by installOpencodeFamilySkills. + * + * The YAML-quoted description value is unescaped before embedding so that + * YAML escape sequences (e.g. \" in a double-quoted scalar) become the + * literal characters they represent rather than being double-escaped in the + * output. + * + * @param {string} skillsDir - Path to the skills/ directory (e.g. ~/.codex/skills) + */ +function writeCodexSkillMetadataFiles(skillsDir) { + if (!fs.existsSync(skillsDir)) return; + // Mirror the user-owned list from installOpencodeFamilySkills (#2973). + // We MUST skip these dirs — their contents are user-generated and must + // never be overwritten by GSD's install path. + const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']); + for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) { + if (!entry.isDirectory() || !entry.name.startsWith('gsd-')) continue; + if (_userOwnedSkillDirs.has(entry.name)) continue; // preserve user content + const skillDir = path.join(skillsDir, entry.name); + const skillMdPath = path.join(skillDir, 'SKILL.md'); + try { + const content = fs.readFileSync(skillMdPath, 'utf8'); + const { frontmatter } = extractFrontmatterAndBody(content); + // Prefer the short-description field emitted by convertClaudeCommandToCodexSkill; + // fall back to description, then a synthetic label from the skill name. + let shortDesc = ''; + if (frontmatter) { + // SKILL.md uses YAML frontmatter with a nested metadata.short-description key. + // extractFrontmatterField handles only top-level keys; parse the metadata block + // by looking for " short-description:" directly. + const metaMatch = frontmatter.match(/^[ \t]*metadata\s*:\s*\n((?:[ \t]+.*\n?)*)/m); + if (metaMatch) { + const metaBlock = metaMatch[1]; + const sdMatch = metaBlock.match(/^[ \t]+short-description\s*:\s*(.+)$/m); + if (sdMatch) { + // Unescape YAML double-quoted scalar escapes before embedding. + // convertClaudeCommandToCodexSkill always emits a double-quoted + // value (via yamlQuote) so only double-quote unescaping is needed. + let raw = sdMatch[1].trim(); + if (raw.startsWith('"') && raw.endsWith('"')) { + // Strip outer double-quotes and decode \" → " and \\ → \ + raw = raw.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); + } else { + // Single-quoted or unquoted: strip surrounding quotes/whitespace + raw = raw.replace(/^["']|["']$/g, ''); + } + shortDesc = raw; + } + } + if (!shortDesc) { + shortDesc = extractFrontmatterField(frontmatter, 'description') || ''; + } + } + if (!shortDesc) { + shortDesc = `Run GSD workflow ${entry.name}.`; + } + const yamlContent = generateCodexSkillMetadataYaml(entry.name, shortDesc); + const agentsSubdir = path.join(skillDir, 'agents'); + fs.mkdirSync(agentsSubdir, { recursive: true }); + fs.writeFileSync(path.join(agentsSubdir, 'openai.yaml'), yamlContent); + } catch (_err) { + // Fail open — missing or unreadable SKILL.md must not block the install. + } + } +} + /** * Generate the GSD config block for Codex config.toml. * @param {Array<{name: string, description: string}>} agents @@ -5221,6 +5544,506 @@ function stripGsdFromCopilotInstructions(content) { return content; } +// ── Cline directory-form rules + hooks + AGENTS.md (issue #787) ──────────────── +// +// Cline v3.36 added a hooks system and a `.clinerules/` directory form. Because +// `.clinerules` cannot be both a file AND a directory, emitting hooks under +// `.clinerules/hooks/` requires migrating the rules content into the directory +// form (`.clinerules/gsd.md`). Sources adjudicated: +// - https://cline.bot/blog/cline-v3-36-hooks +// - https://docs.cline.bot/customization/cline-rules + +const GSD_AGENTS_MD_MARKER = ''; +const GSD_AGENTS_MD_CLOSE_MARKER = ''; + +/** + * The GSD instruction body shared by the Cline directory-form rules file and + * the cross-tool AGENTS.md block. Self-contained — references only the gsd-core + * engine layout, not the (separate) #782 Cline skills directory. + */ +function buildClineRulesBody() { + return [ + '# GSD Core — Git. Ship. Done.', + '', + '- GSD workflows live in `gsd-core/workflows/`. Load the relevant workflow when', + ' the user runs a `/gsd-*` command.', + '- GSD agents live in `agents/`. Use the matching agent when spawning subagents.', + '- GSD tools are at `gsd-core/bin/gsd-tools.cjs`. Run with `node`.', + '- Planning artifacts live in `.planning/`. Never edit them outside a GSD workflow.', + '- Do not apply GSD workflows unless the user explicitly asks for them.', + '- When a GSD command triggers a deliverable (feature, fix, docs), offer the next', + ' step to the user using Cline\'s ask_user tool after completing it.', + ].join('\n') + '\n'; +} + +/** AGENTS.md body for the cross-tool global instruction target (`~/.agents/AGENTS.md`). */ +function buildClineAgentsMdBody() { + return buildClineRulesBody(); +} + +/** + * The Cline PreToolUse hook script (issue #787). + * + * Cline invokes hooks as executable scripts named exactly after the event with + * no extension, passing the operation context as JSON on stdin and reading a + * JSON decision from stdout ({ cancel, errorMessage, contextModification }). + * + * This hook is a self-standing planning-artifact guard: it cancels write-class + * tool calls that target `.planning/` (GSD-owned artifacts), and otherwise + * allows the operation. It FAILS OPEN — any parse/IO error allows the call so a + * hook bug can never wedge the user. No dependency on the #782 skills work. + */ +function buildClinePreToolUseHook() { + return `#!/usr/bin/env node +'use strict'; +/* GSD-managed Cline PreToolUse hook — gsd-core issue #787. + * Protocol: JSON on stdin -> JSON decision on stdout. + * Honored fields: { cancel, errorMessage, contextModification }. + * Fails open: any error allows the operation. */ +let raw = ''; +process.stdin.setEncoding('utf8'); +process.stdin.on('data', (c) => { raw += c; }); +process.stdin.on('end', () => { + const allow = () => process.stdout.write(JSON.stringify({ cancel: false })); + let input; + try { input = JSON.parse(raw || '{}'); } catch { return allow(); } + try { + const tool = String( + input.toolName || input.tool_name || input.tool || + (input.toolInput && input.toolInput.name) || (input.tool_input && input.tool_input.name) || '' + ).toLowerCase(); + const isWrite = /write|edit|replace|create|delete|remove|append|apply|patch|insert|mkdir/.test(tool); + // Collect only PATH-bearing field values (not free-form content), so a doc + // that merely mentions ".planning/" in its body is never falsely blocked. + const paths = []; + const PATH_KEY = /^(path|file|file_?path|filepath|target_?path|target|dir|directory|uri|filename)$/i; + const walk = (v, depth) => { + if (depth > 5 || paths.length > 64) return; + if (Array.isArray(v)) { for (const x of v) walk(x, depth + 1); return; } + if (v && typeof v === 'object') { + for (const k of Object.keys(v)) { + const val = v[k]; + if (typeof val === 'string' && PATH_KEY.test(k)) paths.push(val); + else walk(val, depth + 1); + } + } + }; + walk(input, 0); + const isPlanningPath = (s) => /(^|[\\\\/])\\.planning([\\\\/]|$)/.test(s); + if (isWrite && paths.some(isPlanningPath)) { + return process.stdout.write(JSON.stringify({ + cancel: true, + errorMessage: + 'GSD: .planning/ artifacts are managed by GSD workflows. Edit them only through a /gsd-* command, not directly.', + })); + } + } catch { /* fall through to allow */ } + return allow(); +}); +`; +} + +/** + * Merge the GSD AGENTS.md block into an existing file (or create it), preserving + * any user content. Mirrors mergeCopilotInstructions: marker-delimited, idempotent. + */ +function mergeGsdAgentsMd(filePath, gsdContent) { + const gsdBlock = GSD_AGENTS_MD_MARKER + '\n' + gsdContent.trim() + '\n' + GSD_AGENTS_MD_CLOSE_MARKER; + + if (!fs.existsSync(filePath)) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, gsdBlock + '\n'); + return; + } + + const existing = fs.readFileSync(filePath, 'utf8'); + const openIndex = existing.indexOf(GSD_AGENTS_MD_MARKER); + const closeIndex = existing.indexOf(GSD_AGENTS_MD_CLOSE_MARKER); + + if (openIndex !== -1 && closeIndex !== -1) { + const before = existing.substring(0, openIndex).trimEnd(); + const after = existing.substring(closeIndex + GSD_AGENTS_MD_CLOSE_MARKER.length).trimStart(); + let newContent = ''; + if (before) newContent += before + '\n\n'; + newContent += gsdBlock; + if (after) newContent += '\n\n' + after; + newContent += '\n'; + fs.writeFileSync(filePath, newContent); + return; + } + + fs.writeFileSync(filePath, existing.trimEnd() + '\n\n' + gsdBlock + '\n'); +} + +/** + * Strip the GSD block from AGENTS.md content. Returns null if the file became + * empty (was GSD-only), the unchanged content if no markers were found, or the + * cleaned content otherwise. + */ +function stripGsdFromAgentsMd(content) { + const openIndex = content.indexOf(GSD_AGENTS_MD_MARKER); + const closeIndex = content.indexOf(GSD_AGENTS_MD_CLOSE_MARKER); + if (openIndex !== -1 && closeIndex !== -1) { + const before = content.substring(0, openIndex).trimEnd(); + const after = content.substring(closeIndex + GSD_AGENTS_MD_CLOSE_MARKER.length).trimStart(); + const cleaned = (before + (before && after ? '\n\n' : '') + after).trim(); + if (!cleaned) return null; + return cleaned + '\n'; + } + return content; +} + +/** + * Write the full Cline runtime artifact set (directory-form rules + PreToolUse + * hook) into targetDir, migrating a legacy single-file `.clinerules` if present. + * For global installs, also merge the cross-tool ~/.agents/AGENTS.md target. + * + * Returns the list of manifest-relative paths written under targetDir (so the + * caller can hash-track them). + */ +function writeClineArtifacts(targetDir, isGlobalInstall) { + const written = []; + const clinerulesDir = path.join(targetDir, '.clinerules'); + + // Migrate a pre-#787 single-file `.clinerules` — a path cannot be both a + // file and a directory, so the legacy file must be removed first. The legacy + // file is GSD-authored (the installer wrote its full contents with no user + // merge surface), so replacing it with the newer directory form is the + // intended upgrade. Use lstat so a symlink is unlinked in place rather than + // followed (which would write GSD files through the link into an external dir). + try { + if (fs.existsSync(clinerulesDir)) { + const st = fs.lstatSync(clinerulesDir); + if (st.isFile() || st.isSymbolicLink()) { + fs.unlinkSync(clinerulesDir); + console.log(` ${green}✓${reset} Migrated legacy .clinerules to directory form`); + } + } + } catch { /* best-effort migration */ } + + fs.mkdirSync(clinerulesDir, { recursive: true }); + fs.writeFileSync(path.join(clinerulesDir, 'gsd.md'), buildClineRulesBody()); + written.push('.clinerules/gsd.md'); + console.log(` ${green}✓${reset} Wrote .clinerules/gsd.md`); + + const hooksDir = path.join(clinerulesDir, 'hooks'); + fs.mkdirSync(hooksDir, { recursive: true }); + const hookPath = path.join(hooksDir, 'PreToolUse'); + fs.writeFileSync(hookPath, buildClinePreToolUseHook()); + try { fs.chmodSync(hookPath, 0o755); } catch { /* Windows: hooks unsupported anyway */ } + written.push('.clinerules/hooks/PreToolUse'); + console.log(` ${green}✓${reset} Wrote .clinerules/hooks/PreToolUse`); + + // Global cross-tool instruction target. Cline reads ~/.agents/AGENTS.md + // (docs.cline.bot/customization/cline-rules). Merge-safe so we never clobber + // a user's or another tool's AGENTS.md. Tracked via markers (like copilot), + // not the per-configDir manifest, since it lives outside configDir. + if (isGlobalInstall) { + try { + const agentsPath = path.join(os.homedir(), '.agents', 'AGENTS.md'); + mergeGsdAgentsMd(agentsPath, buildClineAgentsMdBody()); + console.log(` ${green}✓${reset} Merged GSD instructions into ~/.agents/AGENTS.md`); + } catch (err) { + console.warn(` ${yellow}⚠${reset} Could not write ~/.agents/AGENTS.md: ${err.message}`); + } + } + + return written; +} + +// ── Cursor hooks.json reconciler (issue #777) ──────────────────────────────── +// +// Cursor v2.4+ supports a hooks.json lifecycle hook system. GSD registers two +// managed command hooks: +// sessionStart → gsd-cursor-session-start.js (context injection) +// postToolUse → gsd-cursor-post-tool.js (STATE.md update monitor) +// +// hooks.json schema: +// { "version": 1, "hooks": { "": [ { "type": "command", "command": "" } ] } } +// +// Location: +// Global: ~/.cursor/hooks.json +// Local: /.cursor/hooks.json +// +// GSD entries are identified by a top-level `"gsd-managed": true` field on +// each hook entry. Non-GSD entries are preserved. The reconciler is idempotent +// (safe to re-run) and preserves user-owned entries in the file. +// +// References: https://cursor.com/docs/hooks + +/** + * Build a managed Cursor hook entry for a given hook script path. + * + * @param {string} scriptPath - Absolute path to the hook script + * @returns {object} Cursor hook entry object + */ +function buildCursorHookEntry(scriptPath) { + return { + type: 'command', + command: scriptPath.replace(/\\/g, '/'), + [GSD_CURSOR_HOOK_MARKER]: true, + }; +} + +/** + * Return true if a Cursor hook entry is GSD-managed. + * Detection: presence of the GSD_CURSOR_HOOK_MARKER sentinel field. + * + * @param {object} entry - A hooks array element from hooks.json + * @returns {boolean} + */ +function isManagedCursorHookEntry(entry) { + return Boolean(entry && typeof entry === 'object' && entry[GSD_CURSOR_HOOK_MARKER]); +} + +/** + * Reconcile the GSD-managed entries in a Cursor hooks.json file. + * + * Supports both known hooks.json shapes: + * 1) { "version": 1, "hooks": { "sessionStart": [...], "postToolUse": [...] } } + * 2) { "sessionStart": [...], "postToolUse": [...] } (no wrapper object) + * + * Managed entries (those with GSD_CURSOR_HOOK_MARKER) are removed then + * re-added if managedEntries is non-null/non-empty. User-owned entries are + * preserved. File is written atomically only when content changes. + * + * @param {string} hooksJsonPath - Absolute path to the hooks.json file + * @param {{ sessionStart?: object|null, postToolUse?: object|null }|null} managedEntries + * Map from event name to the new hook entry to register (or null to remove). + * Pass null for the whole param to remove all managed entries. + * @returns {{ changed: boolean, wrote: boolean, path: string }} + */ +function reconcileCursorHooksJson(hooksJsonPath, managedEntries) { + let parsed = {}; + let currentContent = null; + + if (fs.existsSync(hooksJsonPath)) { + const raw = fs.readFileSync(hooksJsonPath, 'utf8'); + currentContent = raw; + if (raw.trim()) { + try { + parsed = JSON.parse(raw); + } catch (err) { + throw new Error(`Cursor hooks.json parse failed: ${err && err.message ? err.message : String(err)}`); + } + } + } + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) parsed = {}; + + // Cursor's canonical hooks.json schema is { "version": 1, "hooks": { ... } }. + // GSD always writes (and migrates to) the nested shape so Cursor reads it correctly. + // The flat shape { "sessionStart": [...] } is accepted on read for backwards compat + // with manually-written files, but the output always uses the nested form. + const hasNestedHooksObject = + parsed.hooks && typeof parsed.hooks === 'object' && !Array.isArray(parsed.hooks); + if (!hasNestedHooksObject) { + // Migrate flat shape (or empty {}) to nested: lift event keys into hooks:{}. + const eventKeys = ['sessionStart', 'postToolUse']; + const lifted = {}; + for (const k of eventKeys) { + if (Array.isArray(parsed[k])) { + lifted[k] = parsed[k]; + delete parsed[k]; + } + } + parsed.hooks = lifted; + } + if (!parsed.version) parsed.version = 1; + const hookTable = parsed.hooks; + + // Events GSD manages. + const MANAGED_EVENTS = ['sessionStart', 'postToolUse']; + const entries = managedEntries || {}; + + for (const event of MANAGED_EVENTS) { + const existing = Array.isArray(hookTable[event]) ? hookTable[event] : []; + // Strip all prior GSD-managed entries for this event. + const userOwned = existing.filter((e) => !isManagedCursorHookEntry(e)); + const newEntry = entries[event] || null; + if (newEntry) { + hookTable[event] = [...userOwned, newEntry]; + } else { + // Remove-only: keep user entries, or delete the key if it would be empty. + if (userOwned.length > 0) { + hookTable[event] = userOwned; + } else { + delete hookTable[event]; + } + } + } + + // hookTable is parsed.hooks (always nested now); no reassignment needed. + // Write only if content changed or if we're creating the file for the first time. + const nextContent = `${JSON.stringify(parsed, null, 2)}\n`; + const changed = currentContent !== nextContent; + const shouldWrite = changed && (currentContent !== null || Object.keys(parsed).length > 0); + if (shouldWrite) { + atomicWriteFileSync(hooksJsonPath, nextContent, 'utf8'); + } + + return { changed: changed, wrote: shouldWrite, path: hooksJsonPath }; +} + +/** + * #777 — Write GSD-managed Cursor lifecycle hooks into /hooks.json. + * + * Both managed hook scripts (gsd-cursor-session-start.js, gsd-cursor-post-tool.js) + * are copied from the GSD hooks/ source to /hooks/ first, so the + * hooks.json entries never reference a script that wasn't installed. + * + * @param {string} targetDir - The Cursor config dir (global: ~/.cursor; local: .cursor) + * @param {string} src - The GSD install source root (for copying hook scripts) + * @param {{ absoluteRunner?: string|null }} opts + * @returns {{ hooksJsonPath: string, changed: boolean }} + */ +function writeCursorHooksJson(targetDir, src, opts) { + opts = opts || {}; + const hooksDir = path.join(targetDir, 'hooks'); + fs.mkdirSync(hooksDir, { recursive: true }); + + // Copy the two GSD-managed hook scripts from the GSD source hooks/ directory. + // Apply the same /gsd:/gi → gsd- rewrite used by copyWithPathReplacement for Cursor + // JS files, so the installed hook scripts contain no /gsd: colon refs (bug-376 2b). + // Track which scripts were successfully installed so we never register a hook entry + // that references a script that wasn't copied (dangling command guard). + const hookScripts = [GSD_CURSOR_SESSION_HOOK_SCRIPT, GSD_CURSOR_POST_TOOL_HOOK_SCRIPT]; + const srcHooksDir = path.join(src, 'hooks'); + const installedScripts = new Set(); + for (const script of hookScripts) { + const srcPath = path.join(srcHooksDir, script); + const destPath = path.join(hooksDir, script); + if (fs.existsSync(srcPath)) { + let content = fs.readFileSync(srcPath, 'utf8'); + // Rewrite /gsd: → gsd- so installed hook scripts are consistent + // with the Cursor convention (no colon-form slash commands in agent context). + content = content.replace(/gsd:/gi, 'gsd-'); + fs.writeFileSync(destPath, content); + try { fs.chmodSync(destPath, 0o755); } catch { /* Windows: ignore chmod */ } + installedScripts.add(script); + } + } + + // Build command strings using the same buildHookCommand helper used by other runtimes. + // buildHookCommand resolves the node runner + emits "" "/hooks/". + const hookOpts = { runtime: 'cursor', platform: opts.platform || process.platform }; + // buildHookCommand('gsd-cursor-session-start.js', ...): sessionStart → context injection + // Only register the hook entry if the script was actually installed (dangling guard). + const sessionStartCmd = installedScripts.has('gsd-cursor-session-start.js') + ? buildHookCommand(targetDir, 'gsd-cursor-session-start.js', hookOpts) + : null; + // buildHookCommand('gsd-cursor-post-tool.js', ...): postToolUse → STATE.md update monitor + const postToolCmd = installedScripts.has('gsd-cursor-post-tool.js') + ? buildHookCommand(targetDir, 'gsd-cursor-post-tool.js', hookOpts) + : null; + + // Build managed entries; skip events whose command couldn't be resolved (e.g. no node). + const managedEntries = {}; + if (sessionStartCmd) { + managedEntries.sessionStart = { + type: 'command', + command: sessionStartCmd, + [GSD_CURSOR_HOOK_MARKER]: true, + }; + } + if (postToolCmd) { + managedEntries.postToolUse = { + type: 'command', + command: postToolCmd, + [GSD_CURSOR_HOOK_MARKER]: true, + }; + } + + const hooksJsonPath = path.join(targetDir, 'hooks.json'); + const result = reconcileCursorHooksJson(hooksJsonPath, managedEntries); + return { hooksJsonPath, changed: result.changed }; +} + +/** + * Remove all GSD-managed Cursor lifecycle hook entries from hooks.json. + * User-owned entries are preserved. If the file becomes empty, it is removed. + * + * @param {string} targetDir - The Cursor config dir + * @returns {{ changed: boolean }} + */ +function removeCursorHooksJson(targetDir) { + const hooksJsonPath = path.join(targetDir, 'hooks.json'); + if (!fs.existsSync(hooksJsonPath)) return { changed: false }; + const result = reconcileCursorHooksJson(hooksJsonPath, null); + // If the resulting file has no meaningful hook content, remove it. + // A file is "empty" if it contains only the scaffolding (version, empty hooks + // object, or a bare {}) with no user-authored hook entries. + if (result.changed) { + try { + const contentRaw = fs.readFileSync(hooksJsonPath, 'utf8'); + const parsed = JSON.parse(contentRaw); + // reconcileCursorHooksJson always writes the nested { version, hooks:{} } shape. + // The file is "empty" when there are no remaining hook events with entries. + const hookTable = (parsed.hooks && typeof parsed.hooks === 'object' && !Array.isArray(parsed.hooks)) + ? parsed.hooks + : {}; + const hasAnyEvents = Object.keys(hookTable).some( + (k) => Array.isArray(hookTable[k]) && hookTable[k].length > 0, + ); + if (!hasAnyEvents) { + fs.unlinkSync(hooksJsonPath); + return { changed: true }; + } + } catch { /* best-effort: leave the file */ } + } + return { changed: result.changed }; +} + +/** + * #786 — Build the GSD-managed GitHub Copilot lifecycle hook config object. + * + * Returns the verbatim JSON shape Copilot CLI expects: + * { version: 1, hooks: { sessionStart: [ ] } } + * + * The sessionStart entry is a `command` hook whose `bash`/`powershell` bodies + * run inline (no external script file), so the config can never reference a + * hook script that the installer did not also install — it is self-contained + * by construction. The command is advisory-only (always exits 0) and orients + * the agent toward the project's GSD planning state at session start. + * + * @returns {object} Copilot hooks-configuration object + */ +function buildCopilotHookConfig() { + return { + version: 1, + hooks: { + sessionStart: [ + { + type: 'command', + bash: GSD_COPILOT_SESSION_HOOK_BASH, + powershell: GSD_COPILOT_SESSION_HOOK_PWSH, + timeoutSec: 10, + }, + ], + }, + }; +} + +/** + * #786 — Write the GSD-managed Copilot lifecycle hook config under the runtime + * config dir (`/hooks/gsd-session.json`). For local installs + * targetDir is `.github` (→ `.github/hooks/`); for global installs it is + * `~/.copilot` (→ `~/.copilot/hooks/`) — both are valid Copilot hook locations. + * + * The managed file is fully owned by GSD, so it is overwritten wholesale on + * every install (idempotent). User-authored sibling `*.json` hook files in the + * same directory are untouched. + * + * @param {string} targetDir - The Copilot config dir + * @returns {string} The path the hook config was written to + */ +function writeCopilotHookConfig(targetDir) { + const hooksDir = path.join(targetDir, 'hooks'); + fs.mkdirSync(hooksDir, { recursive: true }); + const hookPath = path.join(hooksDir, GSD_COPILOT_HOOK_FILE); + fs.writeFileSync(hookPath, JSON.stringify(buildCopilotHookConfig(), null, 2) + '\n'); + return hookPath; +} + /** * Generate config.toml and per-agent .toml files for Codex. * Reads agent .md files from source, extracts metadata, writes .toml configs. @@ -5393,7 +6216,7 @@ function convertSlashCommandsToGeminiMentions(content) { }); } -function convertClaudeToGeminiMarkdown(content, { isCommand = false } = {}) { +function convertClaudeToGeminiMarkdown(content, { isCommand = false, commandName = null } = {}) { // Apply Gemini-specific slash command namespacing let converted = convertSlashCommandsToGeminiMentions(content); // Gemini CLI does not expose Claude's AskUserQuestion tool. Convert body @@ -5405,8 +6228,9 @@ function convertClaudeToGeminiMarkdown(content, { isCommand = false } = {}) { converted = stripSubTags(converted); if (isCommand) { - // Convert to Gemini TOML format - converted = convertClaudeToGeminiToml(converted); + // Convert to Gemini TOML format (threads the command name so per-command + // enrichment — e.g. the #778 live-state injection — can target a command). + converted = convertClaudeToGeminiToml(converted, { commandName }); } return converted; @@ -5841,12 +6665,85 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) { return `---\n${newFrontmatter}\n---${body}`; } +/** + * Shared SKILL.md writer for the OpenCode-family runtimes (OpenCode + Kilo), + * which share a config schema (Kilo derives from OpenCode). OpenCode discovers + * skills as `skills//SKILL.md` and Kilo follows the same layout + * (https://opencode.ai/docs/skills, https://kilo.ai/docs/customize/skills). + * + * The skill body reuses the runtime's command-frontmatter converter for tool, + * path, and `/gsd:`→`/gsd-` body rewrites, then rebuilds a minimal skill + * frontmatter: only `name` (lowercase-hyphen, must match the containing + * directory) and `description` (1–1024 chars) are emitted, per the OpenCode + * skill spec. The command's `tools:`/`permission:` block is intentionally + * dropped — OpenCode skills are loaded on-demand via the native skill tool and + * inherit the calling agent's permissions. + * + * @param {string} content - Claude command markdown (with YAML frontmatter) + * @param {string} skillName - Skill directory name (e.g. gsd-help) + * @param {(content: string) => string} frontmatterConverter - runtime command converter + * @returns {string} SKILL.md content + */ +function convertClaudeCommandToOpencodeFamilySkill(content, skillName, frontmatterConverter) { + const converted = frontmatterConverter(content); + const { frontmatter, body } = extractFrontmatterAndBody(converted); + let description = `Run GSD workflow ${skillName}.`; + if (frontmatter) { + const maybeDescription = extractFrontmatterField(frontmatter, 'description'); + if (maybeDescription) { + description = maybeDescription; + } + } + description = toSingleLine(description); + // OpenCode skill descriptions must be 1–1024 characters. + if (description.length > 1024) { + description = `${description.slice(0, 1021)}...`; + } + // `name` must be lowercase alphanumeric with single-hyphen separators and + // match the containing directory name (the staged dir is `${skillName}/`). + const name = yamlIdentifier(skillName); + return `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n---\n\n${body.trimStart()}`; +} + +/** + * Convert a Claude command (.md) to an OpenCode skill (SKILL.md). + * Thin wrapper over the shared OpenCode-family writer. + */ +function convertClaudeCommandToOpencodeSkill(content, skillName) { + return convertClaudeCommandToOpencodeFamilySkill( + content, + skillName, + (c) => convertClaudeToOpencodeFrontmatter(c), + ); +} + +/** + * Convert a Claude command (.md) to a Kilo skill (SKILL.md). + * Thin wrapper over the shared OpenCode-family writer (Kilo shares the schema). + */ +function convertClaudeCommandToKiloSkill(content, skillName) { + return convertClaudeCommandToOpencodeFamilySkill( + content, + skillName, + (c) => convertClaudeToKiloFrontmatter(c), + ); +} + /** * Convert Claude Code markdown command to Gemini TOML format * @param {string} content - Markdown file content with YAML frontmatter * @returns {string} - TOML content */ -function convertClaudeToGeminiToml(content) { +function convertClaudeToGeminiToml(content, { commandName = null } = {}) { + // #778 (c) — Gemini {{args}} interpolation. Claude's $ARGUMENTS placeholder + // maps to Gemini's {{args}} so inline argument references interpolate into the + // command body instead of being emitted as a dead literal. Applied before + // frontmatter parsing so every return path benefits (a command's frontmatter + // never contains $ARGUMENTS, so this is body-only in practice). Gemini injects + // {{args}} as typed outside shell blocks; we never place it inside a !{...} + // block, so there is no shell-escaping/injection interaction. + content = content.replace(/\$ARGUMENTS\b/g, '{{args}}'); + // Check if content has frontmatter if (!content.startsWith('---')) { return `prompt = ${JSON.stringify(content)}\n`; @@ -5858,7 +6755,27 @@ function convertClaudeToGeminiToml(content) { } const frontmatter = content.substring(3, endIndex).trim(); - const body = content.substring(endIndex + 3).trim(); + let body = content.substring(endIndex + 3).trim(); + + // #778 (c) — Gemini !{...} dynamic-output injection for the situational + // `progress` command (GSD's status/dashboard surface). Inject the live + // .planning/STATE.md so the model sees current project state without relying + // on session memory. + // + // SECURITY: the shell command is a FIXED `cat` with NO interpolated user + // input — no {{args}} appears inside the block — so there is no + // shell-injection vector. Gemini still shows its standard per-invocation + // confirmation dialog (verified behavior). `2>/dev/null` keeps an + // uninitialized project (missing STATE.md) from injecting stderr noise. + // Braces inside the block are balanced (none present), per Gemini's parser + // requirement. The append happens AFTER the {{args}} mapping above so the + // injected block can never accidentally carry interpolated arguments. + if (commandName === 'progress') { + body += '\n\n## Live project state\n' + + 'Current contents of `.planning/STATE.md` ' + + '(empty if the project is not yet initialized):\n\n' + + '!{cat .planning/STATE.md 2>/dev/null}\n'; + } // Extract description from frontmatter let description = ''; @@ -5893,6 +6810,31 @@ function convertClaudeToGeminiToml(content) { * @param {string} pathPrefix - Path prefix for file references * @param {string} runtime - Target runtime ('claude', 'opencode', or 'kilo') */ +/** + * Apply OpenCode-family (`opencode`/`kilo`) `@file` path-prefix rewrites to a + * RAW Claude command/skill body, BEFORE the frontmatter converter runs. + * + * This is the single source of truth shared by copyFlattenedCommands (commands) + * and installOpencodeFamilySkills (skills) so the two surfaces produce identical + * path references. Applying pathPrefix pre-conversion (rather than rewriting an + * already-converted body) is what avoids the converter's hardcoded default + * config dir leaking into --local / --config-dir installs, and the + * prefix-overlap double-rewrite hazard for custom dirs like `kilo-alt`. (#784) + * + * @param {string} content - raw Claude command markdown + * @param {string} runtime - 'opencode' or 'kilo' + * @param {string} pathPrefix - trailing-slash install-target prefix + * @returns {string} + */ +function applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix) { + content = content.replace(/~\/\.claude\//g, pathPrefix); + content = content.replace(/\$HOME\/\.claude\//g, pathPrefix); + content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`); + content = content.replace(/~\/\.opencode\//g, pathPrefix); + content = content.replace(/~\/\.kilo\//g, pathPrefix); + return content; +} + function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) { if (!fs.existsSync(srcDir)) { return; @@ -5925,16 +6867,7 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) { const destPath = path.join(destDir, destName); let content = fs.readFileSync(srcPath, 'utf8'); - const globalClaudeRegex = /~\/\.claude\//g; - const globalClaudeHomeRegex = /\$HOME\/\.claude\//g; - const localClaudeRegex = /\.\/\.claude\//g; - const opencodeDirRegex = /~\/\.opencode\//g; - const kiloDirRegex = /~\/\.kilo\//g; - content = content.replace(globalClaudeRegex, pathPrefix); - content = content.replace(globalClaudeHomeRegex, pathPrefix); - content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`); - content = content.replace(opencodeDirRegex, pathPrefix); - content = content.replace(kiloDirRegex, pathPrefix); + content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix); content = processAttribution(content, getCommitAttribution(runtime)); content = runtime === 'kilo' ? convertClaudeToKiloFrontmatter(content) @@ -6119,7 +7052,7 @@ function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = ' if (runtime) { const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope); const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills'); - if (!skillsKindEntry) return false; // runtime has no skills layout (e.g. cline) + if (!skillsKindEntry) return false; // runtime has no skills layout at this scope (e.g. cline local) const stemName = skillsKindEntry.prefix === '' ? 'dev-preferences' : 'gsd-dev-preferences'; skillDir = path.join(targetDir, skillsKindEntry.destSubpath, stemName); } else { @@ -6176,6 +7109,46 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix) { walkAndRewrite(stagedDir); } +/** + * Apply per-runtime content rewrites to flat .md files in a staged commands dir. + * Used for runtimes that have a commandsKind in their layout and need content rewrites + * (e.g. augment — replaces ~/.claude/ paths and applies branding conversions). + * + * IMPORTANT: `stageSkillsForProfile()` returns the original source directory unchanged + * on a full/default profile (skills === '*'). This function MUST NOT mutate that source + * directory. It always copies to a temp dir first, rewrites there, and returns the new + * path so the caller installs from the temp copy, not the source. + * + * @param {string} stagedDir directory of staged flat .md command files (may be source dir) + * @param {string} runtime + * @param {string} pathPrefix + * @returns {string} path to a temp dir with rewritten files (caller is responsible for cleanup) + */ +function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix) { + if (!fs.existsSync(stagedDir)) return stagedDir; + // Always copy to a temp dir — stageSkillsForProfile() returns the original source + // dir on full/default profile (skills === '*'), so writing in-place would corrupt the + // package source. A temp copy is unconditional to keep the code simple and safe. + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cmd-rewrites-')); + try { + for (const entry of fs.readdirSync(stagedDir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith('.md')) continue; + let content = fs.readFileSync(path.join(stagedDir, entry.name), 'utf8'); + content = _applyRuntimeRewrites(content, runtime, pathPrefix); + // For augment commands, apply the markdown conversion so tool references + // and skill paths use Augment equivalents. + if (runtime === 'augment') { + content = convertClaudeToAugmentMarkdown(content); + } + fs.writeFileSync(path.join(tempDir, entry.name), content); + } + } catch (err) { + try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { /* best-effort */ } + throw err; + } + return tempDir; +} + /** * Apply the per-runtime rewrite table to a single content string. * Extracted so it can be unit-tested independently of the filesystem walk. @@ -6198,6 +7171,22 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix) { content = processAttribution(content, getCommitAttribution(runtime)); break; + case 'cline': + // Slash forms: both the original ~/.claude/ (safety net) and the stage-time + // converted ~/.cline/ (from convertClaudeToCliineMarkdown) → pathPrefix + content = content.replace(/~\/\.claude\//g, pathPrefix); + content = content.replace(/\$HOME\/\.claude\//g, pathPrefix); + content = content.replace(/\.\/\.claude\//g, `./${dirName}/`); + content = content.replace(/~\/\.cline\//g, pathPrefix); + content = content.replace(/\$HOME\/\.cline\//g, pathPrefix); + // Bare forms (no trailing slash) + content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix); + content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix); + content = content.replace(/~\/\.cline\b/g, normalizedPathPrefix); + content = content.replace(/\$HOME\/\.cline\b/g, normalizedPathPrefix); + content = processAttribution(content, getCommitAttribution(runtime)); + break; + case 'cursor': content = content.replace(/~\/\.claude\//g, pathPrefix); content = content.replace(/\$HOME\/\.claude\//g, pathPrefix); @@ -6240,7 +7229,14 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix) { content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix); content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix); content = content.replace(/\.\/\.claude\b/g, `./${dirName}`); + // The codebuddy converter rewrites `.claude/` → `.codebuddy/` at stage + // time, so `$HOME/.claude/...` arrives here as `$HOME/.codebuddy/...`. + // Normalize BOTH the `~/` and `$HOME/` forms (slash + bare) to the install + // target so `--config-dir`/local installs don't leak the default home. content = content.replace(/~\/\.codebuddy\//g, pathPrefix); + content = content.replace(/\$HOME\/\.codebuddy\//g, pathPrefix); + content = content.replace(/~\/\.codebuddy\b/g, normalizedPathPrefix); + content = content.replace(/\$HOME\/\.codebuddy\b/g, normalizedPathPrefix); content = processAttribution(content, getCommitAttribution(runtime)); break; @@ -6295,7 +7291,11 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix) { break; default: - // Unknown runtime — no rewrites + // Unknown runtime — no rewrites. + // OpenCode/Kilo are intentionally absent: their skills are written by + // installOpencodeFamilySkills, which applies pathPrefix BEFORE the + // command→skill conversion (mirroring copyFlattenedCommands) rather than + // rewriting already-converted SKILL.md bodies. See #784. break; } @@ -6558,8 +7558,14 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) { for (const kind of layout.kinds) { const staged = kind.stage(resolvedProfile); + // stagedForCopy: the directory to copy from (may differ from staged if rewrites + // produce a temp copy — see applyRuntimeContentRewritesForCommandsInPlace). + let stagedForCopy = staged; if (kind.kind === 'skills') { applyRuntimeContentRewritesInPlace(staged, runtime, pathPrefix); + } else if (kind.kind === 'commands') { + // Returns a temp dir with rewritten content so source files are never mutated. + stagedForCopy = applyRuntimeContentRewritesForCommandsInPlace(staged, runtime, pathPrefix); } const dest = path.join(layout.configDir, kind.destSubpath); fs.mkdirSync(dest, { recursive: true }); @@ -6581,8 +7587,8 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) { if (kind.prefix === '') { // Hermes: wipes entire dest dir — preserve anything not in staged. - const stagedNames = fs.existsSync(staged) - ? new Set(fs.readdirSync(staged, { withFileTypes: true }) + const stagedNames = fs.existsSync(stagedForCopy) + ? new Set(fs.readdirSync(stagedForCopy, { withFileTypes: true }) .filter(e => e.isDirectory()).map(e => e.name)) : new Set(); for (const entry of fs.readdirSync(dest, { withFileTypes: true })) { @@ -6603,7 +7609,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) { } _removeGsdEntries(dest, kind); - _copyStaged(staged, dest, kind); + _copyStaged(stagedForCopy, dest, kind); // Restore user-owned dirs after the prune+copy for (const [dirName, snap] of toPreserve) { @@ -6613,11 +7619,92 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) { // For non-skills kinds (commands, agents): no user content to preserve; // just prune stale gsd-* entries and copy new ones. _removeGsdEntries(dest, kind); - _copyStaged(staged, dest, kind); + _copyStaged(stagedForCopy, dest, kind); } } } +/** + * Install the skills layout kind for an OpenCode-family runtime (OpenCode/Kilo). + * + * These runtimes do NOT go through installRuntimeArtifacts (their commands use a + * bespoke flattened-command writer), so this writes ONLY the skills kind + * alongside their existing command/ + agents/ surfaces. Uninstall is already + * layout-driven (uninstallRuntimeArtifacts iterates layout.kinds), so the + * skills/ dir is cleaned up automatically once the layout declares it. + * + * `rawCommandsDir` MUST be the SAME staged command directory the flattened + * command writer consumes (the caller passes its `_stageSkills()` output) so the + * command/ and skills/ surfaces always cover the identical, profile-resolved set + * — including the `--minimal`/`--core-only` alias path, which stages differently + * from a plain `--profile=core`. + * + * Mirrors copyFlattenedCommands exactly per file — pathPrefix rewrite → + * attribution → command→skill conversion — guaranteeing command/ and skills/ + * bodies match byte-for-byte for global, --local, and --config-dir installs. + * We deliberately do NOT use skillsKindEntry.stage(): that converts before any + * pathPrefix is known, so its bodies would carry the converter's hardcoded + * default config dir. (#784) + * + * @param {string} runtime - 'opencode' or 'kilo' + * @param {string} targetDir - resolved runtime config directory + * @param {string} rawCommandsDir - staged RAW Claude command dir (caller's _stageSkills output) + * @param {string} pathPrefix - computed config-path prefix for body rewrites + * @returns {number} number of gsd-* skill directories written + */ +function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPrefix) { + const layout = resolveRuntimeArtifactLayout(runtime, targetDir); + const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills'); + if (!skillsKindEntry) return 0; + const rawDir = rawCommandsDir; + if (!rawDir || !fs.existsSync(rawDir)) return 0; + + const converter = runtime === 'kilo' + ? convertClaudeCommandToKiloSkill + : convertClaudeCommandToOpencodeSkill; + + const dest = path.join(targetDir, skillsKindEntry.destSubpath); + fs.mkdirSync(dest, { recursive: true }); + + // Preserve user-owned GSD-prefixed skill dirs across the gsd-* prune. + // gsd-dev-preferences is generated by the user (via generate-dev-preferences) + // and lives at /skills/gsd-dev-preferences — _removeGsdEntries + // would otherwise wipe it. Mirrors the preservation in installRuntimeArtifacts + // (#2973). + const USER_OWNED_SKILL_DIRS = ['gsd-dev-preferences']; + const toPreserve = new Map(); // dirName -> Map + for (const dirName of USER_OWNED_SKILL_DIRS) { + const skillDir = path.join(dest, dirName); + if (!fs.existsSync(skillDir)) continue; + const snap = _snapshotDir(skillDir); + if (snap.size > 0) toPreserve.set(dirName, snap); + } + + _removeGsdEntries(dest, skillsKindEntry); + + let count = 0; + for (const entry of fs.readdirSync(rawDir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith('.md')) continue; + const stem = entry.name.slice(0, -3); + const skillName = `${skillsKindEntry.prefix}${stem}`; + let content = fs.readFileSync(path.join(rawDir, entry.name), 'utf8'); + content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix); + content = processAttribution(content, getCommitAttribution(runtime)); + content = converter(content, skillName); + const skillDir = path.join(dest, skillName); + fs.mkdirSync(skillDir, { recursive: true }); + fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content); + count++; + } + + // Restore user-owned dirs after the prune+copy. + for (const [dirName, snap] of toPreserve) { + _restoreDir(path.join(dest, dirName), snap); + } + + return count; +} + /** * Layout-driven uninstall orchestrator. * Runs legacy cleanup first, then uses resolveRuntimeArtifactLayout to @@ -6722,8 +7809,11 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand : convertClaudeToOpencodeFrontmatter(content); fs.writeFileSync(destPath, content); } else if (isGemini) { - // Apply Gemini-specific Markdown transformations (slash commands, TOML) - const processed = convertClaudeToGeminiMarkdown(content, { isCommand }); + // Apply Gemini-specific Markdown transformations (slash commands, TOML). + // #778: thread the command name (file stem) so per-command TOML + // enrichment (live-state injection) can target a specific command. + const geminiCommandName = isCommand ? entry.name.replace(/\.md$/, '') : null; + const processed = convertClaudeToGeminiMarkdown(content, { isCommand, commandName: geminiCommandName }); const finalPath = isCommand ? destPath.replace(/\.md$/, '.toml') : destPath; fs.writeFileSync(finalPath, processed); } else if (isCodex) { @@ -6968,7 +8058,10 @@ const GSD_UNINSTALL_HOOKS = [ 'gsd-statusline.js', 'gsd-check-update.js', 'gsd-check-update.cmd', + 'gsd-config-reload.js', 'gsd-context-monitor.js', + 'gsd-cursor-session-start.js', + 'gsd-cursor-post-tool.js', 'gsd-prompt-guard.js', 'gsd-read-guard.js', 'gsd-read-injection-scanner.js', @@ -7002,10 +8095,14 @@ function uninstall(isGlobal, runtime = 'claude') { const isCodebuddy = runtime === 'codebuddy'; const dirName = getDirName(runtime); - // Get the target directory based on runtime and install type + // Get the target directory based on runtime and install type. Cline local + // installs write to the project root (.clinerules/ lives at the root, not in + // a .cline/ subdir), mirroring the install() path resolution (#787). const targetDir = isGlobal - ? getGlobalDir(runtime, explicitConfigDir) - : path.join(process.cwd(), dirName); + ? getGlobalConfigDir(runtime, explicitConfigDir) + : runtime === 'cline' + ? process.cwd() + : path.join(process.cwd(), dirName); const locationLabel = isGlobal ? targetDir.replace(os.homedir(), '~') @@ -7028,6 +8125,24 @@ function uninstall(isGlobal, runtime = 'claude') { console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`); + // #786: AGENTS.md lives at the repo root (outside targetDir) for local Copilot + // installs, so its cleanup must run even when .github (targetDir) was already + // removed — i.e. BEFORE the "target directory missing" early-return below. + if (isCopilot && !isGlobal) { + const agentsMdPath = path.join(process.cwd(), 'AGENTS.md'); + if (fs.existsSync(agentsMdPath)) { + const content = fs.readFileSync(agentsMdPath, 'utf8'); + const cleaned = stripGsdFromCopilotInstructions(content); + if (cleaned === null) { + fs.unlinkSync(agentsMdPath); + console.log(` ${green}✓${reset} Removed AGENTS.md (was GSD-only)`); + } else if (cleaned !== content) { + fs.writeFileSync(agentsMdPath, cleaned); + console.log(` ${green}✓${reset} Cleaned GSD section from AGENTS.md`); + } + } + } + // Check if target directory exists if (!fs.existsSync(targetDir)) { console.log(` ${yellow}⚠${reset} Directory does not exist: ${locationLabel}`); @@ -7087,6 +8202,15 @@ function uninstall(isGlobal, runtime = 'claude') { removedCount++; console.log(` ${green}✓${reset} Removed managed Codex SessionStart hook from hooks.json`); } + + // #772: remove new Codex hook event registrations added by this enhancement. + for (const eventName of ['SubagentStart', 'Stop', 'PostToolUse']) { + const eventCleanup = removeCodexHooksJsonEvent(targetDir, eventName); + if (eventCleanup.changed) { + removedCount++; + console.log(` ${green}✓${reset} Removed managed Codex ${eventName} hook from hooks.json`); + } + } } // 1b. Non-layout Copilot side-effect: copilot-instructions.md cleanup @@ -7105,6 +8229,99 @@ function uninstall(isGlobal, runtime = 'claude') { console.log(` ${green}✓${reset} Cleaned GSD section from copilot-instructions.md`); } } + + // #786: remove the GSD-managed Copilot lifecycle hook config and prune the + // hooks dir if we left it empty. + const hookPath = path.join(targetDir, 'hooks', GSD_COPILOT_HOOK_FILE); + if (fs.existsSync(hookPath)) { + fs.unlinkSync(hookPath); + removedCount++; + console.log(` ${green}✓${reset} Removed Copilot lifecycle hook (${GSD_COPILOT_HOOK_FILE})`); + try { + const hooksDir = path.join(targetDir, 'hooks'); + if (fs.existsSync(hooksDir) && fs.readdirSync(hooksDir).length === 0) { + fs.rmdirSync(hooksDir); + } + } catch { /* non-fatal: leave a non-empty/locked hooks dir in place */ } + } + // Note: AGENTS.md (repo root) is cleaned earlier, before the targetDir + // existence early-return, since it lives outside targetDir (#786). + } + + // 1b-cline. Non-layout Cline side-effects (issue #787): remove the + // directory-form rules + PreToolUse hook, and strip the GSD block from the + // global cross-tool ~/.agents/AGENTS.md target. + if (runtime === 'cline') { + const clinerulesDir = path.join(targetDir, '.clinerules'); + for (const rel of ['gsd.md', path.join('hooks', 'PreToolUse')]) { + const p = path.join(clinerulesDir, rel); + try { + if (fs.existsSync(p)) { + fs.unlinkSync(p); + removedCount++; + } + } catch { /* best-effort */ } + } + // Also remove a legacy single-file .clinerules left by pre-#787 installs. + try { + if (fs.existsSync(clinerulesDir) && fs.statSync(clinerulesDir).isFile()) { + fs.unlinkSync(clinerulesDir); + removedCount++; + } + } catch { /* best-effort */ } + // Prune now-empty GSD-created directories (leave any user-added rule files). + for (const dir of [path.join(clinerulesDir, 'hooks'), clinerulesDir]) { + try { + if (fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.readdirSync(dir).length === 0) { + fs.rmdirSync(dir); + } + } catch { /* best-effort */ } + } + if (isGlobal) { + const agentsPath = path.join(os.homedir(), '.agents', 'AGENTS.md'); + try { + if (fs.existsSync(agentsPath)) { + const content = fs.readFileSync(agentsPath, 'utf8'); + const cleaned = stripGsdFromAgentsMd(content); + if (cleaned === null) { + fs.unlinkSync(agentsPath); + removedCount++; + console.log(` ${green}✓${reset} Removed ~/.agents/AGENTS.md (was GSD-only)`); + } else if (cleaned !== content) { + fs.writeFileSync(agentsPath, cleaned); + removedCount++; + console.log(` ${green}✓${reset} Cleaned GSD section from ~/.agents/AGENTS.md`); + } + } + } catch { /* best-effort */ } + } + } + + // 1b-cursor. Non-layout Cursor side-effects (issue #777): remove GSD-managed + // hook entries from hooks.json and clean up the managed hook scripts. + if (isCursor) { + const hooksJsonCleanup = removeCursorHooksJson(targetDir); + if (hooksJsonCleanup.changed) { + removedCount++; + console.log(` ${green}✓${reset} Removed GSD-managed Cursor hooks from hooks.json`); + } + // Remove the managed hook scripts (session-start + post-tool). + const hooksDir = path.join(targetDir, 'hooks'); + for (const script of [GSD_CURSOR_SESSION_HOOK_SCRIPT, GSD_CURSOR_POST_TOOL_HOOK_SCRIPT]) { + const p = path.join(hooksDir, script); + try { + if (fs.existsSync(p)) { + fs.unlinkSync(p); + removedCount++; + } + } catch { /* best-effort */ } + } + // Prune hooks/ if empty. + try { + if (fs.existsSync(hooksDir) && fs.readdirSync(hooksDir).length === 0) { + fs.rmdirSync(hooksDir); + } + } catch { /* best-effort */ } } // 1c. Claude local: remove commands/gsd/ (primary local install location). @@ -7305,8 +8522,14 @@ function uninstall(isGlobal, runtime = 'claude') { } // Remove GSD hooks from settings — per-hook granularity to preserve - // user hooks that share an entry with a GSD hook (#1755 followup) - for (const eventName of ['SessionStart', 'PostToolUse', 'AfterTool', 'PreToolUse', 'BeforeTool']) { + // user hooks that share an entry with a GSD hook (#1755 followup). + // Includes the 3 Qwen-only events added in #788 (SubagentStop, Stop, + // PreCompact, also registered for Claude in #770), the 3 Gemini-only + // events added in #776 (BeforeAgent, AfterAgent, BeforeModel), and the + // Claude-only FileChanged event added in #770 — safe to iterate for all + // runtimes; installs that don't register these events simply find no + // entries and skip. + for (const eventName of ['SessionStart', 'PostToolUse', 'AfterTool', 'PreToolUse', 'BeforeTool', 'SubagentStop', 'Stop', 'PreCompact', 'BeforeAgent', 'AfterAgent', 'BeforeModel', 'FileChanged']) { if (settings.hooks && settings.hooks[eventName]) { const before = JSON.stringify(settings.hooks[eventName]); settings.hooks[eventName] = settings.hooks[eventName] @@ -7339,6 +8562,37 @@ function uninstall(isGlobal, runtime = 'claude') { delete settings.hooks; } + // #768 — Remove GSD-owned Claude permissions from settings.json. + // Applies only to Claude uninstalls. Filter only the exact GSD-owned entries + // to preserve any user-added allow/deny entries. + // Uses a local flag to avoid the shared `settingsModified` producing a false + // "Removed GSD permissions" message when only hooks/statusline changed. + if (runtime === 'claude' && settings.permissions) { + let permissionsModified = false; + if (Array.isArray(settings.permissions.allow)) { + const before = settings.permissions.allow.length; + settings.permissions.allow = settings.permissions.allow.filter( + (e) => !GSD_CLAUDE_ALLOW_PERMISSIONS.includes(e) + ); + if (settings.permissions.allow.length !== before) { + permissionsModified = true; + } + } + if (Array.isArray(settings.permissions.deny)) { + const before = settings.permissions.deny.length; + settings.permissions.deny = settings.permissions.deny.filter( + (e) => !GSD_CLAUDE_DENY_PERMISSIONS.includes(e) + ); + if (settings.permissions.deny.length !== before) { + permissionsModified = true; + } + } + if (permissionsModified) { + settingsModified = true; + console.log(` ${green}✓${reset} Removed GSD permissions from settings.json`); + } + } + if (settingsModified) { writeSettings(settingsPath, settings); removedCount++; @@ -7517,7 +8771,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) { // For local installs, use ./.opencode/ // For global installs, use ~/.config/opencode/ const opencodeConfigDir = configDir || (isGlobal - ? getGlobalDir('opencode', explicitConfigDir) + ? getGlobalConfigDir('opencode', explicitConfigDir) : path.join(process.cwd(), '.opencode')); // Ensure config directory exists fs.mkdirSync(opencodeConfigDir, { recursive: true }); @@ -7597,7 +8851,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) { // For local installs, use ./.kilo/ // For global installs, use ~/.config/kilo/ const kiloConfigDir = configDir || (isGlobal - ? getGlobalDir('kilo', explicitConfigDir) + ? getGlobalConfigDir('kilo', explicitConfigDir) : path.join(process.cwd(), '.kilo')); // Ensure config directory exists fs.mkdirSync(kiloConfigDir, { recursive: true }); @@ -7871,11 +9125,15 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { } } } - // Track .clinerules file in manifest for Cline installs + // Track Cline directory-form artifacts in the manifest (issue #787): the + // rules file and the PreToolUse hook. (~/.agents/AGENTS.md is tracked via its + // marker block, not the per-configDir manifest, since it lives outside it.) if (isCline) { - const clinerulesDest = path.join(configDir, '.clinerules'); - if (fs.existsSync(clinerulesDest)) { - manifest.files['.clinerules'] = fileHash(clinerulesDest); + for (const rel of ['.clinerules/gsd.md', '.clinerules/hooks/PreToolUse']) { + const dest = path.join(configDir, rel); + if (fs.existsSync(dest)) { + manifest.files[rel] = fileHash(dest); + } } } @@ -8240,6 +9498,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { const isHermes = runtime === 'hermes'; const isCodebuddy = runtime === 'codebuddy'; const isCline = runtime === 'cline'; + const configIntent = resolveRuntimeConfigIntent(runtime); const dirName = getDirName(runtime); const src = path.join(__dirname, '..'); @@ -8277,7 +9536,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { // Cline local installs write to the project root (like Claude Code) — .clinerules // lives at the root, not inside a .cline/ subdirectory. const targetDir = isGlobal - ? getGlobalDir(runtime, explicitConfigDir) + ? getGlobalConfigDir(runtime, explicitConfigDir) : isCline ? process.cwd() : path.join(process.cwd(), dirName); @@ -8575,6 +9834,10 @@ function install(isGlobal, runtime = 'claude', options = {}) { // agentsSrc is declared here (let, not const) because installCodexConfig() inside the // Codex config block below also references it, and that block is outside the try scope. let agentsSrc = path.join(src, 'agents'); + // Capture upgrade signal BEFORE files are written (#683). Must be declared at function + // scope (outside the try block below) so it is accessible in the settings section later. + // Absent VERSION = fresh install; present VERSION = upgrade/re-install. + const priorInstallExisted = fs.existsSync(path.join(targetDir, 'gsd-core', 'VERSION')); try { installerMigrationResult = runInstallerMigrations({ configDir: targetDir, @@ -8642,7 +9905,8 @@ function install(isGlobal, runtime = 'claude', options = {}) { // // Non-layout side-effects preserved inline: // Hermes: writeHermesCategoryDescription (not a layout kind) - // Cline: no-op (cline layout has empty kinds[]) + // Cline global: skills emitted via layout; .clinerules still written below (#782) + // Cline local: no skills (only .clinerules) — falls through to cline-rules surface // Gemini: conflict-detection logic (not expressible in layout) // OpenCode/Kilo: copyFlattenedCommands (frontmatter conversion not in commandsKind) // Claude local: copyWithPathReplacement + stale-skills cleanup @@ -8650,15 +9914,27 @@ function install(isGlobal, runtime = 'claude', options = {}) { // Layout-driven path for all skills-based runtimes (full and minimal modes). // applyRuntimeContentRewritesInPlace (called inside installRuntimeArtifacts) // handles per-runtime path + branding rewrites, including Qwen/Hermes. + // Cline global: emit skills to ~/.cline/skills/ (Cline >= v3.48.0 — #782). const _isSkillsRuntime = isCodex || isCopilot || isAntigravity || isCursor || isWindsurf || isAugment || isTrae || isCodebuddy || isQwen || isHermes || - (runtime === 'claude' && isGlobal); + (runtime === 'claude' && isGlobal) || + (isCline && isGlobal); if (_isSkillsRuntime) { // Layout-driven install for skills-based runtimes (full and minimal modes) const scope = isGlobal ? 'global' : 'local'; installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile); + // #774 — Codex only: write agents/openai.yaml TUI chip metadata alongside each + // installed skill so the /skills popup shows name + description for each gsd-* skill. + // The SkillMetadataFile is loaded by codex-rs/core-skills/src/loader.rs from + // /agents/openai.yaml; absence is silently tolerated (fails open). + // We parse the SKILL.md frontmatter to extract short-description already emitted + // by convertClaudeCommandToCodexSkill and use it as the TUI chip description. + if (isCodex) { + writeCodexSkillMetadataFiles(path.join(targetDir, 'skills')); + } + // Hermes only: write DESCRIPTION.md for the gsd/ category after layout install if (isHermes) { writeHermesCategoryDescription(path.join(targetDir, 'skills', 'gsd')); @@ -8692,6 +9968,53 @@ function install(isGlobal, runtime = 'claude', options = {}) { } else { failures.push('skills/gsd-*'); } + // Augment: also verify commands/ (emitted alongside skills/) + if (isAugment) { + const commandsDir = path.join(targetDir, 'commands'); + if (fs.existsSync(commandsDir)) { + const cmdCount = fs.readdirSync(commandsDir) + .filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length; + if (cmdCount > 0) { + console.log(` ${green}✓${reset} Installed ${cmdCount} commands to commands/`); + } else { + failures.push('commands/gsd-*'); + } + } else { + failures.push('commands/gsd-*'); + } + } + + // Cursor only: also report the commands/ output (#785 — Cursor 1.6 slash commands) + if (isCursor) { + const commandsDir = path.join(targetDir, 'commands'); + if (fs.existsSync(commandsDir)) { + const cmdCount = fs.readdirSync(commandsDir) + .filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length; + if (cmdCount > 0) { + console.log(` ${green}✓${reset} Installed ${cmdCount} slash commands to commands/`); + } else { + failures.push('commands/gsd-*'); + } + } else { + failures.push('commands/gsd-*'); + } + } + + // CodeBuddy only: also report the commands/ output (#789 — slash commands) + if (isCodebuddy) { + const commandsDir = path.join(targetDir, 'commands'); + if (fs.existsSync(commandsDir)) { + const cmdCount = fs.readdirSync(commandsDir) + .filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length; + if (cmdCount > 0) { + console.log(` ${green}✓${reset} Installed ${cmdCount} slash commands to commands/`); + } else { + failures.push('commands/gsd-*'); + } + } else { + failures.push('commands/gsd-*'); + } + } } } else if (isOpencode || isKilo) { // OpenCode/Kilo: flat structure in command/ directory @@ -8707,9 +10030,21 @@ function install(isGlobal, runtime = 'claude', options = {}) { } else { failures.push('command/gsd-*'); } + + // Also emit OpenCode-family skills (skills//SKILL.md). OpenCode and + // Kilo support native, on-demand skills in addition to flat commands — see + // resolveRuntimeArtifactLayout's opencode/kilo entries. Derive skills from + // the SAME staged command set (gsdSrc) so both surfaces match exactly. (#784) + const _skillCount = installOpencodeFamilySkills(runtime, targetDir, gsdSrc, pathPrefix); + if (_skillCount > 0) { + console.log(` ${green}✓${reset} Installed ${_skillCount} skills to skills/`); + } else { + failures.push('skills/gsd-*'); + } } else if (isCline) { - // Cline is rules-based — commands are embedded in .clinerules (generated below). - // No skills/commands directory needed. Engine is installed via copyWithPathReplacement. + // Cline local install: rules-based only — commands are embedded in .clinerules (generated below). + // No skills/commands directory needed for local installs. + // Global installs are handled above by _isSkillsRuntime (#782). console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`); } else if (isGemini) { // #3037: when running --local --gemini and a GSD-managed user-scope @@ -9089,8 +10424,10 @@ function install(isGlobal, runtime = 'claude', options = {}) { } // Gate hooks/lib/ install on the same runtimes that receive hooks (see line ~8702). - // Codex/Copilot/Cursor/Windsurf/Trae/Cline skip hooks entirely, so they must not - // receive the hooks/lib/ helpers either — otherwise the Codex comment downstream + // Codex/Copilot/Cursor/Windsurf/Trae/Cline do not use the shared hooks/lib/ helpers + // (Cursor uses standalone .js hook scripts registered via hooks.json; Codex uses + // hooks.json directly; the others skip hooks entirely), so they must not receive + // the hooks/lib/ helpers — otherwise the Codex comment downstream // ("we deliberately do *not* copy hooks/lib/ for Codex") is contradicted in practice. const hooksLibSrc = path.join(src, 'hooks', 'lib'); if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && fs.existsSync(hooksLibSrc)) { @@ -9196,7 +10533,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { throw _earlyInstallErr; } - if (isCodex && !isMinimalMode(_effectiveInstallMode)) { + if (configIntent.installSurface === 'codex-toml' && !isMinimalMode(_effectiveInstallMode)) { // Capture pre-install snapshots before ANY GSD mutation // (#2760 fix 3). On post-write schema-validation failure OR any throw // during the mutation sequence (write failure, merge throw, etc.) we @@ -9378,10 +10715,10 @@ function install(isGlobal, runtime = 'claude', options = {}) { } // Copy only the hook files that Codex actually registers via its hook configuration (#2153). - // Codex primarily needs gsd-check-update.js for the SessionStart update-check hook. + // #772: added gsd-context-monitor.js for the new SubagentStart/Stop/PostToolUse events. // We deliberately do *not* copy gsd-graphify-update.sh or hooks/lib/ for Codex // in this change (graphify auto-update support for Codex is out of scope for #3579). - const CODEX_HOOKS_TO_COPY = ['gsd-check-update.js']; + const CODEX_HOOKS_TO_COPY = ['gsd-check-update.js', 'gsd-context-monitor.js']; const codexHooksSrc = path.join(src, 'hooks', 'dist'); if (fs.existsSync(codexHooksSrc)) { const codexHooksDest = path.join(targetDir, 'hooks'); @@ -9511,6 +10848,39 @@ function install(isGlobal, runtime = 'claude', options = {}) { console.log(` ${green}✓${reset} Verified Codex hooks (SessionStart via hooks.json)`); } } + + // ── Codex extended hook events (#772) ──────────────────────────────── + // Codex CLI stabilised a full hook-event set in rust-v0.137.0. Register + // three new high-value lifecycle events — all routed through + // gsd-context-monitor.js so context-headroom warnings surface at: + // SubagentStart — subagent session open (environment / agent-name aware) + // Stop — model stop / session final-response moment + // PostToolUse — after each tool invocation (mirrors Claude baseline) + // + // Note: UserPromptSubmit is NOT wired — gsd-prompt-guard exits unless + // tool_name is Write|Edit (PreToolUse payload shape), so it would be a + // silent no-op for the UserPromptSubmit payload. Registration deferred + // to a follow-on issue. + // + // Guard: only register when the context-monitor file exists and the node + // runner is available — same guards as the SessionStart path above. + const contextMonitorFile = path.join(targetDir, 'hooks', 'gsd-context-monitor.js'); + if (codexNodeRunner && fs.existsSync(contextMonitorFile)) { + for (const codexEvent of ['SubagentStart', 'Stop', 'PostToolUse']) { + const eventWrite = ensureCodexHooksJsonEvent(targetDir, codexEvent, { + absoluteRunner: codexNodeRunner, + platform: process.platform, + }); + if (eventWrite.wrote) { + console.log(` ${green}✓${reset} Configured Codex hooks (${codexEvent} via hooks.json)`); + } else if (eventWrite.changed) { + console.log(` ${green}✓${reset} Verified Codex hooks (${codexEvent} via hooks.json)`); + } + } + } else if (!codexNodeRunner) { + console.warn(` ${yellow}⚠${reset} Skipped Codex SubagentStart/Stop/PostToolUse hook registration — Node runner unavailable.`); + } + // ── end Codex extended hook events ──────────────────────────────────── } } catch (e) { // #2760 — schema-validation and write failures must be loud and fatal @@ -9542,7 +10912,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir }; } - if (isCopilot) { + if (configIntent.installSurface === 'copilot-instructions') { // Generate copilot-instructions.md const templatePath = path.join(targetDir, 'gsd-core', 'templates', 'copilot-instructions.md'); const instructionsPath = path.join(targetDir, 'copilot-instructions.md'); @@ -9550,47 +10920,57 @@ function install(isGlobal, runtime = 'claude', options = {}) { const template = fs.readFileSync(templatePath, 'utf8'); mergeCopilotInstructions(instructionsPath, template); console.log(` ${green}✓${reset} Generated copilot-instructions.md`); + // #786: also emit AGENTS.md, which Copilot CLI reads as primary + // instructions from the repository root. AGENTS.md is a repo-root concept + // (no documented user-scope home), so emit it only for local installs; + // global scope is already covered by ~/.copilot/copilot-instructions.md. + if (!isGlobal) { + const agentsMdPath = path.join(process.cwd(), 'AGENTS.md'); + mergeCopilotInstructions(agentsMdPath, template); + console.log(` ${green}✓${reset} Generated AGENTS.md`); + } } - // Copilot: no settings.json, no hooks, no statusline (like Codex) + // #786: emit a self-contained Copilot lifecycle hook (sessionStart). Copilot + // command hooks run inline bash/powershell, so this needs no separate hook + // script and cannot dangle. Repo scope → .github/hooks/, user → ~/.copilot/hooks/. + // The hook is a required install artifact, so a write failure is fatal (it + // propagates) rather than silently producing a "successful" install missing + // the feature. + writeCopilotHookConfig(targetDir); + console.log(` ${green}✓${reset} Configured Copilot lifecycle hook (sessionStart)`); persistActiveProfileMarker(); return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir }; } - if (isCursor) { - // Cursor uses skills — no config.toml, no settings.json hooks needed + if (configIntent.installSurface === 'cursor-hooks-json') { + // #777: Cursor v2.4+ supports hooks.json. Register sessionStart + postToolUse. + // Hook scripts are copied to /hooks/ and referenced by hooks.json. + const cursorHookResult = writeCursorHooksJson(targetDir, src, {}); + if (cursorHookResult.changed) { + console.log(` ${green}✓${reset} Configured Cursor lifecycle hooks (sessionStart, postToolUse)`); + } else { + console.log(` ${green}✓${reset} Cursor lifecycle hooks already up to date`); + } + // Re-run the manifest pass so the hook scripts + hooks.json are hash-tracked. + writeManifest(targetDir, runtime, { mode: _effectiveInstallMode }); persistActiveProfileMarker(); return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir }; } - if (isWindsurf) { - // Windsurf uses skills — no config.toml, no settings.json hooks needed + if (configIntent.installSurface === 'profile-marker-only') { + // Windsurf/Trae use skills — no config.toml, no settings.json hooks needed persistActiveProfileMarker(); return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir }; } - if (isTrae) { - // Trae uses skills — no settings.json hooks needed - persistActiveProfileMarker(); - return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir }; - } - - if (isCline) { - // Cline uses .clinerules — generate a rules file with GSD system instructions - const clinerulesDest = path.join(targetDir, '.clinerules'); - const clinerules = [ - '# GSD Core — Git. Ship. Done.', - '', - '- GSD workflows live in `gsd-core/workflows/`. Load the relevant workflow when', - ' the user runs a `/gsd-*` command.', - '- GSD agents live in `agents/`. Use the matching agent when spawning subagents.', - '- GSD tools are at `gsd-core/bin/gsd-tools.cjs`. Run with `node`.', - '- Planning artifacts live in `.planning/`. Never edit them outside a GSD workflow.', - '- Do not apply GSD workflows unless the user explicitly asks for them.', - '- When a GSD command triggers a deliverable (feature, fix, docs), offer the next', - ' step to the user using Cline\'s ask_user tool after completing it.', - ].join('\n') + '\n'; - fs.writeFileSync(clinerulesDest, clinerules); - console.log(` ${green}✓${reset} Wrote .clinerules`); + if (configIntent.installSurface === 'cline-rules') { + // Cline uses the `.clinerules/` directory form (issue #787): GSD rules live + // at .clinerules/gsd.md and a PreToolUse lifecycle hook at + // .clinerules/hooks/PreToolUse. Global installs also get ~/.agents/AGENTS.md. + writeClineArtifacts(targetDir, isGlobal); + // Re-run the manifest pass: these artifacts are written *after* the earlier + // writeManifest() call, so a second pass is needed to hash-track them. + writeManifest(targetDir, runtime, { mode: _effectiveInstallMode }); persistActiveProfileMarker(); return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir }; } @@ -9741,6 +11121,9 @@ function install(isGlobal, runtime = 'claude', options = {}) { const readInjectionScannerCommand = isGlobal ? buildHookCommand(targetDir, 'gsd-read-injection-scanner.js', hookOpts) : localCmd('gsd-read-injection-scanner.js'); + const configReloadCommand = isGlobal + ? buildHookCommand(targetDir, 'gsd-config-reload.js', hookOpts) + : localCmd('gsd-config-reload.js'); // #3002 CR: when resolveNodeRunner() returns null, every dependent JS-hook // command is null too. Emit one warning here so the operator sees the cause @@ -10094,8 +11477,156 @@ function install(isGlobal, runtime = 'claude', options = {}) { } else if (!hasPhaseBoundaryHook && !phaseBoundaryCommand) { console.warn(` ${yellow}⚠${reset} Skipped phase boundary hook — Bash executable path unavailable (#3393)`); } + + // ── Extended hook events: SubagentStop / Stop / PreCompact (#788 + #770) ── + // Claude Code (since #770) and Qwen Code (since #788) both support these + // three lifecycle events. Wire gsd-context-monitor so agents get context- + // headroom warnings at subagent completion, model stop, and pre-compaction + // (the most critical moment to surface headroom info). + // + // SubagentStop — subagent lifecycle completion (context headroom tracking) + // Stop — model stop / final-response moment (context headroom) + // PreCompact — fires before conversation compaction (most critical + // moment to surface context headroom warnings) + // + // Note: UserPromptSubmit is NOT wired here. That event carries the raw + // user prompt text, not a tool invocation, so gsd-prompt-guard (which + // exits unless tool_name is Write/Edit) would be a silent no-op. A + // dedicated handler for UserPromptSubmit is deferred to a follow-on issue. + if (isQwen || runtime === 'claude') { + const runtimeLabel = isQwen ? 'Qwen Code' : 'Claude Code'; + // SubagentStop, Stop, PreCompact — route through the context monitor. + for (const event of ['SubagentStop', 'Stop', 'PreCompact']) { + if (!settings.hooks[event]) { + settings.hooks[event] = []; + } + const alreadyHasContextMonitor = settings.hooks[event].some(entry => + entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-context-monitor')) + ); + if (!alreadyHasContextMonitor && fs.existsSync(contextMonitorFile) && contextMonitorCommand) { + settings.hooks[event].push({ + hooks: [ + { + type: 'command', + command: contextMonitorCommand, + timeout: 10 + } + ] + }); + console.log(` ${green}✓${reset} Configured ${event} context monitor hook (${runtimeLabel})`); + } else if (!alreadyHasContextMonitor && !fs.existsSync(contextMonitorFile)) { + console.warn(` ${yellow}⚠${reset} Skipped ${event} hook — gsd-context-monitor.js not found at target`); + } + } + } + // ── end SubagentStop / Stop / PreCompact events ──────────────────────────── + + // ── Gemini-only extended hook events (#776) ─────────────────────────────── + // Gemini CLI exposes several hook events beyond BeforeTool/AfterTool that + // gsd previously did not register. Three high-value events are added here: + // + // BeforeAgent — fires after user submits a prompt, before the agent + // plans. Wire gsd-context-monitor for context headroom + // awareness at prompt time. + // AfterAgent — fires once per turn after the model generates its final + // response. Wire gsd-context-monitor to track headroom + // after each agent turn completes. + // BeforeModel — fires before each LLM call (per-turn, not per-session). + // Wire gsd-context-monitor for per-turn context injection + // — more precise than session-start-only injection. + // + // All three reuse gsd-context-monitor.js — no new hook files needed. + // The `decision:"deny"` retry capability of AfterAgent is intentionally + // left to the hook script to implement when triggered (gsd-context-monitor + // exits 0 / advisory-only today; an active quality gate is a follow-on). + // + // Note: BeforeToolSelection is NOT wired. That event does not map to a + // gsd hook use case at this time; deferred to a follow-on issue. + // + // Guard: isGemini is defined at the top of install() (line ~8696). + if (isGemini) { + for (const geminiEvent of ['BeforeAgent', 'AfterAgent', 'BeforeModel']) { + if (!Array.isArray(settings.hooks[geminiEvent])) { + settings.hooks[geminiEvent] = []; + } + const alreadyHasContextMonitor = settings.hooks[geminiEvent].some(entry => + entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-context-monitor')) + ); + if (!alreadyHasContextMonitor && fs.existsSync(contextMonitorFile) && contextMonitorCommand) { + settings.hooks[geminiEvent].push({ + hooks: [ + { + type: 'command', + command: contextMonitorCommand, + timeout: 10 + } + ] + }); + console.log(` ${green}✓${reset} Configured ${geminiEvent} context monitor hook (Gemini)`); + } else if (!alreadyHasContextMonitor && !fs.existsSync(contextMonitorFile)) { + console.warn(` ${yellow}⚠${reset} Skipped ${geminiEvent} hook — gsd-context-monitor.js not found at target`); + } + } + } + // ── end Gemini-only extended hook events ────────────────────────────────── + + // ── FileChanged hook: hot-reload gsd config on .planning/config.json edits ─ + // Claude Code fires FileChanged when a watched file changes on disk. Wire + // gsd-config-reload.js to reload the gsd config context whenever the user + // edits .planning/config.json mid-session, eliminating the need to restart. + // + // The matcher "config.json" watches for changes to any file named config.json + // (Claude Code matches by filename, not full path). The hook exits silently + // when the changed file is not the gsd config. + // + // Scoped to Claude Code only: Qwen Code's FileChanged support is not yet + // verified; extend in a follow-on if empirically confirmed. + if (runtime === 'claude') { + if (!settings.hooks.FileChanged) { + settings.hooks.FileChanged = []; + } + const configReloadFile = path.join(targetDir, 'hooks', 'gsd-config-reload.js'); + const alreadyHasConfigReload = settings.hooks.FileChanged.some(entry => + entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-config-reload')) + ); + if (!alreadyHasConfigReload && fs.existsSync(configReloadFile) && configReloadCommand) { + settings.hooks.FileChanged.push({ + matcher: 'config.json', + hooks: [ + { + type: 'command', + command: configReloadCommand, + timeout: 8 + } + ] + }); + console.log(` ${green}✓${reset} Configured FileChanged config-reload hook (Claude Code)`); + } else if (!alreadyHasConfigReload && !fs.existsSync(configReloadFile)) { + console.warn(` ${yellow}⚠${reset} Skipped FileChanged hook — gsd-config-reload.js not found at target`); + } else if (!alreadyHasConfigReload && !configReloadCommand) { + console.warn(` ${yellow}⚠${reset} Skipped FileChanged hook — Node executable path unavailable`); + } + } + // ── end FileChanged hook ──────────────────────────────────────────────────── } + // ── Gemini hooksConfig.enabled check (#776) ─────────────────────────────── + // Detect `hooksConfig.enabled: false` in the already-loaded settings object + // and emit a clear warning. When this field is false the Gemini CLI silently + // disables ALL hook execution — gsd hooks are registered but will never run. + // The check is read-only (warning only; we do not mutate hooksConfig). + // Note: we use the in-memory `settings` object (already read from disk and + // cleaned up by validateHookFields/cleanupOrphanedHooks above) rather than + // re-reading settings.json, avoiding a TOCTOU window between the two reads. + if (isGemini && settings && settings.hooksConfig && settings.hooksConfig.enabled === false) { + console.warn( + ` ${yellow}⚠${reset} Warning: hooksConfig.enabled is false in your Gemini settings.json.\n` + + ` gsd-core hooks are registered but will NOT run until you set\n` + + ` hooksConfig.enabled: true in ${path.join(targetDir, 'settings.json')}.` + ); + } + // ── end hooksConfig.enabled check ──────────────────────────────────────── + // Compute the update-banner hook command alongside the others so // installAllRuntimes can register it at finalize time when the user opts // in (#2795). Computed here (not in finishInstall) so the same buildHookCommand @@ -10106,6 +11637,68 @@ function install(isGlobal, runtime = 'claude', options = {}) { ? buildHookCommand(targetDir, 'gsd-update-banner.js', hookOpts) : localCmd('gsd-update-banner.js')); + // #683: Set worktree.baseRef:"head" in settings.local.json for local Claude installs. + // Both fresh and upgrade paths apply only when worktrees are enabled for the project. + // Never applies to global installs, non-Claude runtimes, or when the user already + // has an explicit baseRef in EITHER settings.local.json OR settings.json (no-clobber). + // Guard: skip entirely when settings is not a plain object (e.g. parsed to [] or primitive) + // to avoid crashing applyWorktreeBaseRef on unexpected top-level shapes. + if (isLocalClaude && settings !== null && typeof settings === 'object' && !Array.isArray(settings)) { + // Read shared settings.json baseRef so no-clobber spans both files (#683 FIX 1). + // shared settings.json no-clobber is checked here; settings.local.json no-clobber + // is enforced inside applyWorktreeBaseRef itself. + const sharedSettingsForBaseRef = readSettings(path.join(targetDir, 'settings.json')) || {}; + const sharedBaseRef = readBaseRefFromSettings(sharedSettingsForBaseRef); + + // Compute worktrees-enabled ONCE for both fresh and upgrade paths (FIX A: DRY + consistency). + // Read workflow.use_worktrees from .planning/config.json by walking up from + // targetDir (same walk-up pattern as readGsdRuntimeProfileResolver). Defaults + // to enabled (true) when the file is missing, unreadable, or the key is absent; + // only boolean false disables (string "false" stays enabled). + let worktreesEnabled = true; // default: enabled + try { + let probeDir = path.resolve(targetDir); + for (let depth = 0; depth < 8; depth += 1) { + const candidate = path.join(probeDir, '.planning', 'config.json'); + if (fs.existsSync(candidate)) { + try { + const parsed = JSON.parse(stripJsonComments(fs.readFileSync(candidate, 'utf-8'))); + if (parsed && typeof parsed === 'object' && + parsed.workflow && parsed.workflow.use_worktrees === false) { + worktreesEnabled = false; + } + } catch { + // Malformed config.json — treat as enabled (safe fallback). + } + break; + } + const parent = path.dirname(probeDir); + if (parent === probeDir) break; + probeDir = parent; + } + } catch { + // Any unexpected error reading .planning — default to enabled. + } + + if (worktreesEnabled && sharedBaseRef === null) { + if (!priorInstallExisted) { + // Fresh install — apply no-clobber baseRef set. + // canonical no-clobber logic: src/worktree-base-ref.cts applyWorktreeBaseRef (#683) + const { changed } = applyWorktreeBaseRef(settings); + if (changed) { + console.log(` ${green}✓${reset} Set worktree.baseRef:"head" for Claude Code worktrees (forks phase worktrees off HEAD; #683)`); + } + } else { + // Upgrade — auto-apply no-clobber baseRef set when worktrees are enabled. + const { changed } = applyWorktreeBaseRef(settings); + if (changed) { + console.log(` ${green}✓${reset} Enabled worktree.baseRef:"head" for Claude Code worktrees (forks phase worktrees off HEAD; #683)`); + } + } + } + // When worktreesEnabled is false: do nothing, print nothing (both fresh and upgrade). + } + persistActiveProfileMarker(); return { settingsPath, @@ -10130,6 +11723,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS const isWindsurf = runtime === 'windsurf'; const isTrae = runtime === 'trae'; const isCline = runtime === 'cline'; + const configIntent = resolveRuntimeConfigIntent(runtime); if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae) { if (!isGlobal && !forceStatusline) { @@ -10182,6 +11776,14 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS } } + // #768 — Pre-populate permissions.allow/deny for Claude Code installs. + // Merges GSD-owned entries non-destructively (preserves existing user permissions). + // Scoped to Claude only: gemini/antigravity/qwen/hermes/codebuddy also write + // settings.json but use different runtimes and do not use these permission strings. + if (runtime === 'claude') { + mergeClaudePermissions(settings); + } + // Write settings when runtime supports settings.json. // #3002 CR: defense-in-depth — re-run validateHookFields right before // serialization. The push-site guards above already skip null-command @@ -10189,17 +11791,17 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS // {type: 'command', command: null} items that the runtime hook schema // rejects at parse time. validateHookFields filters those out so the file // we write is always schema-valid. - if (!isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf && !isTrae && !isCline) { + if (configIntent.writesSharedSettings) { writeSettings(settingsPath, validateHookFields(settings)); } // Configure OpenCode permissions - if (isOpencode && !process.env.GSD_TEST_MODE) { + if (configIntent.finishPermissionWriter === 'opencode' && !process.env.GSD_TEST_MODE) { configureOpencodePermissions(isGlobal, configDir); } // Configure Kilo permissions - if (isKilo) { + if (configIntent.finishPermissionWriter === 'kilo') { configureKiloPermissions(isGlobal, configDir); } @@ -10543,7 +12145,7 @@ function promptLocation(runtimes) { }); const pathExamples = runtimes.map(r => { - const globalPath = getGlobalDir(r, explicitConfigDir); + const globalPath = getGlobalConfigDir(r, explicitConfigDir); return globalPath.replace(os.homedir(), '~'); }).join(', '); @@ -10915,8 +12517,10 @@ module.exports = { normalizeAgentBodyForRuntime, yamlIdentifier, computePathPrefix, + applyRuntimeContentRewritesInPlace, getCodexSkillAdapterHeader, convertClaudeCommandToCursorSkill, + convertClaudeCommandToCursorCommand, convertClaudeAgentToCursorAgent, convertClaudeToGeminiMarkdown, convertSlashCommandsToGeminiMentions, @@ -10924,6 +12528,8 @@ module.exports = { convertClaudeToGeminiAgent, convertClaudeAgentToCodexAgent, generateCodexAgentToml, + generateCodexSkillMetadataYaml, + writeCodexSkillMetadataFiles, generateCodexConfigBlock, stripGsdFromCodexConfig, migrateCodexHooksMapFormat, @@ -10943,15 +12549,21 @@ module.exports = { install, installAllRuntimes, uninstall, + convertSlashCommandsToCodexSkillMentions, convertClaudeCommandToCodexSkill, convertClaudeToOpencodeFrontmatter, convertClaudeToKiloFrontmatter, + convertClaudeCommandToOpencodeSkill, + convertClaudeCommandToKiloSkill, configureOpencodePermissions, neutralizeAgentReferences, + // #768 — Claude Code permissions pre-population + mergeClaudePermissions, + GSD_CLAUDE_ALLOW_PERMISSIONS, + GSD_CLAUDE_DENY_PERMISSIONS, GSD_CODEX_MARKER, CODEX_AGENT_SANDBOX, getDirName, - getGlobalDir, getConfigDirFromHome, resolveKiloConfigPath, configureKiloPermissions, @@ -10964,6 +12576,9 @@ module.exports = { GSD_COPILOT_INSTRUCTIONS_CLOSE_MARKER, mergeCopilotInstructions, stripGsdFromCopilotInstructions, + GSD_COPILOT_HOOK_FILE, + buildCopilotHookConfig, + writeCopilotHookConfig, convertClaudeToAntigravityContent, convertClaudeCommandToAntigravitySkill, convertClaudeAgentToAntigravityAgent, @@ -10980,9 +12595,27 @@ module.exports = { convertClaudeAgentToTraeAgent, convertClaudeToCodebuddyMarkdown, convertClaudeCommandToCodebuddySkill, + convertClaudeCommandToCodebuddyCommand, convertClaudeAgentToCodebuddyAgent, convertClaudeToCliineMarkdown, + convertClaudeCommandToClineSkill, convertClaudeAgentToClineAgent, + buildClineRulesBody, + buildClineAgentsMdBody, + buildClinePreToolUseHook, + writeClineArtifacts, + mergeGsdAgentsMd, + GSD_CURSOR_SESSION_HOOK_SCRIPT, + GSD_CURSOR_POST_TOOL_HOOK_SCRIPT, + GSD_CURSOR_HOOK_MARKER, + buildCursorHookEntry, + isManagedCursorHookEntry, + reconcileCursorHooksJson, + writeCursorHooksJson, + removeCursorHooksJson, + stripGsdFromAgentsMd, + GSD_AGENTS_MD_MARKER, + GSD_AGENTS_MD_CLOSE_MARKER, writeManifest, saveLocalPatches, reportLocalPatches, @@ -11011,11 +12644,16 @@ module.exports = { rewriteLegacyCodexHookBlock, buildCodexHookWindowsShimIR, ensureCodexHooksJsonSessionStart, + ensureCodexHooksJsonEvent, + removeCodexHooksJsonEvent, + reconcileCodexHooksJsonEvent, readGsdCommandNames, installRuntimeArtifacts, + installOpencodeFamilySkills, uninstallRuntimeArtifacts, parseConfigDirFromArgs, cleanupLegacyGsdCc, + _applyRuntimeRewrites, }; // Main logic — only run when not loaded as a module for testing @@ -11042,7 +12680,7 @@ if (require.main === module && !process.env.GSD_TEST_MODE) { console.error('Usage: node install.js --skills-root '); process.exit(1); } - const globalDir = getGlobalDir(runtimeArg, null); + const globalDir = getGlobalConfigDir(runtimeArg, null); // Hermes nests GSD skills under skills/gsd/ as a single category (#2841). // Other runtimes use a flat skills/ root. const skillsRoot = runtimeArg === 'hermes' diff --git a/commands/gsd/autonomous.md b/commands/gsd/autonomous.md index 1fc44fbca..fce955925 100644 --- a/commands/gsd/autonomous.md +++ b/commands/gsd/autonomous.md @@ -2,6 +2,8 @@ name: gsd:autonomous description: Run all remaining phases autonomously — discuss→plan→execute per phase argument-hint: "[--from N] [--to N] [--only N] [--interactive]" +context: fork +effort: xhigh allowed-tools: - Read - Write diff --git a/commands/gsd/execute-phase.md b/commands/gsd/execute-phase.md index 93542a765..b7acb5885 100644 --- a/commands/gsd/execute-phase.md +++ b/commands/gsd/execute-phase.md @@ -2,6 +2,8 @@ name: gsd:execute-phase description: Execute all plans in a phase with wave-based parallelization argument-hint: " [--wave N] [--gaps-only] [--interactive] [--tdd]" +context: fork +effort: xhigh allowed-tools: - Read - Write diff --git a/commands/gsd/graphify.md b/commands/gsd/graphify.md index 9a025eecd..5780a148c 100644 --- a/commands/gsd/graphify.md +++ b/commands/gsd/graphify.md @@ -79,7 +79,8 @@ Modes: Run: ```bash -node $HOME/.claude/gsd-core/bin/gsd-tools.cjs graphify query +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi +gsd_run graphify query ``` Parse the JSON output and display results: @@ -95,7 +96,8 @@ Parse the JSON output and display results: Run: ```bash -node $HOME/.claude/gsd-core/bin/gsd-tools.cjs graphify status +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi +gsd_run graphify status ``` Parse the JSON output and display: @@ -119,7 +121,8 @@ Surface both so the agent can choose. Run: ```bash -node $HOME/.claude/gsd-core/bin/gsd-tools.cjs graphify diff +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi +gsd_run graphify diff ``` Parse the JSON output and display: @@ -137,7 +140,8 @@ If no snapshot exists, suggest running `build` twice (first to create, second to Run the pre-flight check first: ```bash -node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify build +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi +gsd_run graphify build ``` Parse the JSON output: @@ -156,12 +160,13 @@ GSD > Building knowledge graph... Run the build, copy artifacts, write the diff snapshot, and report the summary in a single foreground Bash call so the whole pipeline survives to completion. Use a `timeout` of `600000` ms (10 minutes), which covers the `graphify.build_timeout` ceiling (default 300 s) with margin: ```bash +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi graphify update . \ && cp graphify-out/graph.json .planning/graphs/graph.json \ && { [ -f graphify-out/graph.html ] && cp graphify-out/graph.html .planning/graphs/graph.html || true; } \ && cp graphify-out/GRAPH_REPORT.md .planning/graphs/GRAPH_REPORT.md \ - && node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify build snapshot \ - && node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify status + && gsd_run graphify build snapshot \ + && gsd_run graphify status ``` Do NOT pass `run_in_background: true`. Typical builds complete in 15-60 seconds and the entire chain must run foreground. diff --git a/commands/gsd/import.md b/commands/gsd/import.md index b9c1370e4..2012bdbcf 100644 --- a/commands/gsd/import.md +++ b/commands/gsd/import.md @@ -33,8 +33,12 @@ $ARGUMENTS If `--from-gsd2` is in $ARGUMENTS: -Run: `node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" from-gsd2` -Pass `--path ` if provided. Present the migration result to the user. +Run the reverse-migration (append `--path ` if provided): +```bash +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi +gsd_run from-gsd2 +``` +Present the migration result to the user. Stop here (do not run the standard import workflow). Otherwise, execute the import workflow end-to-end. diff --git a/commands/gsd/plan-phase.md b/commands/gsd/plan-phase.md index 3e4314f8b..47549dfbf 100644 --- a/commands/gsd/plan-phase.md +++ b/commands/gsd/plan-phase.md @@ -2,6 +2,8 @@ name: gsd:plan-phase description: Create detailed phase plan (PLAN.md) with verification loop argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase ] [--view] [--gaps] [--skip-verify] [--prd ] [--ingest ] [--ingest-format ] [--reviews] [--text] [--tdd] [--mvp]" +context: fork +effort: xhigh allowed-tools: - Read - Write @@ -22,8 +24,8 @@ Create executable phase prompts (PLAN.md files) for a roadmap phase with integra **Research-only mode (`--research-phase `):** Spawn `gsd-phase-researcher` for phase `N`, write `RESEARCH.md`, then exit before the planner runs. Useful for cross-phase research, doc review before committing to a planning approach, and correction-without-replanning loops where iterating on research alone is dramatically cheaper than re-spawning the planner. Replaces the deleted research-phase command (#3042). **Research-only modifiers:** -- **No flag** — when `RESEARCH.md` already exists, prompt the user to choose `update / view / skip`. -- **`--research`** — force-refresh: re-spawn the researcher unconditionally, no prompt. Skips the existing-RESEARCH.md menu. +- **No flag** — when `RESEARCH.md` already exists, auto-uses it: emits a one-line notice and exits cleanly, no prompt. +- **`--research`** — force-refresh: re-spawn the researcher unconditionally, no prompt. Bypasses the existing-RESEARCH.md auto-use path. - **`--view`** — view-only: print existing `RESEARCH.md` to stdout. Does not spawn the researcher. Cheapest mode for the correction-without-replanning loop. If no `RESEARCH.md` exists yet, errors with a hint to drop `--view`. **Orchestrator role:** Parse arguments, validate phase, research domain (unless skipped), spawn gsd-planner, verify with gsd-plan-checker, iterate until pass or max iterations, present results. diff --git a/commands/gsd/progress.md b/commands/gsd/progress.md index 89c274a01..d35473d2c 100644 --- a/commands/gsd/progress.md +++ b/commands/gsd/progress.md @@ -2,6 +2,7 @@ name: gsd:progress description: Check progress, advance workflow, or dispatch freeform intent — the unified GSD situational command argument-hint: "[--forensic | --next | --do \"task description\"]" +effort: low allowed-tools: - Read - Bash diff --git a/commands/gsd/stats.md b/commands/gsd/stats.md index ca62f6f81..ebdba3cca 100644 --- a/commands/gsd/stats.md +++ b/commands/gsd/stats.md @@ -1,6 +1,7 @@ --- name: gsd:stats description: Display project statistics — phases, plans, requirements, git metrics, and timeline +effort: low allowed-tools: - Read - Bash diff --git a/commands/gsd/update.md b/commands/gsd/update.md index dacd1b0d3..516709a16 100644 --- a/commands/gsd/update.md +++ b/commands/gsd/update.md @@ -1,7 +1,7 @@ --- name: gsd:update description: Update GSD to latest version with changelog display -argument-hint: "[--sync | --reapply]" +argument-hint: "[--sync | --reapply | --next | --rc]" allowed-tools: - Read - Write @@ -31,6 +31,7 @@ Routes to the update workflow which handles: - **--sync**: Sync managed GSD skills across runtime roots so multi-runtime users stay aligned after an update. Runs the sync-skills workflow (--from, --to, --dry-run, --apply flags supported). - **--reapply**: Reapply local modifications after a GSD update. Uses three-way comparison (pristine baseline, user-modified backup, newly installed version) to merge user customizations back. Runs the reapply-patches workflow. +- **--next** (alias **--rc**): Target the `@next` RC dist-tag instead of `@latest` so you can install or refresh a release candidate (e.g. `1.4.0-rc.1`) through the normal update flow — scope/runtime detection, changelog preview, custom-file backup, and cache clearing all still apply. Omitting it keeps targeting `@latest` (no change). See ADR #660 for the RC channel. - **(no flag)**: Standard update — check for new version, show changelog, install. @@ -38,7 +39,7 @@ Routes to the update workflow which handles: Parse the first token of $ARGUMENTS: - If it is `--sync`: strip the flag, execute the sync-skills workflow (passing remaining args for --from/--to/--dry-run/--apply). - If it is `--reapply`: strip the flag, execute the reapply-patches workflow. -- Otherwise: execute the update workflow end-to-end. +- Otherwise (including `--next` / `--rc`): execute the update workflow end-to-end, passing `$ARGUMENTS` through so the workflow's parse_update_channel step can select the release channel. diff --git a/docs/AGENTS.md b/docs/AGENTS.md index b63a9176f..85d0fae12 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -42,6 +42,7 @@ GSD uses a multi-agent architecture where thin orchestrators (workflow files) sp | **Parallelism** | 4 instances (stack, features, architecture, pitfalls) | | **Tools** | Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp (context7) | | **Model (balanced)** | Sonnet | +| **Color** | Cyan | | **Produces** | `.planning/research/STACK.md`, `FEATURES.md`, `ARCHITECTURE.md`, `PITFALLS.md` | **Capabilities:** @@ -61,6 +62,7 @@ GSD uses a multi-agent architecture where thin orchestrators (workflow files) sp | **Parallelism** | 4 instances (same focus areas as project researcher) | | **Tools** | Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp (context7) | | **Model (balanced)** | Sonnet | +| **Color** | Cyan | | **Produces** | `{phase}-RESEARCH.md` | **Capabilities:** @@ -80,7 +82,7 @@ GSD uses a multi-agent architecture where thin orchestrators (workflow files) sp | **Parallelism** | Single instance | | **Tools** | Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp (context7) | | **Model (balanced)** | Sonnet | -| **Color** | `#E879F9` (fuchsia) | +| **Color** | Purple | | **Produces** | `{phase}-UI-SPEC.md` | **Capabilities:** @@ -268,7 +270,7 @@ GSD uses a multi-agent architecture where thin orchestrators (workflow files) sp | **Parallelism** | Single instance | | **Tools** | Read, Bash, Glob, Grep | | **Model (balanced)** | Sonnet | -| **Color** | `#22D3EE` (cyan) | +| **Color** | Cyan | | **Produces** | BLOCK/FLAG/PASS verdict | --- @@ -292,6 +294,7 @@ GSD uses a multi-agent architecture where thin orchestrators (workflow files) sp - Logs issues for `/gsd-verify-work` to address - Milestone scope filtering: gaps addressed in later phases are marked as "deferred", not reported as failures (v1.32) - **Test quality audit** (v1.32): verifies that tests prove what they claim by checking for disabled/skipped tests on requirements, circular test patterns (system generating its own expected values), assertion strength (existence vs. value vs. behavioral), and expected value provenance. Blockers from test quality audit override an otherwise passing verification +- Runs the full workspace test suite at most once per verification — proves a test *exists* by enumeration and that it *passes* via a single named test, never re-running the whole suite per must-have. --- @@ -305,6 +308,7 @@ GSD uses a multi-agent architecture where thin orchestrators (workflow files) sp | **Parallelism** | Single instance | | **Tools** | Read, Write, Edit, Bash, Grep, Glob | | **Model (balanced)** | Sonnet | +| **Color** | Purple | | **Produces** | Test files, updated `VALIDATION.md` | **Key behaviors:** @@ -324,7 +328,7 @@ GSD uses a multi-agent architecture where thin orchestrators (workflow files) sp | **Parallelism** | Single instance | | **Tools** | Read, Write, Bash, Grep, Glob | | **Model (balanced)** | Sonnet | -| **Color** | `#F472B6` (pink) | +| **Color** | Pink | | **Produces** | `{phase}-UI-REVIEW.md` with scores | **6 Audit Pillars (scored 1-4):** @@ -400,7 +404,7 @@ runs its default whole-repo scan. | **Parallelism** | Single instance | | **Tools** | Read | | **Model (balanced)** | Sonnet | -| **Color** | Magenta | +| **Color** | Purple | | **Produces** | `USER-PROFILE.md`, `CLAUDE.md` profile section | **Behavioral Dimensions:** @@ -466,7 +470,7 @@ Communication style, decision patterns, debugging approach, UX preferences, vend | **Parallelism** | Single instance | | **Tools** | Read, Write, Edit, Bash, Glob, Grep | | **Model (balanced)** | Sonnet | -| **Color** | `#EF4444` (red) | +| **Color** | Red | | **Produces** | `{phase}-SECURITY.md` | **Key behaviors:** @@ -492,7 +496,7 @@ Twelve additional agents ship under `agents/gsd-*.md` and are used by specialty | **Parallelism** | Single instance | | **Tools** | Read, Bash, Glob, Grep, Write | | **Model (balanced)** | Sonnet | -| **Color** | Magenta | +| **Color** | Purple | | **Produces** | `PATTERNS.md` in the phase directory | **Key behaviors:** @@ -532,7 +536,7 @@ Twelve additional agents ship under `agents/gsd-*.md` and are used by specialty | **Parallelism** | Typically single instance per review scope | | **Tools** | Read, Write, Bash, Grep, Glob | | **Model (balanced)** | Sonnet | -| **Color** | `#F59E0B` (amber) | +| **Color** | Orange | | **Produces** | `REVIEW.md` in the phase directory | **Key behaviors:** @@ -552,7 +556,7 @@ Twelve additional agents ship under `agents/gsd-*.md` and are used by specialty | **Parallelism** | Single instance | | **Tools** | Read, Edit, Write, Bash, Grep, Glob | | **Model (balanced)** | Sonnet | -| **Color** | `#10B981` (emerald) | +| **Color** | Green | | **Produces** | `REVIEW-FIX.md`; one atomic git commit per applied fix | **Key behaviors:** @@ -572,7 +576,7 @@ Twelve additional agents ship under `agents/gsd-*.md` and are used by specialty | **Parallelism** | Single instance (sequential with domain-researcher / eval-planner) | | **Tools** | Read, Write, Bash, Grep, Glob, WebFetch, WebSearch, mcp (context7) | | **Model (balanced)** | Sonnet | -| **Color** | `#34D399` (green) | +| **Color** | Green | | **Produces** | Sections 3–4b of `AI-SPEC.md` (framework quick reference + implementation guidance) | **Key behaviors:** @@ -591,7 +595,7 @@ Twelve additional agents ship under `agents/gsd-*.md` and are used by specialty | **Parallelism** | Single instance | | **Tools** | Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp (context7) | | **Model (balanced)** | Sonnet | -| **Color** | `#A78BFA` (violet) | +| **Color** | Purple | | **Produces** | Section 1b of `AI-SPEC.md` | **Key behaviors:** @@ -610,7 +614,7 @@ Twelve additional agents ship under `agents/gsd-*.md` and are used by specialty | **Parallelism** | Single instance (sequential after domain-researcher) | | **Tools** | Read, Write, Bash, Grep, Glob, AskUserQuestion | | **Model (balanced)** | Sonnet | -| **Color** | `#F59E0B` (amber) | +| **Color** | Orange | | **Produces** | Sections 5–7 of `AI-SPEC.md` (Evaluation Strategy, Guardrails, Production Monitoring) | **Required reading:** `gsd-core/references/ai-evals.md` (evaluation framework). @@ -631,7 +635,7 @@ Twelve additional agents ship under `agents/gsd-*.md` and are used by specialty | **Parallelism** | Single instance | | **Tools** | Read, Write, Bash, Grep, Glob | | **Model (balanced)** | Sonnet | -| **Color** | `#EF4444` (red) | +| **Color** | Red | | **Produces** | `EVAL-REVIEW.md` with dimension scores, findings, and remediation guidance | **Required reading:** `gsd-core/references/ai-evals.md`. @@ -652,7 +656,7 @@ Twelve additional agents ship under `agents/gsd-*.md` and are used by specialty | **Parallelism** | Single instance (interactive) | | **Tools** | Read, Bash, Grep, Glob, WebSearch, AskUserQuestion | | **Model (balanced)** | Sonnet | -| **Color** | `#38BDF8` (sky blue) | +| **Color** | Cyan | | **Produces** | Scored ranked recommendation (structured return to orchestrator) | **Required reading:** `gsd-core/references/ai-frameworks.md` (decision matrix). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ec5226e00..b2b1b4e08 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -145,19 +145,43 @@ Orchestration logic that commands reference. Contains the step-by-step process i #### Progressive disclosure for workflows Workflow files are loaded verbatim into Claude's context every time the -corresponding `/gsd-*` command is invoked. To keep that cost bounded, the -workflow size budget enforced by `tests/workflow-size-budget.test.cjs` -mirrors the agent budget from #2361: +corresponding `/gsd-*` command is invoked. The workflow size budget enforced by +`tests/workflow-size-budget.test.cjs` keeps each file bounded, mirroring the +agent budget from #2361. The budget is measured in **bytes** (#717), not lines: +line count over-penalizes prose and under-catches token-dense tables and code +blocks, whereas bytes are deterministic and match the unit our vendors bound on +— Codex truncates instruction docs past 32,768 bytes (`project_doc_max_bytes`). +We adopt that unit, not that exact number: the XL/LARGE ceilings below sit above +32,768 because these are grandfathered top-level orchestrators loaded by Claude, +not Codex AGENTS.md docs. -| Tier | Per-file line limit | -|-----------|--------------------| -| `XL` | 1700 — top-level orchestrators (`execute-phase`, `plan-phase`, `new-project`) | -| `LARGE` | 1500 — multi-step planners and large feature workflows | -| `DEFAULT` | 1000 — focused single-purpose workflows (the target tier) | +| Tier | Per-file byte limit | +|-----------|---------------------| +| `XL` | 90,000 — top-level orchestrators (`execute-phase`, `plan-phase`, `new-project`) | +| `LARGE` | 54,000 — multi-step planners and large feature workflows | +| `DEFAULT` | 38,000 — focused single-purpose workflows (the target tier) | -`workflows/discuss-phase.md` is held to a stricter <500-line ceiling per -issue #2551. When a workflow grows beyond its tier, extract per-mode bodies -into `workflows//modes/.md`, templates into +Ceilings are not fixed forever: under the tighten-only ratchet (#597) each one +tracks its tier's current high-water mark within a small grace band, so budgets +may only decrease over time. + +**Why the budget exists.** With prompt caching the per-invocation *cost* of a +large workflow is modest (cache reads run ~10% of input). The stronger, +caching-independent reason is **quality**: as context grows, recall and +reasoning degrade ("context rot" / attention budget), so leaner, higher-signal +instructions produce better plans. The ceiling protects the agent's attention, +not just the token bill. + +Because the budget measures one file, it is a proxy for the real goal — +*bounded loaded context*. Extraction only helps when the extracted content is +loaded **lazily** (Read at the step that needs it). Moving prose into a file +that is still eagerly `@`-imported shrinks the measured file without shrinking +loaded context, which games the proxy rather than serving the goal. + +`workflows/discuss-phase.md` is held to a stricter <30,000-byte ceiling per +issue #2551 (originally <500 lines; re-based to bytes for #717). When a workflow grows +beyond its tier, extract per-mode bodies into +`workflows//modes/.md`, templates into `workflows//templates/`, and shared knowledge into `gsd-core/references/`. The parent file becomes a thin dispatcher that Reads only the mode and template files needed for the current invocation. @@ -169,6 +193,16 @@ parent dispatches, modes/ holds per-flag behavior (`power.md`, `all.md`, checkpoint.json schemas that are read only when the corresponding output file is being written. +`workflows/plan-phase.md`, `workflows/execute-phase.md`, and the +`gsd-planner` / `gsd-executor` agent definitions apply the same discipline +to their MVP-only reference bodies — `planner-mvp-mode.md`, +`user-story-template.md`, `skeleton-template.md`, and `execute-mvp-tdd.md` +are referenced for the planner/executor to Read only on MVP, +Walking-Skeleton, or MVP+TDD paths, rather than eagerly `@`-imported, so +non-MVP runs do not pay their context cost (guards against the "`@`-import +behind a conditional still loads eagerly" leak; see #720). The dedicated +`mvp-phase` workflow keeps its eager imports, since it is always MVP. + ### Agents (`agents/*.md`) Specialized agent definitions with frontmatter specifying: @@ -270,6 +304,37 @@ See [`docs/INVENTORY.md`](INVENTORY.md#hooks-11-shipped) for the authoritative 1 CJS command family routers dispatch through `CommandRoutingHub`. The hub owns the no-throw pure-result contract (`hub.dispatch()` catches internal exceptions and returns `{ ok: false, kind, ...typedPayload }`) and the closed runtime error taxonomy (`UnknownCommand`, `InvalidArgs`, `HandlerRefusal`, `HandlerFailure`). Router adapters remain thin CLI translators — they build the hub, call `dispatch`, then map the Result to `output()`/`error()` calls. The runtime is single-path (no dual-runtime mode selection). See `docs/adr/0174-retire-gsd-sdk-package-boundary.md`. +### Research Module (`src/research-{store,provider}.cts`, `src/package-legitimacy.cts`) + +The Research Module implements an **L2-hybrid seam**: code owns the cache, provider policy, and package legitimacy verdicts; MCP owns the actual network fetch. + +Three compiled modules (generated to `gsd-core/bin/lib/*.cjs` per ADR-457) are reachable via `gsd-tools query research-plan | research-store | package-legitimacy`: + +- **Research Store** — content-addressed cache (`sha256(ecosystem+library+version+query+kind)`) with per-source TTL (curated-doc: 30 d, medium: 7 d, web/synthesis: 1 d) and two storage tiers: `~/.gsd/research-cache` for cross-project curated-doc hits, `.planning/research/.cache` for project-local web/synthesis results. +- **Research Provider** — single `PROVIDER_WATERFALL` (`Context7→Ref→Jina→websearch` for docs; `Exa→Tavily→Perplexity→Brave→websearch` for web; `Firecrawl→Jina` for scrape-only). `planResearch()` returns cache hits plus a fetch plan; `classifyConfidence()` stamps `HIGH|MEDIUM|LOW` by provider tier. +- **Package Legitimacy** — registry-API verdicts (npm/PyPI/crates.io injectable adapters) producing `OK|SUS|SLOP` per package. `slopcheck` is an optional escalate-only adapter; absence leaves registry verdicts intact rather than downgrading everything to `[ASSUMED]`. + +**Data flow:** + +``` +agent + │ + ▼ +gsd-tools query research-plan ← Research Provider: check cache, build fetch plan + │ + ├── [cache hits] ──────────────────► RESEARCH.md (digest only, no raw content) + │ + └── [fetch plan] ──────────────────► MCP fetch (agent calls MCP tools with the plan) + │ + ▼ + gsd-tools query research-store (put) + │ + ▼ + RESEARCH.md path returned to orchestrator +``` + +Agents always return a `RESEARCH.md` path, never raw fetched content. Context discipline is enforced through subagent isolation, compact provider output, and fetch-to-disk. See [ADR-0656](adr/0656-research-module-seam.md). + ### CLI Tools (`gsd-core/bin/`) Node.js CLI utility (`gsd-tools.cjs`) with domain modules split across `gsd-core/bin/lib/` (see [`docs/INVENTORY.md`](INVENTORY.md#cli-modules-33-shipped) for the authoritative roster): @@ -735,15 +800,15 @@ The migration-specific ownership and source snapshots live in | Kilo | `~/.config/kilo` | `./.kilo` | `command/gsd-*.md` | `agents/gsd-*.md` | `kilo.json` or `kilo.jsonc`; no GSD hooks | | Gemini CLI | `~/.gemini` | `./.gemini` | `commands/gsd/*.toml` | `agents/gsd-*.md` | `settings.json` feature flag, hooks, and statusline | | Codex | `~/.codex` | `./.codex` | `skills/gsd-*/SKILL.md` | `agents/` source markdown plus per-agent TOML | `config.toml` `[agents.gsd-*]`, `[features].hooks` (canonical; legacy alias `codex_hooks` is recognized and migrated forward on reinstall, #3566), and hook tables | -| GitHub Copilot | `~/.copilot` | `./.github` | `skills/gsd-*/SKILL.md` and `copilot-instructions.md` | `.agent.md` files | No GSD hooks or statusline | +| GitHub Copilot | `~/.copilot` | `./.github` | `skills/gsd-*/SKILL.md`, `copilot-instructions.md`, and `AGENTS.md` (repo root, local) | `.agent.md` files | Self-contained `sessionStart` hook (`hooks/gsd-session.json`, inline `command` type); no statusline | | Antigravity | auto-detected: `~/.gemini/antigravity`, `~/.gemini/antigravity-ide`, or `~/.gemini/antigravity-cli` | `./.agent` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Gemini-style `settings.json` hook entries when installed by GSD | -| Cursor | `~/.cursor` | `./.cursor` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Rule references under `rules/`; no GSD hooks | +| Cursor | `~/.cursor` | `./.cursor` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Rule references under `rules/`; `hooks.json` with sessionStart context injection and postToolUse STATE.md monitor (#777) | | Windsurf | `~/.codeium/windsurf` | `./.windsurf` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Rule references under `rules/`; no GSD hooks | | Augment Code | `~/.augment` | `./.augment` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | No GSD hooks or statusline | | Trae | `~/.trae` | `./.trae` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Rule references under `rules/`; no GSD hooks | | Qwen Code | `~/.qwen` | `./.qwen` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Common GSD settings and hook entries where supported | | Hermes Agent | `~/.hermes` | `./.hermes` | `skills/gsd/DESCRIPTION.md` plus `skills/gsd/gsd-*/SKILL.md` | `agents/gsd-*.md` | Common GSD settings and hook entries where supported | -| CodeBuddy | `~/.codebuddy` | `./.codebuddy` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Common GSD settings and hook entries where supported | +| CodeBuddy | `~/.codebuddy` | `./.codebuddy` | `skills/gsd-*/SKILL.md` (`user-invocable: false`) | `agents/gsd-*.md` | `/gsd-*` slash commands under `commands/`; common GSD settings and hook entries where supported | | Cline | `~/.cline` | project root | `.clinerules` | Rules only | No GSD hooks or statusline | ### Upstream Contract Sources diff --git a/docs/CLI-TOOLS.md b/docs/CLI-TOOLS.md index 79d55434c..27e774b51 100644 --- a/docs/CLI-TOOLS.md +++ b/docs/CLI-TOOLS.md @@ -421,6 +421,34 @@ node gsd-tools.cjs websearch [--limit N] [--freshness day|week|month] --- +## Worktree Commands + +Diagnose and configure the worktree fork base used by Claude Code's `isolation="worktree"` executor dispatch. These commands address the branch-divergence condition described in [Fix the worktree base-mismatch (exit 42) error](how-to/fix-worktree-base-mismatch.md). + +```bash +# Check whether the current HEAD has diverged from the worktree fork base. +# Returns JSON: { shouldDegrade, reason, message, headSha, forkRef, forkSha } +node gsd-tools.cjs worktree base-check + +# Write worktree.baseRef:"head" into .claude/settings.local.json (no-clobber). +# Returns JSON: { changed, skipped, previous, baseRef, file } +node gsd-tools.cjs worktree set-baseref +``` + +**`worktree base-check`** reads `worktree.baseRef` from `.claude/settings.local.json` (then `.claude/settings.json`) and compares the current `HEAD` SHA against `origin/HEAD`. The `shouldDegrade` field is `true` when the execute-phase orchestrator will fall back to sequential execution. Possible `reason` values: + +| `reason` | `shouldDegrade` | Meaning | +|---|---|---| +| `baseref-head` | `false` | `worktree.baseRef:"head"` is set; no mismatch possible | +| `head-matches-fork` | `false` | HEAD and `origin/HEAD` are the same commit | +| `head-diverged-from-fork` | `true` | Branch is ahead of or diverged from `origin/HEAD` | +| `fork-ref-unknown` | `true` | `origin/HEAD` could not be resolved | +| `no-head` | `false` | Not in a git repo (no `HEAD`) | + +**`worktree set-baseref`** applies a no-clobber write of `worktree.baseRef:"head"` to `.claude/settings.local.json`. If the file already contains an explicit `baseRef` value other than `"head"`, the existing value is preserved and `skipped:"explicit-other"` is returned. Malformed JSON causes an error rather than a silent overwrite. Both fresh installs and upgrades of GSD Core run this automatically when `workflow.use_worktrees` is enabled (the default); the command is also available for manual use — for example, to apply the setting when worktrees were toggled on after installation, or to re-apply it after a settings change. + +--- + ## Graphify Build, query, and inspect the project knowledge graph in `.planning/graphs/`. Requires `graphify.enabled: true` in `config.json` (see [Configuration Reference](CONFIGURATION.md#graphify-settings)). @@ -471,6 +499,7 @@ User-facing entry point: `/gsd-graphify` (see [Command Reference](COMMANDS.md#gs | Audit | `lib/audit.cjs` | Phase/milestone audit queue handlers; `audit-open` helper | | GSD2 Import | `lib/gsd2-import.cjs` | Reverse-migration importer from GSD-2 projects (backs `/gsd-import --from-gsd2`) | | Intel | `lib/intel.cjs` | Queryable codebase intelligence index (backs `/gsd-map-codebase --query`) | +| Worktree Base Ref | `lib/worktree-base-ref.cjs` | Worktree fork-base detection and `worktree base-check` / `set-baseref` commands (#683) | --- @@ -498,4 +527,5 @@ API keys configured via `/gsd-settings` (`brave_search`, `firecrawl`, `exa_searc - [Commands](COMMANDS.md) - [Configuration](CONFIGURATION.md) - [Architecture](ARCHITECTURE.md) +- [Fix the worktree base-mismatch (exit 42) error](how-to/fix-worktree-base-mismatch.md) - [docs index](README.md) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index f9d337f78..50c636ca8 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -12,6 +12,14 @@ The hyphen and colon forms are *runtime-specific spellings of the same command*. Whichever runtime you're on, the installer writes the correct form into your runtime's command directory. +### Skill Runtime Behavior (Claude Code) + +Heavy workflow skills (`/gsd-plan-phase`, `/gsd-execute-phase`, `/gsd-autonomous`) carry `context: fork` in their frontmatter. On Claude Code, this runs each skill in an isolated subagent context window, protecting the main session's context budget. The skills also declare `effort: xhigh`, signalling maximum token budget to the runtime. + +Quick-status skills (`/gsd-progress`, `/gsd-stats`) declare `effort: low`, directing the runtime to use a minimal token budget for fast reads. + +These fields are Claude Code–specific frontmatter. On runtimes that do not recognise them (Gemini, Codex, Cursor, etc.) the fields are silently ignored — existing behaviour is unchanged. + --- ## Namespace Meta-Skills @@ -157,12 +165,13 @@ Research, plan, and verify a phase. | `--skip-bounce` | Skip plan bounce even if enabled in config | | `--mvp` | Vertical MVP mode — planner organizes tasks as feature slices (UI→API→DB) instead of horizontal layers. On Phase 1 of a new project with no prior phase summaries, also emits `SKELETON.md` (Walking Skeleton). Can be persisted on a phase via `**Mode:** mvp` in ROADMAP.md, which applies `--mvp` automatically without the flag. | | `--tdd` | TDD mode — planner applies `type: tdd` to eligible behavior-adding tasks so each begins with a failing test. Composable with `--mvp`: `--mvp --tdd` produces vertical slices where every behavior-adding task starts red-green. | +| `--granularity ` | Override the planning granularity for this invocation, ignoring config. Valid values: `coarse`, `standard`, `fine`. Takes precedence over `granularities.planning`, top-level `granularity`, and `planning.granularity` config. | **Prerequisites:** `.planning/ROADMAP.md` exists **Produces:** `{phase}-RESEARCH.md`, `{phase}-{N}-PLAN.md`, `{phase}-VALIDATION.md`; `{phase}/SKELETON.md` when Walking Skeleton mode fires **Research-only mode (`--research-phase `):** -- No modifier: prompts `update / view / skip` if RESEARCH.md already exists. +- No modifier: when RESEARCH.md already exists, auto-uses it — emits a one-line notice and exits, no prompt. - With `--research`: force-refresh — re-spawn researcher unconditionally, no prompt. - With `--view`: print existing RESEARCH.md to stdout, no spawn. Errors if RESEARCH.md missing. @@ -185,7 +194,7 @@ See [Package Legitimacy Gate in the User Guide](USER-GUIDE.md#package-legitimacy /gsd-plan-phase 1 --bounce # Plan + external bounce validation /gsd-plan-phase 2 --ingest docs/adr/0010.md # ADR express path for context synthesis /gsd-plan-phase 2 --ingest 'docs/adr/00*.md' --ingest-format auto -/gsd-plan-phase --research-phase 4 # Research only on phase 4 (prompts if RESEARCH.md exists) +/gsd-plan-phase --research-phase 4 # Research only on phase 4 (auto-uses existing RESEARCH.md, no prompt) /gsd-plan-phase --research-phase 4 --view # Print existing RESEARCH.md, no spawn /gsd-plan-phase --research-phase 4 --research # Force-refresh research, no prompt /gsd-plan-phase 1 --mvp # Vertical-slice plan for phase 1 @@ -713,13 +722,17 @@ Run all remaining phases autonomously. |------|-------------| | `--from N` | Start from a specific phase number | | `--to N` | Stop after completing a specific phase number | +| `--only N` | Restrict execution to phase N; lifecycle step is skipped | | `--interactive` | Lean context with user input | +| `--text` | Replace `AskUserQuestion` prompts with plain numbered lists | ```bash /gsd-autonomous # Run all remaining phases /gsd-autonomous --from 3 # Start from phase 3 /gsd-autonomous --to 5 # Run up to and including phase 5 /gsd-autonomous --from 3 --to 5 # Run phases 3 through 5 +/gsd-autonomous --only 4 # Run only phase 4 +/gsd-autonomous --text # Run with text-mode prompts ``` ### `/gsd-debug` @@ -1135,11 +1148,13 @@ Update GSD with changelog preview, and optionally sync skills or reapply local p |------|-------------| | `--sync` | Sync skills from the GSD registry after updating | | `--reapply` | Restore local modifications (patches) after updating | +| `--next` / `--rc` | Target the `@next` RC dist-tag instead of `@latest` (installs or refreshes a release candidate, e.g. `1.4.0-rc.1`; see ADR #660) | ```bash /gsd-update # Check for updates and install /gsd-update --sync # Update and sync skills /gsd-update --reapply # Update and reapply local patches +/gsd-update --next # Install from the @next RC dist-tag ``` --- diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e6ba53b4b..f18291470 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -115,6 +115,9 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new }, "project_code": null, "agent_skills": {}, + "agent_skills_security": { + "trusted_global_roots": [] + }, "response_language": null, "features": { "thinking_partner": false, @@ -245,7 +248,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin | `workflow.max_discuss_passes` | number | `3` | Maximum number of question rounds in discuss-phase before the workflow stops asking. Useful in headless/auto mode to prevent infinite discussion loops. | | `workflow.skip_discuss` | boolean | `false` | When `true`, `/gsd-autonomous` bypasses the discuss-phase entirely, writing minimal CONTEXT.md from the ROADMAP phase goal. Useful for projects where developer preferences are fully captured in PROJECT.md/REQUIREMENTS.md. Added in v1.28 | | `workflow.text_mode` | boolean | `false` | Replaces AskUserQuestion TUI menus with plain-text numbered lists. Required for Claude Code remote sessions (`/rc` mode) where TUI menus don't render. Can also be set per-session with `--text` flag on discuss-phase. Added in v1.28 | -| `workflow.use_worktrees` | boolean | `true` | When `false`, disables git worktree isolation for parallel execution. Users who prefer sequential execution or whose environment does not support worktrees can disable this. Added in v1.31 | +| `workflow.use_worktrees` | boolean | `true` | When `false`, disables git worktree isolation for parallel execution. Users who prefer sequential execution or whose environment does not support worktrees can disable this. Added in v1.31. **Branch-divergence note:** when your branch is ahead of `origin/HEAD`, GSD auto-degrades to sequential and prints a warning. Set `worktree.baseRef:"head"` in `.claude/settings.local.json` (run `node gsd-tools.cjs worktree set-baseref`) to restore parallel execution. See [Fix the worktree base-mismatch (exit 42) error](how-to/fix-worktree-base-mismatch.md). | | `workflow.worktree_skip_hooks` | boolean | `false` | When `true`, executor agents in worktree mode pass `--no-verify` (skipping pre-commit hooks) and post-wave hook validation runs against the merged result instead. Opt-in escape hatch for projects whose hooks cannot run in agent worktrees. Default `false` runs hooks on every commit (#2924). | | `workflow.code_review` | boolean | `true` | Enable `/gsd-code-review` and `/gsd-code-review --fix` commands. When `false`, the commands exit with a configuration gate message. Added in v1.34 | | `workflow.code_review_depth` | string | `standard` | Default review depth for `/gsd-code-review`: `quick` (pattern-matching only), `standard` (per-file analysis), or `deep` (cross-file with import graphs). Can be overridden per-run with `--depth=`. Added in v1.34 | @@ -395,6 +398,7 @@ Inject custom skill files into GSD subagent prompts. Skills are read by agents a | Setting | Type | Default | Description | |---------|------|---------|-------------| | `agent_skills` | object | `{}` | Map of agent types to skill directory paths | +| `agent_skills_security.trusted_global_roots` | array of strings | `[]` | Opt-in allowlist of additional trusted directories for `global:` skills. See [Trusted global skill roots](#trusted-global-skill-roots-agent_skills_securitytrusted_global_roots) | ### Configuration @@ -454,6 +458,50 @@ gsd-tools query config-set agent_skills.gsd-executor '["skills/my-skill"]' --- +## Trusted Global Skill Roots (`agent_skills_security.trusted_global_roots`) + +Widen the symlink-safety boundary for `global:` skills by declaring additional trusted root directories. + +### Purpose + +By default, a `global:` skill whose `SKILL.md` real path (after resolving symlinks) escapes the runtime's global skills directory (e.g. `~/.claude/skills/`) is rejected as a symlink-escape. `agent_skills_security.trusted_global_roots` lets you declare additional trusted root directories so symlinked skills whose real target lives under one of them are accepted. + +Common use case: a single source-of-truth skills directory elsewhere on disk (e.g. `~/shared/skills`) symlinked into `~/.claude/skills/` so `git pull` or `rsync` keeps a team's skills up to date without maintaining copies. + +### Configuration + +```json +{ + "agent_skills_security": { + "trusted_global_roots": [ + "~/shared/skills", + "/opt/shared-skills" + ] + } +} +``` + +### How It Works + +- **Default `[]`** — behavior is byte-identical to omitting the option entirely: only skills whose real `SKILL.md` path resolves inside the default global skills directory are accepted. +- **Absolute or tilde-prefixed paths only.** Each entry must be an absolute path (`/opt/shared-skills`) or a `~`/`~/`-prefixed path (tilde expands to your home directory). Project-relative paths are rejected, so an untrusted repo's `.planning/config.json` cannot point trust at a directory inside itself. +- **`realpathSync` at load time.** Each declared root is resolved with `realpathSync` on every run, so trust follows the real target and cannot silently drift if a root itself later becomes a symlink. Non-existent or unreadable roots are dropped without error. +- **Dangerously broad roots are refused.** The filesystem root (`/`), drive or UNC roots, and your home directory itself cannot be declared as trusted roots — these would make the allowlist meaningless. +- **Acceptance rule.** A skill is accepted if and only if its real `SKILL.md` path lies inside the default global skills directory OR inside one of the resolved trusted roots. Skills resolving outside all of these are still rejected. +- **Audit note.** When a skill is accepted via a trusted root rather than the default global skills directory, a `[agent-skills] NOTE:` line is written to stderr so the widened boundary remains visible. + +> **Security note:** `trusted_global_roots` is read from the project-local `.planning/config.json`. Only add roots you control and trust. Declaring a broad shared directory widens which symlinked global skills will load for every agent in this project. + +### CLI + +```bash +gsd config-set agent_skills_security.trusted_global_roots '["~/shared/skills"]' +``` + +Setting the parent object (`agent_skills_security`) directly is not supported; use the dot-notation leaf form shown above. + +--- + ## Feature Flags Toggle optional capabilities via the `features.*` config namespace. Feature flags default to `false` (disabled) — enabling a flag opts into new behavior without affecting existing workflows. diff --git a/docs/FEATURES.md b/docs/FEATURES.md index cdda06ee3..7c96d5895 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -904,6 +904,7 @@ continues. Drift detection cannot fail verification. - REQ-UPDATE-03: System MUST be runtime-aware and target the correct directory - REQ-UPDATE-04: System MUST back up locally modified files to `gsd-local-patches/` - REQ-UPDATE-05: `/gsd-update --reapply` MUST restore local modifications after update +- REQ-UPDATE-06: `/gsd-update --next` (alias `--rc`) MUST target the `@next` RC dist-tag for version check and install; omitting the flag MUST keep `@latest` behavior unchanged (ADR #660) --- @@ -925,7 +926,7 @@ continues. Drift detection cannot fail verification. | `granularity` | enum | `standard` | `coarse`, `standard`, or `fine` | | `model_profile` | enum | `balanced` | `quality`, `balanced`, `budget`, or `inherit` | | `models.` | enum | (none) | Per-phase-type tier override (`planning`, `discuss`, `research`, `execution`, `verification`, `completion`). Values: `opus`, `sonnet`, `haiku`, `inherit`. Coarse phase-level tuning that wins over `model_profile` but loses to per-agent `model_overrides`. See [CONFIGURATION.md](CONFIGURATION.md#per-phase-type-models-models--added-in-v140). Added in v1.40 | -| `granularities.` | enum | (none) | Per-phase-type granularity override (`planning`, `discuss`, `research`, `execution`, `verification`, `completion`). Values: `coarse`, `standard`, `fine`. Mirrors `models.` for granularity. See [CONFIGURATION.md](CONFIGURATION.md#core-settings). Added in v1.43 ([#68](https://github.com/open-gsd/gsd-core/issues/68)) | +| `granularities.` | enum | (none) | Per-phase-type granularity override (`planning`, `discuss`, `research`, `execution`, `verification`, `completion`). Values: `coarse`, `standard`, `fine`. Mirrors `models.` for granularity. See [CONFIGURATION.md](CONFIGURATION.md#core-settings). Added in v1.43 ([#68](https://github.com/open-gsd/gsd-core/issues/68)). `/gsd:plan-phase --granularity ` overrides all config-based granularity for a single invocation (takes precedence over `granularities.planning`, top-level `granularity`, and `planning.granularity`). ([#703](https://github.com/open-gsd/gsd-core/issues/703)) | | `dynamic_routing.enabled` | boolean | `false` | Master switch for failure-tier escalation. When `true`, agents resolve to `tier_models[default_tier]` and escalate one tier on orchestrator-detected soft failure. Capped by `max_escalations`. See [CONFIGURATION.md](CONFIGURATION.md#dynamic-routing-with-failure-tier-escalation-dynamic_routing--added-in-v140). Added in v1.40 | | `workflow.research` | boolean | `true` | Domain research before planning | | `workflow.plan_check` | boolean | `true` | Plan verification loop | @@ -1013,12 +1014,18 @@ fix(03-01): correct auth token expiry **Runtime Transformations:** -| Aspect | Claude Code | OpenCode | Gemini | Kilo | Codex | Copilot | Antigravity | Trae | Cline | Augment | CodeBuddy | Qwen Code | -|--------|------------|----------|--------|-------|-------|---------|-------------|------|-------|---------|-----------|-----------| -| Commands | Slash commands | Slash commands | Slash commands | Slash commands | Skills (TOML) | Slash commands | Skills | Skills | Rules | Skills | Skills | Skills | -| Agent format | Claude native | `mode: subagent` | Claude native | `mode: subagent` | Skills | Tool mapping | Skills | Skills | Rules | Skills | Skills | Skills | -| Hook events | `PostToolUse` | N/A | `AfterTool` | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | -| Config | `settings.json` | `opencode.json(c)` | `settings.json` | `kilo.json(c)` | TOML | Instructions | Config | Config | `.clinerules` | Config | Config | Config | +| Aspect | Claude Code | OpenCode | Gemini | Kilo | Codex | Copilot | Antigravity | Cursor | Trae | Cline | Augment | CodeBuddy | Qwen Code | +|--------|------------|----------|--------|-------|-------|---------|-------------|--------|------|-------|---------|-----------|-----------| +| Commands | Slash commands | Slash commands | Slash commands | Slash commands | Skills (TOML) | Slash commands | Skills | Skills + Slash commands | Skills | Rules | Skills | Skills | Skills | +| Agent format | Claude native | `mode: subagent` | Claude native | `mode: subagent` | Skills | Tool mapping | Skills | Skills | Skills | Rules | Skills | Skills | Skills | +| Hook events | `PostToolUse` | N/A | `AfterTool` | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | +| Config | `settings.json` | `opencode.json(c)` | `settings.json` | `kilo.json(c)` | TOML | Instructions | Config | Config | Config | `.clinerules` | Config | Config | Config | + +**Cursor artifact surfaces:** `gsd install --cursor` writes two artifact kinds: +- `~/.cursor/skills/gsd-/SKILL.md` — rich skills with YAML frontmatter, Cursor tool-name mapping, and adapter context header (existing surface) +- `~/.cursor/commands/gsd-.md` — plain markdown slash commands (no frontmatter) invocable via `/` in the Agent input (Cursor 1.6+, added in #785) + +**Claude Code native plugin distribution:** GSD Core ships a `.claude-plugin/plugin.json` manifest, enabling installation and lifecycle management via `claude plugin install|enable|disable|update gsd-core`. Commands load under the `/gsd-core:` namespace (e.g. `/gsd-core:plan-phase`), avoiding slash-command collisions with the classic npm installer which uses `/gsd:`. Always-on guard and update hooks are wired automatically via `hooks/hooks.json`. The plugin path is additive — the npm installer (`npx @opengsd/gsd-core`) remains fully supported. --- @@ -2284,7 +2291,7 @@ Test suite that scans all agent, workflow, and command files for embedded inject **Requirements:** - REQ-CLINE-02: Cline install MUST write `.clinerules` to `~/.cline/` (global) or `./.cline/` (local). No custom slash commands — rules-based integration only. Flag: `--cline`. -- REQ-CODEBUDDY-01: CodeBuddy install MUST deploy skills to `~/.codebuddy/skills/gsd-*/SKILL.md`. Flag: `--codebuddy`. +- REQ-CODEBUDDY-01: CodeBuddy install MUST deploy skills to `~/.codebuddy/skills/gsd-*/SKILL.md` (emitted `user-invocable: false`), `/gsd-*` slash commands to `~/.codebuddy/commands/gsd-*.md`, and subagents to `~/.codebuddy/agents/gsd-*.md`. The commands surface is the sole `/` menu entry point. No `mcp.json` is written (gsd ships no MCP server). Flag: `--codebuddy`. - REQ-QWEN-01: Qwen Code install MUST deploy skills to `~/.qwen/skills/gsd-*/SKILL.md`, following the open standard used by Claude Code 2.1.88+. `QWEN_CONFIG_DIR` env var overrides the default path. Flag: `--qwen`. **Runtime summary:** diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index 64303c715..5d7854d75 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -1,5 +1,5 @@ { - "generated": "2026-06-02", + "generated": "2026-06-07", "families": { "agents": [ "gsd-advisor-researcher", @@ -227,6 +227,7 @@ "planner-graphify-auto-update.md", "planner-human-verify-mode.md", "planner-interface-context.md", + "planner-load-graph-context.md", "planner-mvp-mode.md", "planner-reviews.md", "planner-revision.md", @@ -234,6 +235,9 @@ "planning-config.md", "project-skills-discovery.md", "questioning.md", + "research-documentation-lookup.md", + "research-philosophy.md", + "research-verification-protocol.md", "revision-loop.md", "scout-codebase.md", "skeleton-template.md", @@ -268,6 +272,7 @@ "audit.cjs", "check-command-router.cjs", "cjs-command-router-adapter.cjs", + "cli-exit.cjs", "clock.cjs", "clusters.cjs", "code-review-flags.cjs", @@ -302,6 +307,7 @@ "model-catalog.cjs", "model-profiles.cjs", "package-identity.cjs", + "package-legitimacy.cjs", "phase-command-router.cjs", "phase-lifecycle.cjs", "phase.cjs", @@ -312,11 +318,14 @@ "profile-pipeline.cjs", "project-root.cjs", "prompt-budget.cjs", + "research-provider.cjs", + "research-store.cjs", "review-reviewer-selection.cjs", "roadmap-command-router.cjs", "roadmap-upgrade.cjs", "roadmap.cjs", "runtime-artifact-layout.cjs", + "runtime-config-adapter-registry.cjs", "runtime-homes.cjs", "runtime-name-policy.cjs", "runtime-slash.cjs", @@ -336,18 +345,24 @@ "update-context.cjs", "validate-command-router.cjs", "validate.cjs", + "verification-command-router.cjs", + "verification.cjs", "verify-command-router.cjs", "verify.cjs", "workstream-inventory-builder.cjs", "workstream-inventory.cjs", "workstream-name-policy.cjs", "workstream.cjs", + "worktree-base-ref.cjs", "worktree-safety.cjs" ], "hooks": [ "gsd-check-update-worker.js", "gsd-check-update.js", + "gsd-config-reload.js", "gsd-context-monitor.js", + "gsd-cursor-post-tool.js", + "gsd-cursor-session-start.js", "gsd-graphify-update.sh", "gsd-phase-boundary.sh", "gsd-prompt-guard.js", diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index 3f059653a..ebce42500 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -264,7 +264,7 @@ Full roster at `gsd-core/workflows/*.md`. Workflows are thin orchestrators that --- -## References (63 shipped) +## References (67 shipped) Full roster at `gsd-core/references/*.md`. References are shared knowledge documents that workflows and agents `@-reference`. The groupings below match [`docs/ARCHITECTURE.md`](ARCHITECTURE.md#references-gsd-corereferencesmd) — core, workflow, thinking-model clusters, and the modular planner decomposition. @@ -288,6 +288,9 @@ Full roster at `gsd-core/references/*.md`. References are shared knowledge docum | `debugger-philosophy.md` | Evergreen debugging disciplines loaded by `gsd-debugger`. | | `mandatory-initial-read.md` | Shared required-reading boilerplate injected into agent prompts. | | `project-skills-discovery.md` | Shared project-skills-discovery boilerplate injected into agent prompts. | +| `research-documentation-lookup.md` | Shared documentation-lookup protocol (Context7 MCP + guarded CLI fallback) injected into all researcher agents. | +| `research-philosophy.md` | Shared research philosophy (training-as-hypothesis, honest reporting, investigation-not-confirmation) injected into researcher agents. | +| `research-verification-protocol.md` | Shared research verification protocol (4 pitfalls + pre-submission checklist) injected into researcher agents. | ### Workflow References @@ -358,15 +361,16 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t | `planner-human-verify-mode.md` | Rules for `workflow.human_verify_mode = end-of-phase`: suppress `checkpoint:human-verify` task emission and route deferred items via ``. | | `planner-graphify-auto-update.md` | How `load_graph_context` surfaces `.last-build-status.json` auto-update state (running / failed / stale head) alongside the existing staleness annotation. Opt-in via `graphify.auto_update` (#3347). | | `planner-interface-context.md` | Interface context rules for executors — how to extract key interfaces/types/exports from existing code and document new interfaces that downstream plans will consume. | +| `planner-load-graph-context.md` | Planner's load_graph_context step: knowledge-graph freshness + dependency-context query via the gsd_run launcher (extracted from gsd-planner.md). | | `skeleton-template.md` | SKELETON.md template emitted for new-project Walking Skeleton (Phase 1 + `--mvp`). | | `user-story-template.md` | User story format for MVP planning — "As a / I want to / So that" structured fields. | | `spidr-splitting.md` | SPIDR splitting decomposition rules for handling large user stories in MVP mode. | -> **Subdirectory:** `gsd-core/references/few-shot-examples/` contains additional few-shot examples (`plan-checker.md`, `verifier.md`) that are referenced from specific agents. These are not counted in the 63 top-level references. +> **Subdirectory:** `gsd-core/references/few-shot-examples/` contains additional few-shot examples (`plan-checker.md`, `verifier.md`) that are referenced from specific agents. These are not counted in the 64 top-level references. --- -## CLI Modules (82 shipped) +## CLI Modules (90 shipped) Full listing: `gsd-core/bin/lib/*.cjs`. @@ -378,6 +382,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`. | `artifacts.cjs` | Canonical artifact registry — known `.planning/` root file names; used by `gsd-health` W019 lint | | `audit.cjs` | Audit dispatch, audit open sessions, audit storage helpers | | `check-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools check` | +| `cli-exit.cjs` | `ExitError` class and `runMain()` helper — CLI entrypoints throw `ExitError` instead of calling `process.exit()`; `runMain()` translates the outcome into `process.exitCode` so output flushes cleanly | | `cjs-command-router-adapter.cjs` | Shared compatibility adapter for manifest-backed CJS command-family routers | | `clock.cjs` | Injectable clock seam (now/sleep) for deterministic lock testing | | `clusters.cjs` | Skill cluster definitions for the runtime surface module (ADR-0011 Phase 2) | @@ -413,6 +418,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`. | `model-catalog.cjs` | CJS adapter over the shared model catalog JSON; exports canonical runtime tier defaults, agent profile maps, alias maps, and routing metadata for all CLI consumers | | `model-profiles.cjs` | Backward-compatible profile helpers derived from `model-catalog.cjs`; no longer owns its own model table | | `package-identity.cjs` | Generated single source for GSD's published-package coordinates (npm name, bin name, repo slug, changelog URL, manual-install command), derived from package.json; read by the update worker, `check-latest-version`, and installer (#498) | +| `package-legitimacy.cjs` | Registry-API package legitimacy verdicts (OK/SUS/SLOP) from npm/PyPI/crates, slopcheck optional | | `phase-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools phase` | | `phase-lifecycle.cjs` | Pure-computation phase lifecycle helpers extracted from the phase-lifecycle SDK handler | | `phase.cjs` | Phase directory operations, decimal numbering, plan indexing | @@ -423,11 +429,14 @@ Full listing: `gsd-core/bin/lib/*.cjs`. | `profile-output.cjs` | Profile rendering, USER-PROFILE.md and dev-preferences.md generation | | `profile-pipeline.cjs` | User behavioral profiling data pipeline, session file scanning | | `prompt-budget.cjs` | Pure token-budget accounting for review prompts — estimates tokens, applies deterministic trim priority (head-shrink PROJECT.md, proportional plan truncation, drop context/research/requirements, hard-fail guard), returns structured metadata for `review.max_prompt_tokens` (#3081) | +| `research-provider.cjs` | Research provider waterfall, confidence tiers, and planResearch (cache-hits + fetch plan) | +| `research-store.cjs` | Content-addressed research cache: sha256 keys, per-source TTL staleness, two-tier (user ~/.gsd / project .planning) store | | `review-reviewer-selection.cjs` | Reviewer selection/normalization helpers for `/gsd-review` default reviewer policy and precedence | | `roadmap-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools roadmap` | | `roadmap-upgrade.cjs` | Migration tool for converting legacy `Phase N` entries to milestone-prefixed `Phase M-NN` convention; `computeMigrationPlan` + `applyMigration` with dry-run default and atomic rollback | | `roadmap.cjs` | ROADMAP.md parsing, phase extraction, plan progress | | `runtime-artifact-layout.cjs` | Runtime artifact layout module — resolves the artifact directory shapes (commands, agents, skills) for each supported runtime; single source of truth for per-runtime artifact placement (#3663) | +| `runtime-config-adapter-registry.cjs` | Explicit runtime config adapter registry — resolves per-runtime config-mutation install intent (install surface, shared-settings gate, finish-phase permission writer); see ADR-58. | | `runtime-name-policy.cjs` | Runtime name normalization policy — canonical token sanitization for runtime identifiers used in path construction and display | | `runtime-homes.cjs` | Canonical runtime → global config/skills directory mapping; first-class support for all 15 runtimes including Hermes nested layout and Cline rules-based exclusion (#3126) | | `runtime-slash.cjs` | Runtime-aware slash-command formatter — single source of truth for emitting `/gsd-` (skills-based runtimes) and `$gsd-` (codex) in user-facing output and persisted artifacts (#3584) | @@ -447,19 +456,22 @@ Full listing: `gsd-core/bin/lib/*.cjs`. | `update-context.cjs` | Pure install-context resolver for `/gsd:update` — runtime/scope/config-dir/version detection (LOCAL/GLOBAL/UNKNOWN) ported from update.md bash; backs `gsd-tools update-context` (#498) | | `validate-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools validate` | | `validate.cjs` | Pure phase variant normalization helpers (`phaseVariants`, `buildRoadmapPhaseVariants`, `buildNotStartedPhaseVariants`) used by `verify.cjs` for W006/W007 checks; no I/O, no async | +| `verification-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools verification` | +| `verification.cjs` | Verification-status routing — consolidates pass/gaps_found/human_needed status from phase verifier-emitted VERIFICATION.md frontmatter (#651) | | `verify-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools verify` | | `verify.cjs` | Plan structure, phase completeness, reference, commit validation | | `workstream-inventory-builder.cjs` | Pure workstream inventory projection builder | | `workstream-inventory.cjs` | Shared workstream inventory projection: state fields, phase/plan/summary counts, roadmap phase count, and active marker — thin orchestrator that delegates pure projection to `workstream-inventory-builder.cjs` | | `workstream-name-policy.cjs` | Canonical workstream name validation (`isValidActiveWorkstreamName`, `hasInvalidPathSegment`, `validateWorkstreamName`) and slug normalization (`toWorkstreamSlug`) | | `workstream.cjs` | Workstream CRUD, migration, session-scoped active pointer | +| `worktree-base-ref.cjs` | Worktree base-ref drift detection and degrade decision (`evaluateWorktreeBaseDegrade`) plus no-clobber `worktree.baseRef` settings management for the `base-check`/`set-baseref` subcommands (#683) | | `worktree-safety.cjs` | Worktree-root resolution and non-destructive prune policy decisions; owns W017 health-check logic | [`docs/CLI-TOOLS.md`](CLI-TOOLS.md) may describe a subset of these modules; when it disagrees with the filesystem, this table and the directory listing are authoritative. --- -## Hooks (14 shipped) +## Hooks (17 shipped) Full listing: `hooks/`. @@ -470,11 +482,14 @@ Full listing: `hooks/`. | `gsd-check-update.js` | `SessionStart` | Background check for new GSD versions | | `gsd-check-update-worker.js` | (worker) | Background worker helper for check-update | | `gsd-update-banner.js` | `SessionStart` | Opt-in banner surfacing update availability when GSD statusline isn't used (PR #2795) | +| `gsd-cursor-session-start.js` | Cursor `sessionStart` | Cursor-native context injection at session start (issue #777) | +| `gsd-cursor-post-tool.js` | Cursor `postToolUse` | Cursor-native STATE.md update monitor after tool calls (issue #777) | | `gsd-prompt-guard.js` | `PreToolUse` | Scans `.planning/` writes for prompt-injection patterns (advisory) | | `gsd-workflow-guard.js` | `PreToolUse` | Detects file edits outside GSD workflow context (advisory, opt-in) | | `gsd-read-guard.js` | `PreToolUse` | Advisory guard preventing Edit/Write on unread files | | `gsd-read-injection-scanner.js` | `PostToolUse` | Scans tool Read results for prompt-injection patterns (v1.36+, PR #2201) | | `gsd-worktree-path-guard.js` | `PreToolUse` | Hard-blocks Edit/Write/MultiEdit with absolute paths outside the worktree root (PR #579, #260) | +| `gsd-config-reload.js` | `FileChanged` | Hot-reloads GSD config context when `.planning/config.json` changes mid-session (#770) | | `gsd-session-state.sh` | `PostToolUse` | Session-state tracking for shell-based runtimes | | `gsd-validate-commit.sh` | `PostToolUse` | Commit validation for conventional-commit enforcement | | `gsd-phase-boundary.sh` | `PostToolUse` | Phase-boundary detection for workflow transitions | diff --git a/docs/README.md b/docs/README.md index e86925c66..5d773af02 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,6 +16,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md) ## How-to guides - [Install on your runtime](how-to/install-on-your-runtime.md) — runtime-specific install steps for all 15 supported runtimes +- [Install a minimal GSD and add skills later](how-to/install-minimal-and-add-skills.md) — install only the core skills, then grow the surface with profiles and `/gsd:surface` - [Discuss a phase](how-to/discuss-a-phase.md) — capture implementation decisions before planning begins - [Plan a phase](how-to/plan-a-phase.md) — run research, decompose work, and verify plan quality - [Execute a phase](how-to/execute-a-phase.md) — run plans in parallel waves with fresh-context subagents @@ -33,6 +34,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md) - [Migrate from GSD 2](how-to/migrate-from-gsd-2.md) — upgrade an existing GSD 2 project to GSD Core - [Update GSD](how-to/update-gsd.md) — re-run the installer to pick up the latest release - [Clean up get-shit-done-cc](cleanup-get-shit-done-cc.md) — remove leftover old-package artifacts that cause a spurious `⬆ /gsd:update` indicator after migrating to `@opengsd/gsd-core` +- [Fix the worktree base-mismatch (exit 42) error](how-to/fix-worktree-base-mismatch.md) — resolve the branch-divergence condition that halts parallel phase execution - [Recover and troubleshoot](how-to/recover-and-troubleshoot.md) — fix common problems, rebuild context, and uninstall --- diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 161552d2d..7f5d9d373 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -688,6 +688,16 @@ To assign different models on a non-Claude runtime: } ``` +#### Codex skill picker and agent scheduling (#774) + +GSD enriches each Codex install with two additional artifacts: + +- **Skill TUI chip** — each installed `gsd-*` skill directory contains an `agents/openai.yaml` file that populates the Codex `/skills` picker with a human-readable display name and a short description, so you can browse and invoke GSD skills from the Codex TUI without typing the full skill name. + +- **Flex-tier scheduling** — light-tier agents (haiku-equivalent) emit `service_tier = "flex"` and `model_verbosity = "low"` in their agent TOML. The Codex scheduler routes these agents to the flex tier (lower cost, background processing) and suppresses verbose token output. + +Both enrichments are written automatically at install time and require no manual configuration. Requires Codex CLI ≥ 0.130.0. + #### Switching from Claude to Codex with one config change (#2517) ```json @@ -699,6 +709,15 @@ To assign different models on a non-Claude runtime: See [Runtime-Aware Profiles](CONFIGURATION.md#runtime-aware-profiles-2517). +#### Per-runtime command enrichment + +When generating artifacts, the installer adapts GSD commands to each runtime's native command schema: + +- **Gemini CLI** — generated TOML commands use Gemini's `{{args}}` placeholder (translated from Claude's `$ARGUMENTS`) so typed arguments interpolate into the prompt, and `/gsd:progress` injects live project state via a fixed `!{cat .planning/STATE.md 2>/dev/null}` shell block (no interpolated input, so no injection risk; Gemini shows its standard confirmation dialog). +- **Qwen Code** — main-loop skills carry Qwen's numeric `priority` field so the most-used workflows (e.g. `new-project`, `plan-phase`, `execute-phase`) sort first in the `/skills` list; utility skills are left unset. Higher values sort earlier; the field affects only the `/skills` list order. + +See [How to install GSD Core on your runtime](how-to/install-on-your-runtime.md) for the full per-runtime details. + ### Manual install / no-Node.js setup If you cannot run the GSD installer, you cannot use the source files in `agents/` directly — they are in Claude Code's native frontmatter format. For OpenCode, two transformations are required: @@ -727,12 +746,51 @@ npx @opengsd/gsd-core --cline --local # this project only npx @opengsd/gsd-core --codebuddy --global ``` +GSD installs four surfaces for CodeBuddy: `/gsd-*` slash commands in `~/.codebuddy/commands/`, subagents in `~/.codebuddy/agents/`, model-invocable skills in `~/.codebuddy/skills/`, and `settings.json` hooks. The skills are emitted with `user-invocable: false` so the slash commands are the single `/` menu surface (no duplicate entries). + ### Installing for Qwen Code ```bash npx @opengsd/gsd-core --qwen --global ``` +### Installing as a Gemini CLI extension (#775) + +GSD ships a `gemini-extension.json` extension manifest at the repository root, so +Gemini CLI users can install, update, and remove GSD through Gemini's own +extension lifecycle — and have it show up in `gemini extensions list`: + +```bash +# Install (Gemini clones the repo and copies the extension) +gemini extensions install https://github.com/open-gsd/gsd-core + +# Update to the latest released manifest version +gemini extensions update gsd-core + +# Remove +gemini extensions uninstall gsd-core +``` + +For local development against a checkout, symlink it instead of copying: + +```bash +gemini extensions link /path/to/gsd-core +``` + +**What the extension delivers today:** it loads GSD's operating context +(`GEMINI.md`) into every Gemini session in the project, and gives you the +discoverable install/update/remove lifecycle above. The `/gsd:*` slash commands, +agents, and hooks are still installed via the dedicated installer: + +```bash +npx @opengsd/gsd-core --gemini --global +``` + +The two paths are complementary and additive — installing the extension does not +change or replace the `npx gsd-core --gemini` install, and either can be used on +its own. (Slash-command/agent/hook projection into the extension package itself +is a planned follow-up.) + ### Installing for Prerelease Editions Set the runtime's `*_CONFIG_DIR` env var to the prerelease directory before running the installer: @@ -749,7 +807,7 @@ WINDSURF_CONFIG_DIR=~/.codeium/windsurf-next npx @opengsd/gsd-core@latest --wind | Gemini CLI | `~/.gemini` | `GEMINI_CONFIG_DIR` | | OpenCode | `XDG_CONFIG_HOME/opencode` | `OPENCODE_CONFIG_DIR` | | Codex | (per Codex CLI) | `--config-dir` flag | -| Copilot | `~/.copilot` | `COPILOT_CONFIG_DIR` | +| Copilot | `~/.copilot` | `COPILOT_CONFIG_DIR` (or `COPILOT_HOME`) | | Cursor | `~/.cursor` | `CURSOR_CONFIG_DIR` | | Windsurf | `~/.codeium/windsurf` | `WINDSURF_CONFIG_DIR` | | Antigravity | auto-detected | `ANTIGRAVITY_CONFIG_DIR` | @@ -772,6 +830,18 @@ Set `commit_docs: false` during `/gsd-new-project` or via `/gsd-settings`. Add ` Since v1.17, the installer backs up locally modified files to `gsd-local-patches/`. Run `/gsd-update --reapply` to merge your changes back. +### Install or Refresh a Release Candidate + +To install or refresh GSD from the `@next` RC dist-tag (the pre-release channel established by ADR #660), run: + +```bash +/gsd-update --next +# or equivalently: +/gsd-update --rc +``` + +The same scope/runtime detection, changelog preview, custom-file backup, and cache clearing apply. Omitting `--next`/`--rc` keeps targeting `@latest` (stable channel, no change). Only the `@latest` and `@next` channels are supported — no arbitrary dist-tag can be passed. + ### Cannot Update via npm See [docs/manual-update.md](manual-update.md) for a step-by-step manual update procedure. @@ -780,6 +850,35 @@ See [docs/manual-update.md](manual-update.md) for a step-by-step manual update p When a workflow fails in a non-obvious way, run `/gsd-forensics` to generate a diagnostic report covering git history anomalies, artifact integrity, and state inconsistencies. Output goes to `.planning/forensics/`. +### Pre-populated Permissions (Claude Code) + +Since v1.3.1, the installer pre-populates `~/.claude/settings.json` (or +`settings.local.json` for local installs) with the core permissions GSD needs: + +```json +{ + "permissions": { + "allow": [ + "Bash(npx gsd-core *)", + "Read(.planning/*)", + "Write(.planning/*)", + "Read(STATE.md)", + "Write(STATE.md)" + ], + "deny": [ + "Read(.env)", + "Read(.env.*)", + "Read(.secrets)" + ] + } +} +``` + +These entries eliminate first-run approval prompts for GSD's own tool calls. The +merge is non-destructive — your existing permissions are preserved and GSD entries +are only appended. Uninstalling GSD removes exactly these entries and preserves +any others. + ### Executor Subagent Gets "Permission denied" on Bash Commands Add the required patterns to `~/.claude/settings.json`. Core patterns needed for all stacks: diff --git a/docs/adr/0656-research-module-seam.md b/docs/adr/0656-research-module-seam.md new file mode 100644 index 000000000..096930c6b --- /dev/null +++ b/docs/adr/0656-research-module-seam.md @@ -0,0 +1,45 @@ +# ADR-0656: Research Module — L2-hybrid seam for cached, curated-first research + +- **Status:** Accepted +- **Date:** 2026-06-03 + +## Context + +Research in GSD was entirely prose-duplicated. Seven researcher agents each carried their own copy of the provider waterfall (Context7, Ref, Jina, Exa, Tavily, Perplexity, Brave, Firecrawl, websearch), their own confidence-tier definitions, and their own fallback policy. Every time a new provider was added or the ordering changed, all seven files drifted independently — the exact failure mode `META.RULE.brief-no-paraphrase` exists to prevent. + +There was no research cache. Agents checked for an existing `RESEARCH.md` file but had no TTL, no content-addressing, and no notion of staleness. Identical queries re-fetched from live providers across phases and projects. + +Package legitimacy was a pip-install `slopcheck` bolt-on. When the `slopcheck` binary was absent or crashed, every package was silently downgraded to `[ASSUMED]`, removing the legitimacy gate entirely rather than degrading gracefully. + +Context7 was prompt-only: agents mentioned it in prose but there was no code-level integration, no cache, and no structured verdict returned to the orchestrator. + +## Decision + +Introduce an **L2-hybrid seam**: code owns cache, provider policy, legitimacy verdicts, and confidence classification; MCP owns the actual network fetch (a `.cjs` module cannot call MCP tools directly). + +Three modules are introduced under `src/` compiled to `gsd-core/bin/lib/*.cjs` per ADR-457 (generated-single-source): + +**Research Store** (`src/research-store.cts`): content-addressed cache keyed by `sha256(ecosystem + library + version + query + kind)`. `getResearch()` never throws — it returns `{ hit, stale }` mirroring the graphify staleness tri-state pattern. TTL is per-source: curated-doc providers get 30 days (HIGH), medium-quality sources get 7 days (MED), web/synthesis gets 1 day (LOW). Two storage tiers: curated-doc kinds write to `~/.gsd/research-cache` (cross-project reuse); web and synthesis results write to `.planning/research/.cache` (project-local, gitignored). + +**Research Provider** (`src/research-provider.cts`): single source of truth for `PROVIDER_WATERFALL`. Docs waterfall: Context7 → Ref → Jina → websearch. Web waterfall: Exa → Tavily → Perplexity → Brave → websearch. Scrape: Firecrawl → Jina (Firecrawl is scrape-only, not in docs/web discovery). `planResearch()` returns cache hits plus a fetch plan for misses. `classifyConfidence()` stamps `HIGH | MEDIUM | LOW` by provider authority + verification evidence — the tier set is unchanged (ADR-consistent), but HIGH now requires code-computed ground-truth corroboration (e.g. `legitimacyVerdict: 'OK'`); provider authority alone caps at MEDIUM; `SLOP` caps at LOW. Provider availability is driven by config flags and `_API_KEY` env vars; `context7`, `jina`, and `websearch` are always available as the terminal fallback. + +**Package Legitimacy** (`src/package-legitimacy.cts`): registry-API verdicts via injectable adapters for npm, PyPI, and crates.io. Thresholds: `{ minAgeDays: 30, minWeeklyDownloads: 1000, requireRepo: true }`. Verdict per package: `OK | SUS | SLOP`. `slopcheck` is an optional escalate-only adapter — it can only raise a verdict, never lower it — and is not the install-or-degrade gate. Absence of `slopcheck` leaves registry-API verdicts intact rather than downgrading everything to `[ASSUMED]`. + +All three modules are reachable via `gsd-tools query research-plan | research-store | package-legitimacy`. + +Agents return a `RESEARCH.md` path; they never return raw fetched content. This enforces context discipline: subagent isolation, compact provider output, fetches-to-disk, cache-returns-digest. + +## Consequences + +**Positive:** +- Provider policy lives in one tested module. Adding or reordering a provider is a one-line change that propagates to all researcher agents. +- Content-addressed cache eliminates redundant fetches across phases and projects. +- Package legitimacy is registry-API-first and degrades gracefully; `slopcheck` enriches without gating. +- The `gsd-tools query` interface is the test surface — behavioral tests can assert typed JSON output without source-grep. + +**Deferred to #657:** +- Collapsing the seven researcher agent `.md` files into generated-from-profiles agents (the prose waterfall duplication in those files is the primary `DEFECT.RESEARCH-PROVIDER-PROSE-DRIFT` site). +- The `install.js` MCP tool-mapping for tavily, ref, and jina (those land where the agents declare the tools they need). + +**Known constraint:** +API context-editing primitives (`clear_tool_uses`, memory tool) are the conceptual model for context discipline, but they are not configurable through the Claude Code harness today. The current implementation achieves context discipline through subagent isolation and fetch-to-disk patterns. diff --git a/docs/adr/58-runtime-install-policy-module.md b/docs/adr/58-runtime-install-policy-module.md new file mode 100644 index 000000000..5cbe35794 --- /dev/null +++ b/docs/adr/58-runtime-install-policy-module.md @@ -0,0 +1,56 @@ +# Runtime Install Policy Module owns the typed install-plan projection + +- **Status:** Accepted +- **Date:** 2026-06-07 +- **Issue:** #58 + +## Context + +Runtime install logic is currently spread across one-off helper functions. `getGlobalDir(runtime, explicitDir)` in `bin/install.js` switch-dispatches to per-runtime helpers (`getOpencodeGlobalDir`, `getKiloGlobalDir`), and `getAgentsDir` lives separately in `src/core.cts`. These helpers resolve directories at ~11 call sites and are free to drift from the behavior that install and runtime-query paths actually expect, because nothing owns the *composition* of an install decision as a single value. + +Two adjacent seams already exist: + +- **ADR-3660 (Runtime Artifact Layout Module)** owns *where* per-runtime artifacts (commands, agents, skills) are placed. +- **ADR-0009 (Shell Command Projection Module)** owns runtime-aware *command text* rendering (quoting, path style, wrapper prefixes). + +But no ADR owns composing those — placements + command text + per-runtime config intentions — into one unified, typed install-plan projection. That missing seam is why directory/config logic re-derives itself ad hoc at each call site. + +## Decision + +Introduce a **Runtime Install Policy Module** as the seam that, given a runtime and an install context, **projects a pure, typed `InstallPlan` value** describing everything that should happen for that runtime. The projection: + +- composes artifact placements by delegating to the Runtime Artifact Layout Module (ADR-3660), +- composes command text by delegating to the Shell Command Projection Module (ADR-0009), +- declares config *intentions* (which config files need which keys/values for that runtime), +- performs **no filesystem IO** and **no format-specific serialization** while resolving the plan. + +Concrete execution is owned by runtime-specific **adapters** (made explicit as a registry in #60). Adapters consume the `InstallPlan` and perform the effectful work: file mutations, directory creation, and rendering format-specific config (TOML, JSON, Markdown) for their runtime. + +This follows the repository's established pure-policy-projects / thin-adapters-execute pattern (ADR-0001, Dispatch Policy Module): the `InstallPlan` is the narrow waist, resolution stays free of IO, and callers become thin adapters over a stable interface rather than re-deriving directory logic. + +## What stays OUTSIDE the policy module + +To keep the abstraction honest about the filesystem boundary, the following are explicitly **not** the policy module's responsibility and remain in the runtime adapters: + +- Concrete TOML / JSON / Markdown read-modify-write and serialization. +- Merge semantics for pre-existing config files (preserving user keys, ordering, formatting). +- Filesystem effects: directory creation, atomic write/rename, existence/permission checks. +- Any path resolution that requires touching the disk. + +The policy module resolves *intent* as data; adapters turn that intent into bytes on disk. + +## Consequences + +- Install logic becomes testable as pure data: assert the projected `InstallPlan` for a runtime without a filesystem. +- The scattered directory helpers (`getGlobalDir`, `getOpencodeGlobalDir`, `getKiloGlobalDir`, `getAgentsDir`) gain a single projection to migrate onto, retiring or narrowing them (tracked in #56). +- The plan/adapter contract becomes a stability surface that must be held narrow; drift there reintroduces the very divergence this seam removes. +- Rollout is incremental, not big-bang: this ADR establishes the boundary (#58); the explicit Runtime Adapter Registry lands next (#60); legacy helper retirement follows (#56); downstream cleanup in #57. + +## References + +- ADR-0001 — Dispatch Policy Module (pure-policy-projects / thin-adapters-execute precedent). +- ADR-3660 — Runtime Artifact Layout Module (per-runtime artifact placement; delegated to by this projection). +- ADR-0009 — Shell Command Projection Module (runtime-aware command text; delegated to by this projection). +- ADR-0008 — Installer Migration Module (adjacent installer seam). +- `CONTEXT.md` § Glossary — Domain modules and seams (the architecture seam map / glossary this module is registered in). +- Installer-refactor chain: #58 (this ADR) → #60 (explicit adapter registry) → #56 (retire legacy directory helpers) → #57. diff --git a/docs/adr/766-claude-code-plugin-manifest-module.md b/docs/adr/766-claude-code-plugin-manifest-module.md new file mode 100644 index 000000000..c150f8d03 --- /dev/null +++ b/docs/adr/766-claude-code-plugin-manifest-module.md @@ -0,0 +1,70 @@ +# Claude Code Plugin Manifest Module owns the projection of gsd-core surfaces onto the Claude Code plugin contract + +- **Status:** Accepted +- **Date:** 2026-06-07 +- **Issue:** #766 +- **Implementation:** PR #797 + +## Context + +gsd-core has, until now, reached Claude Code through exactly one Adapter: the file-copy installer. The **Runtime Artifact Layout Module** (ADR-3660) projects gsd-core's artifact surfaces (`commands`, `agents`, `skills`) onto per-runtime filesystem placements, and the **Runtime Install Policy Module** (ADR-58) composes those placements with command text and config intentions into a typed install plan that adapters write to `~/.claude/` / `.claude/`. + +Claude Code now exposes a second, first-class way to receive the same surfaces: the **plugin contract** — a `.claude-plugin/plugin.json` manifest plus a `hooks/hooks.json`, consumed either by a marketplace install or by the zero-friction `@skills-dir` path. This contract is an *external interface owned by Claude Code*, not by gsd-core: it has its own schema, its own namespacing rules (`/:`), its own validation tool (`claude plugin validate`), and its own constraints (notably: plugin-shipped agents may not carry `hooks` / `permissionMode` / `mcpServers` frontmatter — Claude Code silently ignores them). + +Before this ADR, the only record of how gsd-core maps onto that external contract was the manifest files themselves. A hand-authored config file with no named Seam invites drift: the manifest's hook wiring silently diverges from what the Installer Module wires into `settings.json`; the identity fields drift from the Package Identity Module; and a future maintainer has no single place that says *which gsd-core surface maps to which manifest field, and why*. The plugin contract is exactly the kind of external interface that earns a defined, typed mapping rather than an ad-hoc file — the same reasoning that gave the file-copy path the Runtime Artifact Layout Module. + +This is the structural signal the architecture review looks for: **two Adapters at one Seam.** The file-copy layout and the plugin manifest are two projections of *the same* gsd-core artifact surfaces onto two different distribution contracts. That makes the distribution Seam real, and the plugin-side projection deserves a name. + +## Decision + +Introduce the **Claude Code Plugin Manifest Module** as the Seam that owns the projection of gsd-core's artifact surfaces onto the Claude Code plugin contract. It is the plugin-contract sibling of the Runtime Artifact Layout Module: where that Module projects surfaces onto filesystem placements, this Module projects the same surfaces onto `.claude-plugin/plugin.json` + `hooks/hooks.json`. + +The mapping is **defined, not incidental**: + +| gsd-core surface / source | Claude Code plugin field | Rule / invariant | +|---|---|---| +| Package Identity Module `binName` | `name` | `gsd-core` — drives the `/gsd-core:` command namespace; must be kebab-case (no colon/space/uppercase). | +| Package Identity Module `repoUrl` | `repository`, `homepage` | derived, never re-typed. | +| `package.json` `version` / `description` / `license` | `version` / `description` / `license` | `version` is **required** for `claude plugin validate --strict` (a missing version is a strict failure), so it is synced to `package.json` and held by a drift-guard test. | +| Command surface (`commands/gsd/*.md`) | `commands: "./commands/gsd/"` | exposed as `/gsd-core:`; namespacing replaces the file-copy path's `/gsd:` (an additive UX change, not a data-format break). | +| Agent surface (`agents/*.md`) | *(omitted — default `agents/` discovery)* | the explicit `agents: ` form is rejected by the plugin schema; relying on Claude Code's default `agents/` discovery loads them and stays self-maintaining. Agents are already plugin-safe — their `hooks`/`permissionMode` frontmatter is inert. | +| Always-on hook policy (subset of the Installer Module's `settings.json` wiring) | `hooks: "./hooks/hooks.json"` | see below. | + +The hook projection is the load-bearing part of this Module, because of the external constraint: a plugin's agents cannot carry hook frontmatter, so **all plugin-path hook wiring must live in `hooks/hooks.json`**. The Module projects *only the always-on subset* of the Installer Module's Claude hook wiring — `gsd-check-update` (SessionStart), `gsd-context-monitor` (PostToolUse), and the security guards `gsd-prompt-guard` / `gsd-read-guard` / `gsd-worktree-path-guard` / `gsd-read-injection-scanner` — preserving each event, matcher, and timeout. The installer's **config-gated opt-in** hooks (workflow-guard, validate-commit, graphify-update, session-state, phase-boundary, update-banner) are deliberately excluded: a static manifest cannot read a project's `.planning/config.json` to honor those gates, so projecting them would run them unconditionally — a behavior change the Module must not introduce. Hook commands reference bundled scripts through Claude Code's `${CLAUDE_PLUGIN_ROOT}` variable. + +The interface of this Module is therefore a **conformance contract**, validated two ways: `claude plugin validate --strict` (the external tool's view) and an in-repo drift-guard test (`tests/issue-766-plugin-manifest.test.cjs`) that locks the identity mapping, the version sync, the always-on hook contract, and the absence of opt-in hooks. Manifest component paths are resolved relative to the **plugin root** (the directory containing `.claude-plugin/`), which is the repository root. + +This is **additive**. The file-copy path — Runtime Artifact Layout Module, Runtime Install Policy Module, Installer Module — is unchanged. The plugin manifest is a parallel Adapter, the fallback for users on older Claude Code versions that predate the plugin contract. + +## What stays OUTSIDE this Module + +To keep the Seam honest about where the plugin contract ends: + +- **Runtime execution.** The Module projects the command/agent/hook *surface* and lifecycle metadata. It does not make gsd commands self-contained: their backing logic still resolves the gsd runtime CLI (`gsd-tools`) and `node` on `PATH`. The plugin delivers discoverability and lifecycle (`claude plugin enable|disable|update`); it does not replace the runtime. +- **The file-copy install.** Filesystem placement, `settings.json` merge semantics, and per-runtime config rendering remain owned by the Runtime Artifact Layout / Install Policy / Installer Modules. +- **Marketplace listing.** Publishing gsd-core to a marketplace registry is an external, out-of-repo act. +- **Manifest emission by the installer.** Having `bin/install.js` drop the manifest in-place for the npm `@skills-dir` path is a follow-up; the repo-root manifest already serves the marketplace and git-clone `@skills-dir` paths. + +## Consequences + +- gsd-core gains a one-command install/update/disable lifecycle and automatic `/gsd-core:` namespacing that prevents slash-command collisions, without disturbing the file-copy path. +- The plugin contract gains a named place in the glossary (`CONTEXT.md`) and a defined mapping, so future surface additions have an obvious projection target instead of an ad-hoc file edit. +- **Latent duplication is now named, not hidden.** The always-on hook policy is currently encoded twice — imperatively in the Installer Module's `settings.json` wiring, and declaratively in `hooks/hooks.json` — kept in agreement only by the drift-guard test. This ADR records that as the known cost of a *static* manifest. Elevating the Module from a hand-authored manifest to a **generated projection** (stamping `plugin.json` from the Package Identity Module + `package.json`, and `hooks/hooks.json` from `managed-hooks-registry.cjs` + a shared always-on-hook policy) would collapse the duplication to one source — the same generated-single-source move ADR-457 made for `.cjs` and the Runtime Install Policy Module made for install plans. Deferred; see Open questions. +- The `name` field is a stability surface: it is the published `/gsd-core:` namespace. Changing it is a user-visible break under Hyrum's law, the same way command names are. +- Rollout is incremental: this ADR + the hand-authored manifest land first (#766/PR#797); installer-emit, release-time version stamping, and the generated projection are tracked follow-ups under #766. + +## Open questions + +- Should this Module be **generated** rather than hand-authored, deriving `version` (and identity) at build/release time so a `package.json` bump cannot leave `plugin.json` stale? The release pipeline bumps via `npm version --no-git-tag-version` with no regeneration hook, so today the drift-guard test enforces the sync manually (idiomatic with the repo's other drift guards, but a release speed-bump). +- Should the always-on-hook policy be lifted into a single shared source consumed by *both* the Installer Module and this Module, retiring the dual hand-encoding? + +## References + +- ADR-3660 — Runtime Artifact Layout Module (the file-copy sibling: projects the same surfaces onto filesystem placements). +- ADR-58 — Runtime Install Policy Module (typed install-plan projection for the file-copy path). +- ADR-457 — Generated single-source (the precedent a generated manifest projection would follow). +- ADR-0008 — Installer Migration Module (adjacent installer Seam). +- Package Identity Module (`gsd-core/bin/lib/package-identity.cjs`) — source of the manifest's identity fields. +- Installer Module (`bin/install.js`) — owns the `settings.json` always-on hook wiring this Module mirrors for the plugin path. +- `CONTEXT.md` § Glossary — Domain modules and seams (where this Module is registered). +- Claude Code plugin contract: . diff --git a/docs/adr/README.md b/docs/adr/README.md index f59e54ce6..e91265d49 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -54,6 +54,8 @@ See **[CONTRIBUTING.md — "Proposing an ADR or PRD"](../../CONTRIBUTING.md#prop | [456-test-rigor-architecture.md](456-test-rigor-architecture.md) | Test-rigor architecture — deterministic scheduling, antagonistic tier, typed-surface mandate, delete-bad-tests policy | Accepted | | [457-generated-cjs-single-source.md](457-generated-cjs-single-source.md) | Collapse hand-written CJS to generated single-source | Proposed | | [660-release-from-next-head.md](660-release-from-next-head.md) | Release from the head of next; immutable release tags; @next dist-tag as the RC surface | Proposed | +| [58-runtime-install-policy-module.md](58-runtime-install-policy-module.md) | Runtime Install Policy Module owns the typed install-plan projection | Accepted | +| [766-claude-code-plugin-manifest-module.md](766-claude-code-plugin-manifest-module.md) | Claude Code Plugin Manifest Module owns the projection of gsd-core surfaces onto the Claude Code plugin contract | Accepted | ## Seam map diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md index 04b40b648..a78db6082 100644 --- a/docs/agents/triage-labels.md +++ b/docs/agents/triage-labels.md @@ -9,6 +9,7 @@ Maps the five canonical triage roles to the actual label strings in `open-gsd/gs | `ready-for-agent` | `confirmed` | Bug verified + fully specified — AFK agent can pick up | | `ready-for-human` | `approved-enhancement` / `approved-feature` | Enhancement/feature approved by maintainer — human codes it | | `wontfix` | `wontfix` | Will not be actioned | +| `possible-duplicate` | `possible-duplicate` | Applied by the Duplicate check workflow when a new issue's title closely matches existing open issues. The reporter (or a maintainer) replies justifying why it is not a duplicate within 24h, or the Duplicate auto-close sweep closes it. A reply clears this label and applies needs-maintainer-review for human adjudication. React 👎 to the bot comment to veto auto-close. | ## Notes on this repo's label model @@ -17,3 +18,12 @@ Maps the five canonical triage roles to the actual label strings in `open-gsd/gs - There is no separate "ready-for-human" vs "ready-for-agent" distinction for enhancements — both flow through the same `approved-*` labels. If the work requires human judgment (design decisions, external access), note it in the issue body. - `needs-triage` is removed when any other state label is applied. - `needs-reproduction` is used instead of the generic `needs-info` — be specific in triage comments about what reproduction steps or information are missing. + +## Duplicate detection lifecycle + +The `possible-duplicate` label is managed by three GitHub Actions workflows that together form a self-service deduplication loop: + +1. **Detect on open** — When an issue is opened, `duplicate-check.yml` scores its title against all other open issues using Dice-coefficient similarity. If any match clears the threshold, the bot posts a challenge comment listing the similar issues and applies `possible-duplicate`. +2. **Challenge comment + reporter window** — The reporter (or a maintainer) has `DEFAULT_WINDOW_HOURS` (24h) to reply explaining why the issue is not a duplicate. Reacting 👎 to the bot comment also signals the reporter objects to auto-close. +3. **Daily sweep auto-close** — `duplicate-sweep.yml` runs at 07:00 UTC daily. For each open issue with `possible-duplicate`, it checks whether the window has elapsed, whether the reporter replied, and whether a 👎 reaction exists. Issues with exempt labels (`priority: critical`, `pinned`, `confirmed-bug`, `confirmed`, `fix-pending`) are never auto-closed. Issues that pass the close check receive a closing comment and are closed with `state_reason: duplicate`. +4. **Reporter reply clears label** — `remove-duplicate-label.yml` fires on every new non-bot comment. If the issue still carries `possible-duplicate`, it removes that label and applies `needs-maintainer-review` (the value of `HUMAN_REVIEW_LABEL` in `scripts/issue-dedupe.cjs`), routing the issue to a maintainer for manual adjudication. diff --git a/docs/branching.md b/docs/branching.md index 6dd40958a..22398239f 100644 --- a/docs/branching.md +++ b/docs/branching.md @@ -74,7 +74,7 @@ These are work branches. Open one, push commits, PR it, merge it, let it auto-de | `perf/` | Performance work, no behavior change | `next` | `perf/3300-skill-index` | | `ci/` | CI/workflow changes only | `next` | `ci/3801-add-node-26-matrix` | | `revert/` | Reverting a previously-merged change | `next` (or `main` if urgent) | `revert/3919-bad-merge` | -| `hotfix/X.Y.Z` | Patch release branch (created by `hotfix.yml`) | `main` | `hotfix/1.27.1` | +| `hotfix/X.Y.Z` | Patch release branch (created by `release.yml` with a patch version X.Y.Z) | `main` | `hotfix/1.27.1` | | `release/X.Y.0` | Minor/major release branch (created by `release.yml`) | `main` | `release/1.28.0` | > **The branch name rule is enforced** by `.github/workflows/branch-naming.yml`. @@ -130,10 +130,10 @@ git pull --ff-only git checkout -b fix/3919-critical-crash # ... commit, push, PR to next, merge. -# 2. Trigger the hotfix workflow from the Actions tab: -# workflow: Hotfix Release +# 2. Trigger the Release workflow (release.yml) from the Actions tab: +# workflow: Release # action: create -# version: 1.27.1 (next patch number) +# version: 1.27.1 (next patch number, X.Y.Z) # auto_cherry_pick: true (default) ``` @@ -148,7 +148,7 @@ The workflow: 6. `finalize` publishes to npm `@latest`, tags `v1.27.1`, opens merge-back PRs to **both** `main` and `next`. -> See `.github/workflows/hotfix.yml` and `VERSIONING.md` for the deep dive. +> See `.github/workflows/release.yml` and `VERSIONING.md` for the deep dive. ### Flow 3 — Minor or major release @@ -167,6 +167,7 @@ When `next` has accumulated enough work to ship a new minor/major. - Each fix should also be PR'd to `next` so the next release has it too (or wait for the auto-back-merge after finalize) - Trigger `rc` action to publish RC builds: 1.28.0-rc.1, rc.2, ... + - Each `rc` run also prints a non-destructive preview of the curated `## [X.Y.0]` CHANGELOG section in the Actions job summary — rendered from the `.changeset/` fragments without consuming them — so you can review the upcoming release notes during RC testing. 4. When stable: trigger `finalize`. - Publishes to npm @latest @@ -212,7 +213,7 @@ resolve anyway. The treadmill is gone. ## Cheat sheet: "where does my PR go?" ``` -Is it a hotfix release branch? → main (cut by hotfix.yml) +Is it a hotfix release branch? → main (cut by release.yml with a patch version X.Y.Z) Is it a stable release branch? → main (cut by release.yml) Is it an RC-blocker fix? → release/X.Y.0 (and also next, or rely on back-merge) Is it everything else? → next @@ -248,10 +249,10 @@ the base branch — no need to recreate the PR. ``` - **Phasing in:** see the migration notes in `docs/adr/230-introduce-next-integration-branch.md`. -- **Updating release.yml / hotfix.yml:** these workflows currently branch - from `main` and cherry-pick from `main`. After phase-2 of the migration - they should branch from `next` (release) and cherry-pick from `next` - (hotfix). The patches are inlined in the ADR. +- **Updating release.yml:** this workflow currently branches from `main` and + cherry-picks from `main`. After phase-2 of the migration it should branch + from `next` (release) and cherry-pick from `next` (hotfix). The patches + are inlined in the ADR. --- @@ -262,7 +263,7 @@ A few exceptions where the rules above bend: - **True production-down emergency.** Push directly to a `fix/critical-*` branch, PR to `main`. The auto-back-merge workflow will replay it onto `next` within minutes. Use sparingly — most "urgent" things are fine to go - through `next` and ship same day via the hotfix workflow. + through `next` and ship same day via the Release workflow (patch version). - **Documentation-only typo on a published page.** If the only change is a doc fix that's visible right now and shouldn't wait for the next release, PR it to `main`. The auto-back-merge will sync `next`. Most doc changes diff --git a/docs/how-to/fix-worktree-base-mismatch.md b/docs/how-to/fix-worktree-base-mismatch.md new file mode 100644 index 000000000..16b445198 --- /dev/null +++ b/docs/how-to/fix-worktree-base-mismatch.md @@ -0,0 +1,143 @@ +# How to fix the worktree base-mismatch (exit 42) error + +**Goal:** Understand why `/gsd-execute-phase` halts with `FATAL: worktree base mismatch` / exit 42 when your branch is ahead of the default branch, and choose the right fix to restore normal — or parallel — execution. + +**Prerequisites:** GSD Core is installed and you have an active project. You have run `/gsd-execute-phase` and either seen the exit-42 error or the one-line `⚠ Worktree base mismatch` warning. + +--- + +## What you will see + +When you run `/gsd-execute-phase` on a branch that is ahead of the repository's default branch (for example, an unmerged milestone branch, a long-lived feature branch, or a branch with commits not yet in `origin/HEAD`), you may see one of two messages: + +**Automatic-degrade warning (phase still completes):** + +``` +⚠ Worktree base mismatch: HEAD (abc12345) differs from origin/HEAD (def67890). +Running this phase sequentially on the main working tree. +To keep parallel worktrees, set worktree.baseRef:"head" in +.claude/settings.local.json (or run: gsd-tools worktree set-baseref). See #683. +``` + +The phase runs to completion sequentially; nothing is blocked. This is the runtime mitigation. + +**Exit-42 halt (older installs or misconfigured environments):** + +``` +FATAL: worktree base mismatch +``` + +All worktree-isolated executors halt immediately. Zero progress is made. + +--- + +## Why this happens + +Claude Code's `isolation="worktree"` forks executor worktrees from the repository's default branch (`origin/HEAD`), not from your current `HEAD`. When your branch contains commits that `origin/HEAD` does not have — plan files, new source files, anything added since the branch diverged — those files are absent inside each worktree. GSD's `worktree-branch-check` safety guard correctly refuses to act on a worktree that does not match the orchestrator's state, and exits with code 42. + +This is the guard working as designed: it prevents silent data loss or phantom edits in the wrong tree. The error is a branch-state condition, not an OS-specific or hardware issue. + +--- + +## Option 1 — Do nothing (you are already unblocked) + +If you saw the `⚠ Worktree base mismatch` warning rather than an exit-42 halt, GSD has already automatically degraded to sequential execution on the main working tree for this run. The phase will complete. No action is required. + +Use this option when: + +- You are on a diverged branch temporarily +- You do not care about parallel execution for this phase +- You want to merge back to the default branch soon + +--- + +## Option 2 — Permanent fix: set `worktree.baseRef: "head"` (recommended) + +This option restores parallel worktree execution on diverged branches. It tells Claude Code to fork executor worktrees from your current `HEAD` instead of `origin/HEAD`, so the plan files and branch-only commits are present in every worktree. + +Run the convenience command from your project root: + +```bash +node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" worktree set-baseref +``` + +This writes `worktree.baseRef: "head"` into `.claude/settings.local.json` in your project root. It is no-clobber: if you already have an explicit `baseRef` set to something else, it leaves your value in place and tells you. + +To verify the result: + +```bash +node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" worktree base-check +``` + +The output is JSON. When `shouldDegrade` is `false` and `reason` is `"baseref-head"`, parallel worktrees will work on any branch. + +Alternatively, set the value by hand in `.claude/settings.local.json`: + +```json +{ + "worktree": { + "baseRef": "head" + } +} +``` + +**Note:** Fresh installs and upgrades of GSD Core both set `worktree.baseRef:"head"` automatically in `.claude/settings.local.json` (no-clobber) when `workflow.use_worktrees` is enabled (the default). You can also apply or re-apply it manually at any time with `gsd-tools worktree set-baseref` — for example, if you toggled worktrees on after the initial install. + +Use this option when: + +- You regularly work on long-lived or milestone branches +- You want parallel phase execution (faster, lower context-window pressure) +- You are a solo developer or team working on a feature branch for an extended period + +--- + +## Option 3 — Fallback: disable worktrees entirely + +If worktrees are causing persistent problems beyond the base-mismatch (for example, your environment does not support them), disable them permanently for this project: + +Add or edit `.planning/config.json`: + +```json +{ + "workflow": { + "use_worktrees": false + } +} +``` + +All executor agents will then run sequentially on the main working tree for every phase. This is equivalent to what the automatic degrade does, but permanent. + +Use this option when: + +- Worktrees are consistently problematic in your environment +- You prefer sequential execution for auditability or tooling reasons +- You are on a platform or CI setup that does not support git worktrees + +See also: [`workflow.use_worktrees`](../CONFIGURATION.md#workflow-toggles) in the configuration reference. + +--- + +## The exit-42 backstop + +The `worktree-branch-check` guard (exit 42) remains active in all execution modes as a safety backstop. It fires only when an executor worktree's branch does not match the expected orchestrator state — a condition that should not arise once you have applied one of the options above. If you continue to see exit 42 after setting `worktree.baseRef: "head"`, run `/gsd-forensics` to investigate. + +--- + +## Summary + +| Situation | Recommended action | +|-----------|-------------------| +| Saw the warning, phase completed | Nothing — degrade handled it automatically | +| Regularly on diverged branches, want parallel execution | `worktree set-baseref` (Option 2) | +| Worktrees consistently problematic | Set `workflow.use_worktrees: false` (Option 3) | +| Still seeing exit 42 after fixes | Run `/gsd-forensics "exit 42 after fix"` | + +--- + +## Related + +- [Recover and troubleshoot](recover-and-troubleshoot.md) +- [Debug a failed execution](debug-a-failed-execution.md) +- [Configuration reference — workflow toggles](../CONFIGURATION.md#workflow-toggles) +- [CLI Tools reference — worktree commands](../CLI-TOOLS.md#worktree-commands) +- [docs index](../README.md) diff --git a/docs/how-to/install-minimal-and-add-skills.md b/docs/how-to/install-minimal-and-add-skills.md new file mode 100644 index 000000000..e30537af7 --- /dev/null +++ b/docs/how-to/install-minimal-and-add-skills.md @@ -0,0 +1,127 @@ +# How to install a minimal GSD and add skills later + +Install GSD Core with a small skill footprint to keep cold-start context low, then grow the surface — live or on reinstall — only when you need more. Use this when context budget matters: large existing projects, constrained models, or runtimes where every description token counts. + +**What you need:** A supported runtime and the standard installer prerequisites (Node.js 18+ and npx). If you have not installed GSD at all yet, read [Install on your runtime](install-on-your-runtime.md) first — this guide covers the *profile* choice that layers on top of any runtime install. + +--- + +## Install the minimal profile + +To install only the core main-loop skills, add `--minimal` to the installer: + +```bash +npx @opengsd/gsd-core@latest --claude --global --minimal +``` + +`--minimal` has two aliases — use whichever reads best to you; they are identical: + +```bash +npx @opengsd/gsd-core@latest --claude --global --core-only +npx @opengsd/gsd-core@latest --claude --global --profile=core +``` + +A minimal install gives you the eight skills needed to run the core phase loop: + +- `new-project` +- `discuss-phase` +- `plan-phase` +- `execute-phase` +- `phase` +- `help` +- `update` +- `surface` + +No sub-agents are installed, and the skill-description tokens the model carries at cold start drop to roughly 130, against roughly 1,200 for a full install. The chosen profile is recorded in the `.gsd-profile` marker in your runtime config directory and is reapplied automatically every time you run `/gsd-update`, so you stay minimal across upgrades until you decide otherwise. + +> Do not combine `--minimal` with `--profile=` — the installer treats that as a conflict and exits. + +--- + +## Choose a profile + +If `core` is too small, pick a wider profile instead. Pass it with `--profile=`: + +| Profile | What you get | Approx. description tokens | +|---------|--------------|--------------------------| +| `core` | The eight core-loop skills above. No agents. | ~130 desc tokens | +| `standard` | Everything in `core` plus common management skills — `review`, `config`, `progress`, `resume-work`, `pause-work`, `workspace` — and the sub-agents those skills need. | ~700 desc tokens | +| `full` | Every skill and every sub-agent. This is the default when you pass no profile flag. | ~1,200 desc tokens | + +```bash +# Standard: the core loop plus everyday management commands +npx @opengsd/gsd-core@latest --claude --global --profile=standard +``` + +If you want a named profile plus one extra cluster, compose them with a comma. The installer writes the union of both: + +```bash +# Core loop plus the audit/review skills, nothing else +npx @opengsd/gsd-core@latest --claude --global --profile=core,audit +``` + +--- + +## See what is installed and what is available + +From inside your runtime, list the current surface, the disabled clusters, and the token cost of each: + +```bash +/gsd:surface list +``` + +The skills are grouped into clusters you can toggle as a unit: + +`core_loop`, `audit_review`, `milestone`, `research_ideate`, `workspace_state`, `docs`, `ui`, `ai_eval`, `ns_meta`, `utility` + +--- + +## Add skills later without reinstalling + +If you installed minimal and now need more, you do not have to re-run the installer. `/gsd:surface` changes the live surface and persists the change in a separate `.gsd-surface.json` file, leaving your install-time profile marker untouched. + +To switch to a wider profile in place: + +```bash +/gsd:surface profile standard +``` + +To turn on just one cluster while keeping your base profile: + +```bash +/gsd:surface enable audit_review +``` + +To turn a cluster back off, or to discard all your live changes and return to the profile you installed: + +```bash +/gsd:surface disable utility +/gsd:surface reset +``` + +Surface changes take effect in your next session — restart the runtime to pick them up. + +--- + +## Add skills by reinstalling + +`/gsd:surface` is the right tool for occasional, reversible adjustments. If you have decided you want the wider surface permanently, change the install-time profile instead so every future `/gsd-update` keeps it: + +```bash +# Re-run the installer without --minimal to record `full` as your profile +npx @opengsd/gsd-core@latest --claude --global + +# ...or pin a specific profile +npx @opengsd/gsd-core@latest --claude --global --profile=standard +``` + +Running `/gsd-update` re-reads the `.gsd-profile` marker and reinstalls at that profile, so a one-off reinstall at a new profile is all you need — subsequent updates follow it automatically. + +--- + +## Related + +- [Install on your runtime](install-on-your-runtime.md) +- [Update GSD](update-gsd.md) +- [Configuration](../CONFIGURATION.md) +- [Docs index](../README.md) diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index 0e2a160e7..b2f53f5d5 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -42,6 +42,66 @@ Skills land in `~/.claude/`. Commands appear as `/gsd-*` slash commands in your CLAUDE_CONFIG_DIR=~/.claude-alt npx @opengsd/gsd-core@latest --claude --global ``` +**Hook coverage** + +GSD registers the following Claude Code hook events automatically on install: + +| Event | Hook | Purpose | +|---|---|---| +| `SessionStart` | `gsd-check-update.js`, `gsd-session-state.sh` | Update check, session orientation | +| `PostToolUse` | `gsd-context-monitor.js`, `gsd-read-injection-scanner.js`, `gsd-phase-boundary.sh`, `gsd-graphify-update.sh` | Context monitoring, read-time scan, phase boundary detection | +| `PreToolUse` | `gsd-prompt-guard.js`, `gsd-read-guard.js`, `gsd-workflow-guard.js`, `gsd-worktree-path-guard.js`, `gsd-validate-commit.sh` | Prompt guard, read-before-edit, workflow + worktree safety, commit validation | +| `SubagentStop` | `gsd-context-monitor.js` | Context headroom tracking after subagent completion | +| `Stop` | `gsd-context-monitor.js` | Context headroom tracking before model stop | +| `PreCompact` | `gsd-context-monitor.js` | Context awareness before conversation compaction | +| `FileChanged` (matcher: `config.json`) | `gsd-config-reload.js` | Hot-reloads `.planning/config.json` context mid-session when you edit your GSD config — no session restart required | + +The `FileChanged` hook is always-on and a no-op when `.planning/config.json` does not exist in the project. Editing that file while a session is running injects an `additionalContext` summary of the new configuration so the agent picks up model overrides, workflow toggles, and hook settings immediately. + +--- + +### Claude Code — native plugin install + +GSD Core ships a `.claude-plugin/plugin.json` manifest, which enables installation and lifecycle management through the Claude Code plugin system. This path is **additive** — the npm installer above remains fully supported, and the two approaches differ in namespace and lifecycle only. + +**Install paths** + +*Option A — marketplace or git install (once listed):* + +```bash +claude plugin install gsd-core +``` + +*Option B — zero-friction skills-dir load:* Claude Code automatically discovers any directory under `~/.claude/skills/` that contains a `.claude-plugin/plugin.json` as a plugin. To use gsd-core this way, place (or symlink) the gsd-core package directory there: + +```bash +# Example: place the package under ~/.claude/skills/gsd-core/ +# Claude Code loads it as gsd-core@skills-dir on the next session start. +# No explicit install step required. +``` + +**Command namespace** + +Plugin commands are namespaced as `/gsd-core:` — for example, `/gsd-core:plan-phase`. This is distinct from the classic npm/file-copy installer, which exposes commands as `/gsd:`. Use whichever namespace corresponds to your install method. + +**Lifecycle** + +```bash +claude plugin enable gsd-core +claude plugin disable gsd-core +claude plugin update gsd-core +``` + +**Hooks** + +The plugin wires gsd-core's always-on guard and update hooks automatically via `hooks/hooks.json`. No manual hook registration is required. + +**Prerequisites** + +The `gsd-tools` binary (installed as part of the `@opengsd/gsd-core` npm package) must be available on your `PATH` for gsd commands to execute their backing logic. The plugin delivers the command, agent, and hook surface; the npm package delivers the runtime CLI. + +Node.js (`node`) must also be available on your `PATH`. The plugin's always-on guard hooks (wired in `hooks/hooks.json`) are invoked as `node "${CLAUDE_PLUGIN_ROOT}/hooks/