@opengsd/gsd-core 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +85 -5
- package/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-code-reviewer.compact.md +5 -3
- package/agents/gsd-code-reviewer.md +8 -6
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +3 -3
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +17 -12
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +2 -2
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +15 -9
- package/agents/gsd-planner.md +15 -11
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +21 -30
- package/agents/gsd-ui-auditor.md +166 -37
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +37 -14
- package/bin/install.js +764 -287
- package/commands/gsd/add-tests.md +6 -1
- package/commands/gsd/ai-integration-phase.md +6 -1
- package/commands/gsd/audit-fix.md +5 -0
- package/commands/gsd/audit-milestone.md +6 -1
- package/commands/gsd/autonomous.md +7 -2
- package/commands/gsd/capture.md +9 -5
- package/commands/gsd/code-review.md +7 -2
- package/commands/gsd/complete-milestone.md +4 -0
- package/commands/gsd/config.md +7 -3
- package/commands/gsd/debug.md +11 -7
- package/commands/gsd/discuss-phase.md +7 -3
- package/commands/gsd/docs-update.md +12 -7
- package/commands/gsd/eval-review.md +6 -1
- package/commands/gsd/execute-phase.md +12 -7
- package/commands/gsd/extract-learnings.md +5 -0
- package/commands/gsd/fast.md +4 -0
- package/commands/gsd/forensics.md +5 -1
- package/commands/gsd/graphify.md +10 -6
- package/commands/gsd/health.md +5 -0
- package/commands/gsd/help.md +7 -2
- package/commands/gsd/import.md +7 -3
- package/commands/gsd/inbox.md +5 -0
- package/commands/gsd/ingest-docs.md +5 -1
- package/commands/gsd/manager.md +6 -1
- package/commands/gsd/map-codebase.md +7 -3
- package/commands/gsd/mempalace-capture.md +12 -4
- package/commands/gsd/mempalace-recall.md +5 -1
- package/commands/gsd/milestone-summary.md +5 -1
- package/commands/gsd/mvp-phase.md +8 -3
- package/commands/gsd/new-milestone.md +6 -1
- package/commands/gsd/new-project.md +5 -0
- package/commands/gsd/next.md +6 -1
- package/commands/gsd/ns-context.md +4 -0
- package/commands/gsd/ns-ideate.md +4 -0
- package/commands/gsd/ns-manage.md +4 -0
- package/commands/gsd/ns-project.md +4 -0
- package/commands/gsd/ns-review.md +4 -0
- package/commands/gsd/ns-workflow.md +4 -0
- package/commands/gsd/onboard.md +6 -1
- package/commands/gsd/pause-work.md +5 -1
- package/commands/gsd/phase.md +8 -4
- package/commands/gsd/plan-phase.md +6 -1
- package/commands/gsd/plan-review-convergence.md +11 -7
- package/commands/gsd/pr-branch.md +4 -0
- package/commands/gsd/profile-user.md +5 -1
- package/commands/gsd/progress.md +7 -2
- package/commands/gsd/quick-batch.md +21 -9
- package/commands/gsd/quick.md +12 -7
- package/commands/gsd/review.md +7 -4
- package/commands/gsd/secure-phase.md +6 -1
- package/commands/gsd/ship.md +5 -0
- package/commands/gsd/sketch.md +7 -2
- package/commands/gsd/spec-phase.md +5 -1
- package/commands/gsd/spike.md +8 -3
- package/commands/gsd/surface.md +5 -1
- package/commands/gsd/thread.md +4 -0
- package/commands/gsd/ui-phase.md +6 -1
- package/commands/gsd/ui-review.md +6 -1
- package/commands/gsd/ultraplan-phase.md +5 -1
- package/commands/gsd/undo.md +5 -1
- package/commands/gsd/update.md +6 -2
- package/commands/gsd/validate-phase.md +6 -1
- package/commands/gsd/verify-work.md +6 -1
- package/commands/gsd/workspace.md +7 -3
- package/gsd-core/bin/gsd-tools.cjs +477 -78
- package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
- package/gsd-core/bin/lib/audit.cjs +144 -42
- package/gsd-core/bin/lib/broken-windows.cjs +13 -13
- package/gsd-core/bin/lib/capability-activation.cjs +9 -4
- package/gsd-core/bin/lib/capability-registry.cjs +197 -222
- package/gsd-core/bin/lib/capability-validator.cjs +16 -1
- package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
- package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
- package/gsd-core/bin/lib/cli-exit.cjs +12 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
- package/gsd-core/bin/lib/command-aliases.cjs +7 -0
- package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
- package/gsd-core/bin/lib/commands.cjs +343 -207
- package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +76 -19
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/coverage.cjs +4 -8
- package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/drift.cjs +177 -42
- package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
- package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
- package/gsd-core/bin/lib/frontmatter.cjs +426 -234
- package/gsd-core/bin/lib/gap-checker.cjs +72 -29
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
- package/gsd-core/bin/lib/gate-args.cjs +53 -0
- package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
- package/gsd-core/bin/lib/gate-config.cjs +46 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
- package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
- package/gsd-core/bin/lib/gate-exit.cjs +27 -0
- package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
- package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
- package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
- package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
- package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
- package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
- package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
- package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
- package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
- package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
- package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
- package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +364 -132
- package/gsd-core/bin/lib/install-engine.cjs +30 -33
- package/gsd-core/bin/lib/install-profiles.cjs +7 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/io.cjs +122 -3
- package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
- package/gsd-core/bin/lib/milestone.cjs +37 -6
- package/gsd-core/bin/lib/model-resolver.cjs +171 -62
- package/gsd-core/bin/lib/observability/event.cjs +1 -1
- package/gsd-core/bin/lib/observability/logger.cjs +46 -1
- package/gsd-core/bin/lib/pattern.cjs +10 -0
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
- package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +110 -8
- package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase-status.cjs +360 -0
- package/gsd-core/bin/lib/phase.cjs +489 -88
- package/gsd-core/bin/lib/plan-document.cjs +142 -20
- package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
- package/gsd-core/bin/lib/planning-document.cjs +692 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
- package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/profile-output.cjs +6 -3
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
- package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
- package/gsd-core/bin/lib/report-parser.cjs +269 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
- package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
- package/gsd-core/bin/lib/roadmap.cjs +405 -88
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
- package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
- package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -3
- package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
- package/gsd-core/bin/lib/state-contract.cjs +15 -18
- package/gsd-core/bin/lib/state-document.cjs +100 -22
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +256 -105
- package/gsd-core/bin/lib/surface.cjs +19 -2
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
- package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
- package/gsd-core/bin/lib/uat.cjs +432 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/vendor/README.md +31 -9
- package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
- package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
- package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
- package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +1378 -274
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
- package/gsd-core/bin/lib/verify.cjs +357 -531
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
- package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
- package/gsd-core/bin/lib/workstream.cjs +11 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/references/autonomous-smart-discuss.md +2 -1
- package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-mvp-tdd.md +5 -10
- package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +7 -1
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/offer-next.md +1 -1
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +12 -3
- package/gsd-core/references/spidr-splitting.md +1 -1
- package/gsd-core/references/tdd.md +37 -8
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verifier-phase-gates.md +5 -2
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -3
- package/gsd-core/references/worktree-path-safety.md +321 -0
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/UAT.md +17 -1
- package/gsd-core/templates/config.json +2 -11
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +8 -7
- package/gsd-core/workflows/add-tests.md +4 -3
- package/gsd-core/workflows/add-todo.md +6 -5
- package/gsd-core/workflows/ai-integration-phase.md +13 -4
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +4 -3
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +43 -23
- package/gsd-core/workflows/check-todos.md +7 -6
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
- package/gsd-core/workflows/code-review-fix.md +112 -25
- package/gsd-core/workflows/code-review.md +146 -135
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
- package/gsd-core/workflows/complete-milestone.md +13 -8
- package/gsd-core/workflows/debug.md +32 -7
- package/gsd-core/workflows/diagnose-issues.md +3 -2
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
- package/gsd-core/workflows/discuss-phase.md +4 -3
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +4 -3
- package/gsd-core/workflows/eval-review.md +14 -5
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +96 -178
- package/gsd-core/workflows/execute-plan.md +18 -23
- package/gsd-core/workflows/explore.md +4 -4
- package/gsd-core/workflows/extract-learnings.md +4 -2
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +3 -2
- package/gsd-core/workflows/help/modes/full.compact.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/import.md +4 -3
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +4 -3
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +6 -4
- package/gsd-core/workflows/map-codebase.md +5 -4
- package/gsd-core/workflows/milestone-summary.md +3 -2
- package/gsd-core/workflows/mvp-phase.md +14 -14
- package/gsd-core/workflows/new-milestone.md +11 -11
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +7 -7
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +2 -2
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +50 -24
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +113 -13
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +19 -49
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +15 -10
- package/gsd-core/workflows/quick.md +62 -39
- package/gsd-core/workflows/reapply-patches.md +9 -3
- package/gsd-core/workflows/remove-phase.md +3 -2
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +3 -2
- package/gsd-core/workflows/review.md +33 -17
- package/gsd-core/workflows/scan.md +3 -2
- package/gsd-core/workflows/secure-phase.md +13 -13
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +4 -4
- package/gsd-core/workflows/ship.md +14 -13
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +2 -2
- package/gsd-core/workflows/transition.md +13 -23
- package/gsd-core/workflows/ui-phase.md +48 -11
- package/gsd-core/workflows/ui-review.md +21 -6
- package/gsd-core/workflows/ultraplan-phase.md +3 -2
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +7 -7
- package/gsd-core/workflows/validate-phase.md +12 -13
- package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
- package/gsd-core/workflows/verify-work.md +129 -55
- package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
- package/hooks/dist/gsd-check-update-worker.js +8 -0
- package/hooks/dist/gsd-check-update.js +8 -0
- package/hooks/dist/gsd-context-monitor.js +31 -8
- package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
- package/hooks/dist/gsd-secret-read-guard.js +161 -4
- package/hooks/dist/gsd-statusline.js +85 -20
- package/hooks/dist/gsd-update-banner.js +8 -0
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
- package/hooks/dist/gsd-workflow-guard.js +5 -4
- package/hooks/dist/gsd-worktree-path-guard.js +6 -2
- package/hooks/dist/lib/cli-exit.js +12 -0
- package/hooks/dist/lib/git-probe.js +17 -1
- package/hooks/dist/lib/isolation-sentinel.js +2 -2
- package/hooks/gsd-agent-isolation-guard.js +32 -0
- package/hooks/gsd-check-update-worker.js +8 -0
- package/hooks/gsd-check-update.js +8 -0
- package/hooks/gsd-context-monitor.js +31 -8
- package/hooks/gsd-cursor-subagent-start.js +8 -0
- package/hooks/gsd-secret-read-guard.js +161 -4
- package/hooks/gsd-statusline.js +85 -20
- package/hooks/gsd-update-banner.js +8 -0
- package/hooks/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-windsurf-pre-write.js +11 -2
- package/hooks/gsd-workflow-guard.js +5 -4
- package/hooks/gsd-worktree-path-guard.js +6 -2
- package/hooks/hooks.json +5 -5
- package/hooks/lib/cli-exit.js +12 -0
- package/hooks/lib/git-probe.js +17 -1
- package/hooks/lib/isolation-sentinel.js +2 -2
- package/package.json +22 -4
- package/scripts/build-hooks.js +15 -6
- package/scripts/changeset/parse.cjs +52 -4
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/ci-timeout-report.cjs +770 -4
- package/scripts/command-contract-helpers.cjs +15 -8
- package/scripts/docs-guard-registry.cjs +34 -0
- package/scripts/gen-features.cjs +13 -8
- package/scripts/gen-hooks-cli-exit.cjs +12 -28
- package/scripts/gen-loop-host-contract.cjs +79 -1
- package/scripts/gen-platform-conformance-tier.cjs +187 -1
- package/scripts/gen-plugin-skills.cjs +87 -1
- package/scripts/gen-research-agents.cjs +24 -31
- package/scripts/gen-scripts-cli-exit.cjs +30 -3
- package/scripts/gen-test-timings.cjs +32 -7
- package/scripts/lib/cli-exit.cjs +12 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
- package/scripts/lib/ndjson-reporter.cjs +31 -5
- package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
- package/scripts/lib/registration-ledger-preload.cjs +155 -0
- package/scripts/lib/vendor-bundle.cjs +59 -0
- package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-completion-predicate-drift.cjs +18 -19
- package/scripts/lint-descriptions.cjs +7 -3
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
- package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
- package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-enumeration-drift.cjs +12 -8
- package/scripts/lint-phase-id-drift.cjs +319 -5
- package/scripts/lint-planning-document-positive-control.cjs +329 -0
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-response-language-coverage.cjs +3 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-skill-deps.cjs +7 -3
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +38 -9
- package/scripts/lint-test-file-count.cjs +34 -1
- package/scripts/lint-vendored-deps.cjs +41 -5
- package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
- package/scripts/mutation-matrix.cjs +50 -5
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/scripts/require-issue-link-policy.cjs +6 -2
- package/scripts/sync-runtime-launcher.cjs +184 -2
- package/scripts/verify-npm-publish.cjs +76 -20
- package/skills/gsd-add-tests/SKILL.md +6 -1
- package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
- package/skills/gsd-audit-fix/SKILL.md +5 -0
- package/skills/gsd-audit-milestone/SKILL.md +6 -1
- package/skills/gsd-autonomous/SKILL.md +7 -2
- package/skills/gsd-capture/SKILL.md +9 -5
- package/skills/gsd-code-review/SKILL.md +7 -2
- package/skills/gsd-complete-milestone/SKILL.md +4 -0
- package/skills/gsd-config/SKILL.md +8 -4
- package/skills/gsd-debug/SKILL.md +11 -7
- package/skills/gsd-discuss-phase/SKILL.md +7 -3
- package/skills/gsd-docs-update/SKILL.md +12 -7
- package/skills/gsd-eval-review/SKILL.md +6 -1
- package/skills/gsd-execute-phase/SKILL.md +12 -7
- package/skills/gsd-extract-learnings/SKILL.md +5 -0
- package/skills/gsd-fast/SKILL.md +4 -0
- package/skills/gsd-forensics/SKILL.md +5 -1
- package/skills/gsd-graphify/SKILL.md +10 -6
- package/skills/gsd-health/SKILL.md +5 -0
- package/skills/gsd-help/SKILL.md +7 -2
- package/skills/gsd-import/SKILL.md +7 -3
- package/skills/gsd-inbox/SKILL.md +5 -0
- package/skills/gsd-ingest-docs/SKILL.md +5 -1
- package/skills/gsd-manager/SKILL.md +6 -1
- package/skills/gsd-map-codebase/SKILL.md +7 -3
- package/skills/gsd-mempalace-capture/SKILL.md +12 -4
- package/skills/gsd-mempalace-recall/SKILL.md +5 -1
- package/skills/gsd-milestone-summary/SKILL.md +5 -1
- package/skills/gsd-mvp-phase/SKILL.md +8 -3
- package/skills/gsd-new-milestone/SKILL.md +6 -1
- package/skills/gsd-new-project/SKILL.md +5 -0
- package/skills/gsd-next/SKILL.md +6 -1
- package/skills/gsd-ns-context/SKILL.md +4 -0
- package/skills/gsd-ns-ideate/SKILL.md +4 -0
- package/skills/gsd-ns-manage/SKILL.md +4 -0
- package/skills/gsd-ns-project/SKILL.md +4 -0
- package/skills/gsd-ns-review/SKILL.md +4 -0
- package/skills/gsd-ns-workflow/SKILL.md +4 -0
- package/skills/gsd-onboard/SKILL.md +6 -1
- package/skills/gsd-pause-work/SKILL.md +5 -1
- package/skills/gsd-phase/SKILL.md +8 -4
- package/skills/gsd-plan-phase/SKILL.md +6 -1
- package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
- package/skills/gsd-pr-branch/SKILL.md +4 -0
- package/skills/gsd-profile-user/SKILL.md +5 -1
- package/skills/gsd-progress/SKILL.md +7 -2
- package/skills/gsd-quick/SKILL.md +16 -10
- package/skills/gsd-quick-batch/SKILL.md +21 -9
- package/skills/gsd-review/SKILL.md +7 -4
- package/skills/gsd-review-backlog/SKILL.md +3 -2
- package/skills/gsd-secure-phase/SKILL.md +6 -1
- package/skills/gsd-ship/SKILL.md +5 -0
- package/skills/gsd-sketch/SKILL.md +7 -2
- package/skills/gsd-spec-phase/SKILL.md +5 -1
- package/skills/gsd-spike/SKILL.md +8 -3
- package/skills/gsd-surface/SKILL.md +5 -1
- package/skills/gsd-thread/SKILL.md +4 -0
- package/skills/gsd-ui-phase/SKILL.md +6 -1
- package/skills/gsd-ui-review/SKILL.md +6 -1
- package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
- package/skills/gsd-undo/SKILL.md +5 -1
- package/skills/gsd-update/SKILL.md +6 -2
- package/skills/gsd-validate-phase/SKILL.md +6 -1
- package/skills/gsd-verify-work/SKILL.md +6 -1
- package/skills/gsd-workspace/SKILL.md +7 -3
- package/skills/gsd-workstreams/SKILL.md +6 -6
- package/vscode/package.json +1 -1
|
@@ -38,7 +38,7 @@ const commonjs_marker_cjs_1 = require("./commonjs-marker.cjs");
|
|
|
38
38
|
const imperative_hook_bus_cjs_1 = require("./host-integration-adapters/imperative-hook-bus.cjs");
|
|
39
39
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
40
|
const shellCmdProjection = require("./shell-command-projection.cjs");
|
|
41
|
-
const { isManagedHookBasename, isManagedHookCommand, projectLegacySettingsHookCommand, projectManagedHookCommand, projectPortableHookBaseDir, projectCodexHookTomlCommand, shellHookOmitsBashRunner, escapeTomlDoubleQuotedString, escapePosixDoubleQuoted, } = shellCmdProjection;
|
|
41
|
+
const { isManagedHookBasename, isManagedHookCommand, projectLegacySettingsHookCommand, projectManagedHookCommand, projectPortableHookBaseDir, projectCodexHookTomlCommand, shellHookOmitsBashRunner, escapeTomlDoubleQuotedString, escapePosixDoubleQuoted, resolveExecutableBinary, } = shellCmdProjection;
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
43
43
|
// Terminal color constants (mirrors install.js for console output parity)
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
@@ -576,7 +576,15 @@ function buildBakedNodeToken(opts) {
|
|
|
576
576
|
* guarantee), ahead of `command -v node` and the well-known fallback list.
|
|
577
577
|
*/
|
|
578
578
|
const NODE_RUNNER_RESOLVER_HOOK = 'gsd-node-runner.sh';
|
|
579
|
-
|
|
579
|
+
/**
|
|
580
|
+
* The one Git Bash policy: `bash` off win32; on win32 GSD_BASH_PATH, then the
|
|
581
|
+
* well-known Git for Windows install locations, never a PATH lookup (which
|
|
582
|
+
* can reach WSL's System32 launcher). null when win32 has no Git Bash.
|
|
583
|
+
* #5082: the test process seam (tests/helpers/process-seam.cjs) resolves
|
|
584
|
+
* `bash` through this too, so hooks and bash-driven tests run under the same
|
|
585
|
+
* bash.
|
|
586
|
+
*/
|
|
587
|
+
function resolveBashExecutable(opts) {
|
|
580
588
|
const platform = (opts && opts.platform) || process.platform;
|
|
581
589
|
if (platform !== 'win32')
|
|
582
590
|
return 'bash';
|
|
@@ -594,12 +602,19 @@ function resolveBashRunner(opts) {
|
|
|
594
602
|
candidates.push(node_path_1.default.win32.join(env.SystemDrive, 'Program Files (x86)', 'Git', 'bin', 'bash.exe'));
|
|
595
603
|
}
|
|
596
604
|
for (const candidate of candidates) {
|
|
597
|
-
if (candidate && exists(candidate))
|
|
598
|
-
return
|
|
599
|
-
}
|
|
605
|
+
if (candidate && exists(candidate))
|
|
606
|
+
return shellCmdProjection.posixNormalize(candidate);
|
|
600
607
|
}
|
|
601
608
|
return null;
|
|
602
609
|
}
|
|
610
|
+
function resolveBashRunner(opts) {
|
|
611
|
+
const executable = resolveBashExecutable(opts);
|
|
612
|
+
if (executable === null)
|
|
613
|
+
return null;
|
|
614
|
+
return ((opts && opts.platform) || process.platform) === 'win32'
|
|
615
|
+
? JSON.stringify(executable)
|
|
616
|
+
: executable;
|
|
617
|
+
}
|
|
603
618
|
// #3662 — recognize the two runtime-resolving command shapes the installer
|
|
604
619
|
// emits, so the rewriter never churns (or un-does) an entry that already
|
|
605
620
|
// works in every environment sharing the config root.
|
|
@@ -961,16 +976,18 @@ function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
|
|
|
961
976
|
function reconcileCodexHooksJsonSessionStart(targetDir, opts = {}) {
|
|
962
977
|
return reconcileCodexHooksJsonEvent(targetDir, 'SessionStart', opts);
|
|
963
978
|
}
|
|
964
|
-
function
|
|
965
|
-
if (!absoluteRunnerToken)
|
|
966
|
-
return null;
|
|
967
|
-
let interpreter;
|
|
979
|
+
function parseAbsoluteRunnerToken(absoluteRunnerToken) {
|
|
968
980
|
try {
|
|
969
|
-
|
|
981
|
+
return JSON.parse(absoluteRunnerToken);
|
|
970
982
|
}
|
|
971
983
|
catch {
|
|
972
|
-
|
|
984
|
+
return absoluteRunnerToken;
|
|
973
985
|
}
|
|
986
|
+
}
|
|
987
|
+
function buildCodexHookWindowsShimIR(scriptAbsPath, absoluteRunnerToken) {
|
|
988
|
+
if (!absoluteRunnerToken)
|
|
989
|
+
return null;
|
|
990
|
+
const interpreter = parseAbsoluteRunnerToken(absoluteRunnerToken);
|
|
974
991
|
const targetAbs = shellCmdProjection.posixNormalize(scriptAbsPath);
|
|
975
992
|
const scriptQuoted = JSON.stringify(targetAbs);
|
|
976
993
|
const cmdPath = scriptAbsPath.replace(/\.js$/, '.cmd');
|
|
@@ -995,6 +1012,7 @@ function ensureCodexHooksJsonSessionStart(targetDir, opts = {}) {
|
|
|
995
1012
|
return { changed: false, wrote: false, path: hooksJsonPath };
|
|
996
1013
|
const scriptPath = shellCmdProjection.posixNormalize(node_path_1.default.resolve(targetDir, 'hooks', 'gsd-check-update.js'));
|
|
997
1014
|
const cmdShimPath = scriptPath.replace(/\.js$/, '.cmd');
|
|
1015
|
+
const configuredEntrypoints = [];
|
|
998
1016
|
let managedCommand;
|
|
999
1017
|
if (platform === 'win32') {
|
|
1000
1018
|
const shimIR = buildCodexHookWindowsShimIR(scriptPath, absoluteRunner);
|
|
@@ -1011,6 +1029,7 @@ function ensureCodexHooksJsonSessionStart(targetDir, opts = {}) {
|
|
|
1011
1029
|
return { changed: false, wrote: false, path: hooksJsonPath };
|
|
1012
1030
|
}
|
|
1013
1031
|
managedCommand = shimIR.hookCommand;
|
|
1032
|
+
configuredEntrypoints.push({ runtime: 'codex', configPath: hooksJsonPath, scriptPath: shimIR.cmdPath, platform, selfExecutable: true }, { runtime: 'codex', configPath: hooksJsonPath, scriptPath, interpreterCandidates: [parseAbsoluteRunnerToken(absoluteRunner)], platform });
|
|
1014
1033
|
}
|
|
1015
1034
|
else {
|
|
1016
1035
|
managedCommand = projectManagedHookCommand({
|
|
@@ -1019,13 +1038,23 @@ function ensureCodexHooksJsonSessionStart(targetDir, opts = {}) {
|
|
|
1019
1038
|
runtime: 'codex',
|
|
1020
1039
|
platform,
|
|
1021
1040
|
}) ?? undefined;
|
|
1041
|
+
if (managedCommand) {
|
|
1042
|
+
configuredEntrypoints.push({
|
|
1043
|
+
runtime: 'codex',
|
|
1044
|
+
configPath: hooksJsonPath,
|
|
1045
|
+
scriptPath,
|
|
1046
|
+
interpreterCandidates: [parseAbsoluteRunnerToken(absoluteRunner)],
|
|
1047
|
+
platform,
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1022
1050
|
}
|
|
1023
1051
|
if (!managedCommand)
|
|
1024
1052
|
return { changed: false, wrote: false, path: hooksJsonPath };
|
|
1025
1053
|
const commandWindows = platform === 'win32'
|
|
1026
1054
|
? JSON.stringify(shellCmdProjection.posixNormalize(cmdShimPath))
|
|
1027
1055
|
: undefined;
|
|
1028
|
-
|
|
1056
|
+
const result = reconcileCodexHooksJsonSessionStart(targetDir, { managedCommand, commandWindows });
|
|
1057
|
+
return { ...result, configuredEntrypoints };
|
|
1029
1058
|
}
|
|
1030
1059
|
function ensureCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
|
|
1031
1060
|
const platform = opts.platform || process.platform;
|
|
@@ -1201,6 +1230,56 @@ function cleanupOrphanedCodexContextMonitorScript(targetDir) {
|
|
|
1201
1230
|
}
|
|
1202
1231
|
return result;
|
|
1203
1232
|
}
|
|
1233
|
+
function configuredEntrypointsForHook(configDir, hookName, opts) {
|
|
1234
|
+
const platform = opts.platform || process.platform;
|
|
1235
|
+
const runtime = opts.runtime || 'generic';
|
|
1236
|
+
const configPath = opts.configPath || configDir;
|
|
1237
|
+
const target = {
|
|
1238
|
+
runtime,
|
|
1239
|
+
configPath,
|
|
1240
|
+
scriptPath: node_path_1.default.join(configDir, 'hooks', hookName),
|
|
1241
|
+
platform,
|
|
1242
|
+
};
|
|
1243
|
+
const isShellHook = hookName.endsWith('.sh');
|
|
1244
|
+
if (shellHookOmitsBashRunner({ platform, runtime, isShellHook }))
|
|
1245
|
+
return [{ ...target, selfExecutable: true }];
|
|
1246
|
+
const bash = resolveBashExecutable(opts);
|
|
1247
|
+
if (isShellHook) {
|
|
1248
|
+
// An unresolved bash must still surface as an interpreterCandidates entry
|
|
1249
|
+
// (the literal token, same as the portableHooks runner below) so
|
|
1250
|
+
// validateConfiguredEntrypoints reports 'unresolved-interpreter' instead
|
|
1251
|
+
// of silently skipping the check because the field is absent.
|
|
1252
|
+
return [{ ...target, interpreterCandidates: [bash === null ? 'bash' : bash] }];
|
|
1253
|
+
}
|
|
1254
|
+
// #4249: check the SAME stable alias buildNodeRunnerChainToken bakes as its
|
|
1255
|
+
// first candidate (normalizeNodePath rewrites a version-manager shim like
|
|
1256
|
+
// fnm/nvm/mise/volta into its persistent path), not the raw, currently-
|
|
1257
|
+
// running process.execPath — which always trivially resolves regardless of
|
|
1258
|
+
// whether the alias actually baked into the persisted command still does.
|
|
1259
|
+
const nodeCandidates = [
|
|
1260
|
+
normalizeNodePath(opts.execPath || process.execPath, opts),
|
|
1261
|
+
'node',
|
|
1262
|
+
'/usr/local/bin/node',
|
|
1263
|
+
'/usr/bin/node',
|
|
1264
|
+
].filter((candidate) => Boolean(candidate));
|
|
1265
|
+
if (!opts.portableHooks) {
|
|
1266
|
+
return [{ ...target, interpreterCandidates: nodeCandidates }];
|
|
1267
|
+
}
|
|
1268
|
+
const runner = {
|
|
1269
|
+
runtime,
|
|
1270
|
+
configPath,
|
|
1271
|
+
scriptPath: node_path_1.default.join(configDir, 'hooks', NODE_RUNNER_RESOLVER_HOOK),
|
|
1272
|
+
interpreterCandidates: bash === null ? ['bash'] : [bash],
|
|
1273
|
+
platform,
|
|
1274
|
+
};
|
|
1275
|
+
return [runner, { ...target, interpreterCandidates: nodeCandidates }];
|
|
1276
|
+
}
|
|
1277
|
+
function recordConfiguredHookCommand(command, configDir, hookName, opts) {
|
|
1278
|
+
if (command && opts.configuredEntrypoints) {
|
|
1279
|
+
opts.configuredEntrypoints.push(...configuredEntrypointsForHook(configDir, hookName, opts).map(entry => ({ ...entry, command })));
|
|
1280
|
+
}
|
|
1281
|
+
return command;
|
|
1282
|
+
}
|
|
1204
1283
|
function buildHookCommand(configDir, hookName, opts) {
|
|
1205
1284
|
if (!opts)
|
|
1206
1285
|
opts = {};
|
|
@@ -1208,44 +1287,56 @@ function buildHookCommand(configDir, hookName, opts) {
|
|
|
1208
1287
|
const runtime = opts.runtime || 'generic';
|
|
1209
1288
|
const hookShell = opts.hookShell;
|
|
1210
1289
|
const isShellHook = hookName.endsWith('.sh');
|
|
1290
|
+
const track = (command) => recordConfiguredHookCommand(command, configDir, hookName, opts);
|
|
1211
1291
|
if (shellHookOmitsBashRunner({ platform, runtime, isShellHook })) {
|
|
1212
1292
|
if (opts.portableHooks) {
|
|
1213
1293
|
const portableBaseDir = projectPortableHookBaseDir({
|
|
1214
1294
|
configDir,
|
|
1215
1295
|
homeDir: node_os_1.default.homedir(),
|
|
1216
1296
|
});
|
|
1217
|
-
return JSON.stringify(`${portableBaseDir}/hooks/${hookName}`);
|
|
1297
|
+
return track(JSON.stringify(`${portableBaseDir}/hooks/${hookName}`));
|
|
1218
1298
|
}
|
|
1219
|
-
return JSON.stringify(shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName);
|
|
1299
|
+
return track(JSON.stringify(shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName));
|
|
1220
1300
|
}
|
|
1221
1301
|
// .sh hooks keep the pre-#3662 shape everywhere: the bash runner resolves
|
|
1222
1302
|
// at install time like today, and `bash` itself is a PATH-stable binary
|
|
1223
1303
|
// (the absolute Git-Bash discovery covers win32 — #580/#3393).
|
|
1224
1304
|
if (isShellHook) {
|
|
1225
1305
|
const runner = resolveBashRunner(opts);
|
|
1226
|
-
if (runner === null)
|
|
1306
|
+
if (runner === null) {
|
|
1307
|
+
// #4249 (antigravity review): this early return skips `track()` below,
|
|
1308
|
+
// so an unresolved bash on win32 (no Git Bash found) previously left
|
|
1309
|
+
// this hook silently unregistered with nothing for
|
|
1310
|
+
// validateConfiguredEntrypoints to reject — configuredEntrypointsForHook's
|
|
1311
|
+
// own 'unresolved bash must still surface' comment describes intent this
|
|
1312
|
+
// return never reached. Push the entry directly (no `command`, since
|
|
1313
|
+
// none was ever built) so the gate actually sees it.
|
|
1314
|
+
if (opts.configuredEntrypoints) {
|
|
1315
|
+
opts.configuredEntrypoints.push(...configuredEntrypointsForHook(configDir, hookName, opts));
|
|
1316
|
+
}
|
|
1227
1317
|
return null;
|
|
1318
|
+
}
|
|
1228
1319
|
if (opts.portableHooks) {
|
|
1229
1320
|
const portableBaseDir = projectPortableHookBaseDir({
|
|
1230
1321
|
configDir,
|
|
1231
1322
|
homeDir: node_os_1.default.homedir(),
|
|
1232
1323
|
});
|
|
1233
|
-
return projectManagedHookCommand({
|
|
1324
|
+
return track(projectManagedHookCommand({
|
|
1234
1325
|
absoluteRunner: runner,
|
|
1235
1326
|
scriptPath: `${portableBaseDir}/hooks/${hookName}`,
|
|
1236
1327
|
runtime: opts.runtime || 'generic',
|
|
1237
1328
|
platform,
|
|
1238
1329
|
hookShell,
|
|
1239
|
-
});
|
|
1330
|
+
}));
|
|
1240
1331
|
}
|
|
1241
1332
|
const hooksPath = shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName;
|
|
1242
|
-
return projectManagedHookCommand({
|
|
1333
|
+
return track(projectManagedHookCommand({
|
|
1243
1334
|
absoluteRunner: runner,
|
|
1244
1335
|
scriptPath: hooksPath,
|
|
1245
1336
|
runtime,
|
|
1246
1337
|
platform,
|
|
1247
1338
|
hookShell,
|
|
1248
|
-
});
|
|
1339
|
+
}));
|
|
1249
1340
|
}
|
|
1250
1341
|
// JS hooks (#3662): the node runner is resolved at hook-fire time, never
|
|
1251
1342
|
// baked as a bare absolute path — an install-environment absolute path is
|
|
@@ -1268,7 +1359,7 @@ function buildHookCommand(configDir, hookName, opts) {
|
|
|
1268
1359
|
// Absolute Git-Bash discovery on win32 when available (#580); `bash` on
|
|
1269
1360
|
// PATH otherwise — the same assumption .sh hooks already make.
|
|
1270
1361
|
const resolverRunner = resolveBashRunner(opts) || 'bash';
|
|
1271
|
-
return shellCmdProjection.projectShellCommandText({
|
|
1362
|
+
return track(shellCmdProjection.projectShellCommandText({
|
|
1272
1363
|
runnerToken: resolverRunner,
|
|
1273
1364
|
argTokens: [
|
|
1274
1365
|
JSON.stringify(`${portableBaseDir}/hooks/${NODE_RUNNER_RESOLVER_HOOK}`),
|
|
@@ -1278,19 +1369,19 @@ function buildHookCommand(configDir, hookName, opts) {
|
|
|
1278
1369
|
runtime,
|
|
1279
1370
|
platform,
|
|
1280
1371
|
hookShell,
|
|
1281
|
-
});
|
|
1372
|
+
}));
|
|
1282
1373
|
}
|
|
1283
1374
|
const chainRunner = buildNodeRunnerChainToken(opts);
|
|
1284
1375
|
if (chainRunner === null)
|
|
1285
1376
|
return null;
|
|
1286
1377
|
const hooksPath = shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName;
|
|
1287
|
-
return shellCmdProjection.projectShellCommandText({
|
|
1378
|
+
return track(shellCmdProjection.projectShellCommandText({
|
|
1288
1379
|
runnerToken: chainRunner,
|
|
1289
1380
|
argTokens: [JSON.stringify(hooksPath)],
|
|
1290
1381
|
runtime,
|
|
1291
1382
|
platform,
|
|
1292
1383
|
hookShell,
|
|
1293
|
-
});
|
|
1384
|
+
}));
|
|
1294
1385
|
}
|
|
1295
1386
|
// ---------------------------------------------------------------------------
|
|
1296
1387
|
// Cline helpers
|
|
@@ -1391,6 +1482,7 @@ function mergeGsdAgentsMd(filePath, gsdContent) {
|
|
|
1391
1482
|
// ---------------------------------------------------------------------------
|
|
1392
1483
|
function writeClineArtifacts(targetDir, isGlobalInstall) {
|
|
1393
1484
|
const written = [];
|
|
1485
|
+
const configuredEntrypoints = [];
|
|
1394
1486
|
const clinerulesDir = node_path_1.default.join(targetDir, '.clinerules');
|
|
1395
1487
|
try {
|
|
1396
1488
|
if (node_fs_1.default.existsSync(clinerulesDir)) {
|
|
@@ -1416,6 +1508,20 @@ function writeClineArtifacts(targetDir, isGlobalInstall) {
|
|
|
1416
1508
|
catch { /* Windows: hooks unsupported anyway */ }
|
|
1417
1509
|
written.push('.clinerules/hooks/PreToolUse');
|
|
1418
1510
|
console.log(` ${green}✓${reset} Wrote .clinerules/hooks/PreToolUse`);
|
|
1511
|
+
// #4249 (CodeRabbit): Cline invokes this file directly via its own
|
|
1512
|
+
// `#!/usr/bin/env node` shebang — a hybrid case. The script itself still
|
|
1513
|
+
// needs the execute bit (selfExecutable), but unlike GSD's other JS hooks
|
|
1514
|
+
// (which bake an absolute, install-time-resolved node path specifically to
|
|
1515
|
+
// avoid this) its interpreter is looked up on PATH by `env` at hook-fire
|
|
1516
|
+
// time, so `node` must also resolve or the hook can never run.
|
|
1517
|
+
configuredEntrypoints.push({
|
|
1518
|
+
runtime: 'cline',
|
|
1519
|
+
configPath: hookPath,
|
|
1520
|
+
scriptPath: hookPath,
|
|
1521
|
+
interpreterCandidates: ['node'],
|
|
1522
|
+
selfExecutable: true,
|
|
1523
|
+
platform: process.platform,
|
|
1524
|
+
});
|
|
1419
1525
|
if (isGlobalInstall) {
|
|
1420
1526
|
try {
|
|
1421
1527
|
const agentsPath = node_path_1.default.join(node_os_1.default.homedir(), '.agents', 'AGENTS.md');
|
|
@@ -1426,7 +1532,7 @@ function writeClineArtifacts(targetDir, isGlobalInstall) {
|
|
|
1426
1532
|
console.warn(` ${yellow}⚠${reset} Could not write ~/.agents/AGENTS.md: ${err.message}`);
|
|
1427
1533
|
}
|
|
1428
1534
|
}
|
|
1429
|
-
return written;
|
|
1535
|
+
return { written, configuredEntrypoints };
|
|
1430
1536
|
}
|
|
1431
1537
|
// ---------------------------------------------------------------------------
|
|
1432
1538
|
// Cursor hook functions
|
|
@@ -1677,7 +1783,13 @@ function writeCursorHooksJson(targetDir, src, opts) {
|
|
|
1677
1783
|
if (installedScripts.size > 0) {
|
|
1678
1784
|
ensureCommonJsMarker(hooksDir);
|
|
1679
1785
|
}
|
|
1680
|
-
const
|
|
1786
|
+
const configuredEntrypoints = [];
|
|
1787
|
+
const hookOpts = {
|
|
1788
|
+
runtime: 'cursor',
|
|
1789
|
+
platform: opts.platform || process.platform,
|
|
1790
|
+
configPath: node_path_1.default.join(targetDir, 'hooks.json'),
|
|
1791
|
+
configuredEntrypoints,
|
|
1792
|
+
};
|
|
1681
1793
|
const commands = {};
|
|
1682
1794
|
for (const ev of events) {
|
|
1683
1795
|
const script = imperative_hook_bus_cjs_1.CURSOR_EVENT_SCRIPT_MAP[ev];
|
|
@@ -1691,7 +1803,7 @@ function writeCursorHooksJson(targetDir, src, opts) {
|
|
|
1691
1803
|
const managedEntries = (0, imperative_hook_bus_cjs_1.buildHookBusEntries)(events, commands);
|
|
1692
1804
|
const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
|
|
1693
1805
|
const result = reconcileCursorHooksJson(hooksJsonPath, managedEntries);
|
|
1694
|
-
return { hooksJsonPath, changed: result.changed };
|
|
1806
|
+
return { hooksJsonPath, changed: result.changed, configuredEntrypoints };
|
|
1695
1807
|
}
|
|
1696
1808
|
function removeCursorHooksJson(targetDir) {
|
|
1697
1809
|
const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
|
|
@@ -1896,7 +2008,13 @@ function writeWindsurfHooksJson(targetDir, src, opts) {
|
|
|
1896
2008
|
if (installedScripts.size > 0) {
|
|
1897
2009
|
ensureCommonJsMarker(hooksDir);
|
|
1898
2010
|
}
|
|
1899
|
-
const
|
|
2011
|
+
const configuredEntrypoints = [];
|
|
2012
|
+
const hookOpts = {
|
|
2013
|
+
runtime: 'windsurf',
|
|
2014
|
+
platform: opts.platform || process.platform,
|
|
2015
|
+
configPath: node_path_1.default.join(targetDir, 'hooks.json'),
|
|
2016
|
+
configuredEntrypoints,
|
|
2017
|
+
};
|
|
1900
2018
|
const commands = {};
|
|
1901
2019
|
for (const ev of WINDSURF_HOOK_EVENTS) {
|
|
1902
2020
|
const script = WINDSURF_EVENT_SCRIPT_MAP[ev];
|
|
@@ -1910,7 +2028,7 @@ function writeWindsurfHooksJson(targetDir, src, opts) {
|
|
|
1910
2028
|
}
|
|
1911
2029
|
const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
|
|
1912
2030
|
const result = reconcileWindsurfHooksJson(hooksJsonPath, managedEntries);
|
|
1913
|
-
return { hooksJsonPath, changed: result.changed };
|
|
2031
|
+
return { hooksJsonPath, changed: result.changed, configuredEntrypoints };
|
|
1914
2032
|
}
|
|
1915
2033
|
/**
|
|
1916
2034
|
* Remove all GSD-managed Cascade hook entries from hooks.json. User-owned
|
|
@@ -2008,608 +2126,268 @@ function writeCopilotHookConfig(targetDir) {
|
|
|
2008
2126
|
node_fs_1.default.writeFileSync(hookPath, JSON.stringify(buildCopilotHookConfig(), null, 2) + '\n');
|
|
2009
2127
|
return hookPath;
|
|
2010
2128
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2129
|
+
const BASH_UNAVAILABLE_MESSAGE = 'Bash executable path unavailable (#3393)';
|
|
2130
|
+
// The tables are exported as a contract; freeze every row (and its nested
|
|
2131
|
+
// objects) so no consumer can mutate a matcher or timeout at runtime.
|
|
2132
|
+
function deepFreeze(value) {
|
|
2133
|
+
for (const child of Object.values(value)) {
|
|
2134
|
+
if (child !== null && typeof child === 'object')
|
|
2135
|
+
deepFreeze(child);
|
|
2136
|
+
}
|
|
2137
|
+
return Object.freeze(value);
|
|
2138
|
+
}
|
|
2139
|
+
const SETTINGS_JSON_HOOK_ROWS = deepFreeze([
|
|
2140
|
+
// Guard: only register if the hook file was actually installed (#1754, #1817).
|
|
2141
|
+
{ file: 'gsd-check-update.js', event: 'SessionStart', command: { opts: 'updateCheckCommand' },
|
|
2142
|
+
configuredMessage: 'Configured update check hook', skipLabel: 'update check hook' },
|
|
2143
|
+
{ file: 'gsd-context-monitor.js', event: 'post', matcher: 'Bash|Edit|Write|MultiEdit|Agent|Task', timeout: 10,
|
|
2144
|
+
command: { opts: 'contextMonitorCommand' },
|
|
2145
|
+
configuredMessage: 'Configured context window monitor hook', skipLabel: 'context monitor hook',
|
|
2146
|
+
onPresent: { kind: 'migrate-matcher-timeout', message: 'Updated context monitor hook (added matcher + timeout)' } },
|
|
2147
|
+
{ file: 'gsd-prompt-guard.js', event: 'pre', matcher: 'Write|Edit', timeout: 'blocking',
|
|
2148
|
+
command: { opts: 'promptGuardCommand' },
|
|
2149
|
+
configuredMessage: 'Configured prompt injection guard hook', skipLabel: 'prompt guard hook' },
|
|
2150
|
+
// Read-before-edit guidance (#1628): advisory-only, does not block.
|
|
2151
|
+
{ file: 'gsd-read-guard.js', event: 'pre', matcher: 'Write|Edit', timeout: 5,
|
|
2152
|
+
command: { opts: 'readGuardCommand' },
|
|
2153
|
+
configuredMessage: 'Configured read-before-edit guard hook', skipLabel: 'read guard hook' },
|
|
2154
|
+
// Read-time prompt injection scanning (#2201).
|
|
2155
|
+
{ file: 'gsd-read-injection-scanner.js', event: 'post', matcher: 'Read', timeout: 5,
|
|
2156
|
+
command: { opts: 'readInjectionScannerCommand' },
|
|
2157
|
+
configuredMessage: 'Configured read injection scanner hook', skipLabel: 'read injection scanner hook' },
|
|
2158
|
+
// Community hooks — registered on install but opt-in at runtime via .planning/config.json.
|
|
2159
|
+
{ file: 'gsd-workflow-guard.js', event: 'pre', matcher: 'Bash|Edit|Write|MultiEdit', timeout: 'blocking',
|
|
2160
|
+
command: { build: 'js' },
|
|
2161
|
+
configuredMessage: 'Configured workflow guard hook (opt-in via hooks.workflow_guard)', skipLabel: 'workflow guard hook',
|
|
2162
|
+
onPresent: { kind: 'repair-matcher', message: 'Updated workflow guard hook matcher' } },
|
|
2163
|
+
// Worktree absolute-path safety (#260).
|
|
2164
|
+
{ file: 'gsd-worktree-path-guard.js', event: 'pre', matcher: 'Write|Edit|MultiEdit', timeout: 'blocking',
|
|
2165
|
+
command: { build: 'js' },
|
|
2166
|
+
configuredMessage: 'Configured worktree path guard hook', skipLabel: 'worktree path guard hook' },
|
|
2167
|
+
// Agent-dispatch isolation (#3045); matcher widened to Agent|Task (#3045 MAJOR 1).
|
|
2168
|
+
{ file: 'gsd-agent-isolation-guard.js', event: 'pre', matcher: 'Agent|Task', timeout: 'blocking',
|
|
2169
|
+
command: { build: 'js' },
|
|
2170
|
+
configuredMessage: 'Configured agent isolation dispatch guard hook', skipLabel: 'agent isolation guard hook' },
|
|
2171
|
+
// Catastrophic-shrink protection (#2255, fix 3 of #973).
|
|
2172
|
+
{ file: 'gsd-write-guard.js', event: 'pre', matcher: 'Write', timeout: 'blocking',
|
|
2173
|
+
command: { build: 'js' },
|
|
2174
|
+
configuredMessage: 'Configured write guard hook (catastrophic-shrink protection)', skipLabel: 'write guard hook' },
|
|
2175
|
+
// Secret-file read protection (#4221).
|
|
2176
|
+
{ file: 'gsd-secret-read-guard.js', event: 'pre', matcher: 'Read|Grep|Bash', timeout: 'blocking',
|
|
2177
|
+
command: { build: 'js' },
|
|
2178
|
+
configuredMessage: 'Configured secret read guard hook (.env / .secrets read protection)', skipLabel: 'secret read guard hook' },
|
|
2179
|
+
// The four `.sh` rows: their built commands also feed the #3329 reconcile.
|
|
2180
|
+
{ file: 'gsd-validate-commit.sh', event: 'pre', matcher: 'Bash', timeout: 'blocking',
|
|
2181
|
+
command: { build: 'sh' },
|
|
2182
|
+
configuredMessage: 'Configured commit validation hook (opt-in via config)', skipLabel: 'commit validation hook',
|
|
2183
|
+
noCommandMessage: BASH_UNAVAILABLE_MESSAGE },
|
|
2184
|
+
{ file: 'gsd-graphify-update.sh', event: 'post', matcher: 'Bash', timeout: 5,
|
|
2185
|
+
command: { build: 'sh' },
|
|
2186
|
+
configuredMessage: 'Configured graphify auto-update hook (opt-in via graphify.auto_update)', skipLabel: 'graphify auto-update hook',
|
|
2187
|
+
noCommandMessage: BASH_UNAVAILABLE_MESSAGE },
|
|
2188
|
+
{ file: 'gsd-session-state.sh', event: 'SessionStart',
|
|
2189
|
+
command: { build: 'sh' },
|
|
2190
|
+
configuredMessage: 'Configured session state orientation hook (opt-in via config)', skipLabel: 'session state hook',
|
|
2191
|
+
noCommandMessage: BASH_UNAVAILABLE_MESSAGE },
|
|
2192
|
+
{ file: 'gsd-phase-boundary.sh', event: 'post', matcher: 'Write|Edit', timeout: 5,
|
|
2193
|
+
command: { build: 'sh' },
|
|
2194
|
+
configuredMessage: 'Configured phase boundary detection hook (opt-in via config)', skipLabel: 'phase boundary hook',
|
|
2195
|
+
noCommandMessage: BASH_UNAVAILABLE_MESSAGE },
|
|
2196
|
+
]);
|
|
2197
|
+
// Extended lifecycle events are descriptor-gated (`extendedHookEvents`): a row
|
|
2198
|
+
// registers only for runtimes whose descriptor lists its event. SubagentStart
|
|
2199
|
+
// is qwen/codebuddy-only today; the Before/After Agent/Model rows are an inert
|
|
2200
|
+
// seam (no runtime declares them) that re-activates if one does (#776, #1928).
|
|
2201
|
+
// FileChanged hot-reloads the gsd config on .planning/config.json edits.
|
|
2202
|
+
function contextMonitorRow(event, runtimeLabelSuffix) {
|
|
2203
|
+
return {
|
|
2204
|
+
event,
|
|
2205
|
+
file: 'gsd-context-monitor.js',
|
|
2206
|
+
command: { opts: 'contextMonitorCommand' },
|
|
2207
|
+
timeout: 10,
|
|
2208
|
+
configuredMessage: `Configured ${event} context monitor hook`,
|
|
2209
|
+
...(runtimeLabelSuffix ? { runtimeLabelSuffix } : {}),
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
const SETTINGS_JSON_EXTENDED_ROWS = deepFreeze([
|
|
2213
|
+
contextMonitorRow('SubagentStop', true),
|
|
2214
|
+
contextMonitorRow('Stop', true),
|
|
2215
|
+
contextMonitorRow('PreCompact', true),
|
|
2216
|
+
contextMonitorRow('SubagentStart', true),
|
|
2217
|
+
contextMonitorRow('BeforeAgent', false),
|
|
2218
|
+
contextMonitorRow('AfterAgent', false),
|
|
2219
|
+
contextMonitorRow('BeforeModel', false),
|
|
2220
|
+
{ event: 'FileChanged', file: 'gsd-config-reload.js', command: { opts: 'configReloadCommand' }, matcher: 'config.json', timeout: 8,
|
|
2221
|
+
configuredMessage: 'Configured FileChanged config-reload hook (Claude Code)',
|
|
2222
|
+
noCommandMessage: 'Node executable path unavailable' },
|
|
2223
|
+
]);
|
|
2224
|
+
function hookStem(file) {
|
|
2225
|
+
return file.replace(/\.(?:js|sh)$/, '');
|
|
2226
|
+
}
|
|
2227
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access,
|
|
2228
|
+
@typescript-eslint/no-unsafe-call,
|
|
2229
|
+
@typescript-eslint/no-unsafe-assignment,
|
|
2230
|
+
@typescript-eslint/no-explicit-any */
|
|
2231
|
+
function groupReferencesHook(entry, stem) {
|
|
2232
|
+
return Boolean(entry.hooks && entry.hooks.some((h) => referencesHook(h, stem)));
|
|
2233
|
+
}
|
|
2234
|
+
function resolveRowTimeout(timeout) {
|
|
2235
|
+
return timeout === 'blocking' ? BLOCKING_GUARD_TIMEOUT_S : timeout;
|
|
2236
|
+
}
|
|
2237
|
+
function resolveRowCommand(row, opts) {
|
|
2238
|
+
if ('opts' in row.command)
|
|
2239
|
+
return opts[row.command.opts];
|
|
2240
|
+
if (opts.isGlobal)
|
|
2241
|
+
return buildHookCommand(opts.targetDir, row.file, opts.hookOpts);
|
|
2242
|
+
return row.command.build === 'sh' ? opts.localShellCmd(row.file) : opts.localCmd(row.file);
|
|
2243
|
+
}
|
|
2244
|
+
function reconcilePresentHook(row, entries, existing) {
|
|
2245
|
+
const present = row.onPresent;
|
|
2246
|
+
if (!present)
|
|
2247
|
+
return;
|
|
2248
|
+
if (present.kind === 'repair-matcher') {
|
|
2249
|
+
if (existing.matcher !== row.matcher) {
|
|
2250
|
+
existing.matcher = row.matcher;
|
|
2251
|
+
console.log(` ${green}✓${reset} ${present.message}`);
|
|
2252
|
+
}
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
// migrate-matcher-timeout: add the matcher and timeout an older installer omitted.
|
|
2256
|
+
const stem = hookStem(row.file);
|
|
2257
|
+
const timeout = resolveRowTimeout(row.timeout);
|
|
2258
|
+
for (const entry of entries) {
|
|
2259
|
+
if (!groupReferencesHook(entry, stem))
|
|
2260
|
+
continue;
|
|
2261
|
+
let migrated = false;
|
|
2262
|
+
if (!entry.matcher) {
|
|
2263
|
+
entry.matcher = row.matcher;
|
|
2264
|
+
migrated = true;
|
|
2265
|
+
}
|
|
2266
|
+
for (const h of entry.hooks) {
|
|
2267
|
+
if (referencesHook(h, stem) && !h.timeout) {
|
|
2268
|
+
h.timeout = timeout;
|
|
2269
|
+
migrated = true;
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
if (migrated)
|
|
2273
|
+
console.log(` ${green}✓${reset} ${present.message}`);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
function pushHookGroup(entries, matcher, command, timeout) {
|
|
2277
|
+
const hook = { type: 'command', command };
|
|
2278
|
+
if (timeout !== undefined)
|
|
2279
|
+
hook['timeout'] = timeout;
|
|
2280
|
+
entries.push(matcher === undefined ? { hooks: [hook] } : { matcher, hooks: [hook] });
|
|
2281
|
+
}
|
|
2282
|
+
function applySettingsJsonHookTables(settings, opts, tables) {
|
|
2283
|
+
const { runtime, postToolEvent, hookEvents, extendedHookEvents, hooksSurface, targetDir, hookOpts } = opts;
|
|
2284
|
+
// ADR-857 phase 5f-3: extended hook events are driven by the registry
|
|
2018
2285
|
// descriptor field rather than hardcoded runtime-name checks.
|
|
2019
2286
|
const extendedEvents = Array.isArray(extendedHookEvents) ? extendedHookEvents : [];
|
|
2020
2287
|
// ADR-857 phase 5g drive 3: hook-skip guard is driven by the hooksSurface
|
|
2021
2288
|
// descriptor field. Only runtimes with hooksSurface === 'settings-json'
|
|
2022
|
-
// register settings.json hooks;
|
|
2023
|
-
// (opencode, kilo) are skipped. Equivalence: hooksSurface !== 'none' iff
|
|
2024
|
-
// the old !isOpencode && !isKilo check.
|
|
2289
|
+
// register settings.json hooks; 'none' (opencode, kilo) is skipped.
|
|
2025
2290
|
// #2095: kimi's hooksSurface is 'kimi-hooks-toml' — it registers hooks into
|
|
2026
|
-
// its own native config.toml via writeKimiHooksToml
|
|
2027
|
-
//
|
|
2028
|
-
//
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
if (
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
// and the migration pass right here raises existing managed entries.
|
|
2045
|
-
const BLOCKING_GUARD_TIMEOUT_S = 120;
|
|
2046
|
-
const blockingGuardNames = [
|
|
2047
|
-
'gsd-prompt-guard',
|
|
2048
|
-
'gsd-workflow-guard',
|
|
2049
|
-
'gsd-worktree-path-guard',
|
|
2050
|
-
'gsd-agent-isolation-guard',
|
|
2051
|
-
'gsd-write-guard',
|
|
2052
|
-
'gsd-secret-read-guard',
|
|
2053
|
-
'gsd-validate-commit',
|
|
2054
|
-
];
|
|
2055
|
-
for (const entries of Object.values(settings.hooks)) {
|
|
2056
|
-
if (!Array.isArray(entries))
|
|
2291
|
+
// its own native config.toml via writeKimiHooksToml and never writes
|
|
2292
|
+
// settings.json, so this guard must skip it too, or the console would log
|
|
2293
|
+
// "Configured ..." for a settings object finishInstall() never persists.
|
|
2294
|
+
if (hooksSurface === 'none' || hooksSurface === 'kimi-hooks-toml')
|
|
2295
|
+
return;
|
|
2296
|
+
if (!settings.hooks) {
|
|
2297
|
+
settings.hooks = {};
|
|
2298
|
+
}
|
|
2299
|
+
if (!settings.hooks.SessionStart) {
|
|
2300
|
+
settings.hooks.SessionStart = [];
|
|
2301
|
+
}
|
|
2302
|
+
// #3981: raise existing managed blocking-guard entries still on the old
|
|
2303
|
+
// 5 s budget to BLOCKING_GUARD_TIMEOUT_S (module-level; rationale there).
|
|
2304
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
2305
|
+
if (!Array.isArray(entries))
|
|
2306
|
+
continue;
|
|
2307
|
+
for (const entry of entries) {
|
|
2308
|
+
if (!entry || !Array.isArray(entry.hooks))
|
|
2057
2309
|
continue;
|
|
2058
|
-
for (const
|
|
2059
|
-
if (
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
if (blockingGuardNames.some((name) => referencesHook(h, name)) &&
|
|
2063
|
-
h.timeout === 5) {
|
|
2064
|
-
h.timeout = BLOCKING_GUARD_TIMEOUT_S;
|
|
2065
|
-
}
|
|
2310
|
+
for (const h of entry.hooks) {
|
|
2311
|
+
if (BLOCKING_GUARD_NAMES.some((name) => referencesHook(h, name)) &&
|
|
2312
|
+
h.timeout === 5) {
|
|
2313
|
+
h.timeout = BLOCKING_GUARD_TIMEOUT_S;
|
|
2066
2314
|
}
|
|
2067
2315
|
}
|
|
2068
2316
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
// Configure post-tool hook for context window monitoring
|
|
2090
|
-
if (!settings.hooks[postToolEvent]) {
|
|
2091
|
-
settings.hooks[postToolEvent] = [];
|
|
2092
|
-
}
|
|
2093
|
-
const hasContextMonitorHook = settings.hooks[postToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-context-monitor')));
|
|
2094
|
-
const contextMonitorFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-context-monitor.js');
|
|
2095
|
-
if (!hasContextMonitorHook && node_fs_1.default.existsSync(contextMonitorFile) && contextMonitorCommand) {
|
|
2096
|
-
settings.hooks[postToolEvent].push({
|
|
2097
|
-
matcher: 'Bash|Edit|Write|MultiEdit|Agent|Task',
|
|
2098
|
-
hooks: [
|
|
2099
|
-
{
|
|
2100
|
-
type: 'command',
|
|
2101
|
-
command: contextMonitorCommand,
|
|
2102
|
-
timeout: 10
|
|
2103
|
-
}
|
|
2104
|
-
]
|
|
2105
|
-
});
|
|
2106
|
-
console.log(` ${green}✓${reset} Configured context window monitor hook`);
|
|
2317
|
+
}
|
|
2318
|
+
// hookEvents='gemini' → BeforeTool/AfterTool; all others → PreToolUse/PostToolUse.
|
|
2319
|
+
const preToolEvent = hookEvents === 'gemini' ? 'BeforeTool' : 'PreToolUse';
|
|
2320
|
+
const eventKey = (event) => event === 'SessionStart' ? 'SessionStart' : event === 'post' ? postToolEvent : preToolEvent;
|
|
2321
|
+
// Built `.sh` commands, in row order, feed the #3329 reconcile below.
|
|
2322
|
+
const shellCommands = {};
|
|
2323
|
+
for (const row of tables.rows) {
|
|
2324
|
+
const key = eventKey(row.event);
|
|
2325
|
+
if (!settings.hooks[key]) {
|
|
2326
|
+
settings.hooks[key] = [];
|
|
2327
|
+
}
|
|
2328
|
+
const entries = settings.hooks[key];
|
|
2329
|
+
const command = resolveRowCommand(row, opts);
|
|
2330
|
+
if ('build' in row.command && row.command.build === 'sh')
|
|
2331
|
+
shellCommands[row.file] = command;
|
|
2332
|
+
const stem = hookStem(row.file);
|
|
2333
|
+
const existing = entries.find((entry) => groupReferencesHook(entry, stem));
|
|
2334
|
+
if (existing) {
|
|
2335
|
+
reconcilePresentHook(row, entries, existing);
|
|
2336
|
+
continue;
|
|
2107
2337
|
}
|
|
2108
|
-
|
|
2109
|
-
console.warn(` ${yellow}⚠${reset} Skipped
|
|
2338
|
+
if (!node_fs_1.default.existsSync(node_path_1.default.join(targetDir, 'hooks', row.file))) {
|
|
2339
|
+
console.warn(` ${yellow}⚠${reset} Skipped ${row.skipLabel} — ${row.file} not found at target`);
|
|
2110
2340
|
}
|
|
2111
|
-
else {
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
if (entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-context-monitor'))) {
|
|
2115
|
-
let migrated = false;
|
|
2116
|
-
if (!entry.matcher) {
|
|
2117
|
-
entry.matcher = 'Bash|Edit|Write|MultiEdit|Agent|Task';
|
|
2118
|
-
migrated = true;
|
|
2119
|
-
}
|
|
2120
|
-
for (const h of entry.hooks) {
|
|
2121
|
-
if (referencesHook(h, 'gsd-context-monitor') && !h.timeout) {
|
|
2122
|
-
h.timeout = 10;
|
|
2123
|
-
migrated = true;
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
if (migrated) {
|
|
2127
|
-
console.log(` ${green}✓${reset} Updated context monitor hook (added matcher + timeout)`);
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2341
|
+
else if (command) {
|
|
2342
|
+
pushHookGroup(entries, row.matcher, command, resolveRowTimeout(row.timeout));
|
|
2343
|
+
console.log(` ${green}✓${reset} ${row.configuredMessage}`);
|
|
2131
2344
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
// hookEvents='gemini' → BeforeTool; all others → PreToolUse.
|
|
2135
|
-
// Equivalence: hookEvents='gemini' iff runtime===antigravity (same as old check).
|
|
2136
|
-
const preToolEvent = hookEvents === 'gemini' ? 'BeforeTool' : 'PreToolUse';
|
|
2137
|
-
if (!settings.hooks[preToolEvent]) {
|
|
2138
|
-
settings.hooks[preToolEvent] = [];
|
|
2139
|
-
}
|
|
2140
|
-
const hasPromptGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-prompt-guard')));
|
|
2141
|
-
const promptGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-prompt-guard.js');
|
|
2142
|
-
if (!hasPromptGuardHook && node_fs_1.default.existsSync(promptGuardFile) && promptGuardCommand) {
|
|
2143
|
-
settings.hooks[preToolEvent].push({
|
|
2144
|
-
matcher: 'Write|Edit',
|
|
2145
|
-
hooks: [
|
|
2146
|
-
{
|
|
2147
|
-
type: 'command',
|
|
2148
|
-
command: promptGuardCommand,
|
|
2149
|
-
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
2150
|
-
}
|
|
2151
|
-
]
|
|
2152
|
-
});
|
|
2153
|
-
console.log(` ${green}✓${reset} Configured prompt injection guard hook`);
|
|
2154
|
-
}
|
|
2155
|
-
else if (!hasPromptGuardHook && !node_fs_1.default.existsSync(promptGuardFile)) {
|
|
2156
|
-
console.warn(` ${yellow}⚠${reset} Skipped prompt guard hook — gsd-prompt-guard.js not found at target`);
|
|
2157
|
-
}
|
|
2158
|
-
// Configure PreToolUse hook for read-before-edit guidance (#1628)
|
|
2159
|
-
// Prevents infinite retry loops when non-Claude models attempt to edit
|
|
2160
|
-
// files without reading them first. Advisory-only — does not block.
|
|
2161
|
-
const hasReadGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-read-guard')));
|
|
2162
|
-
const readGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-read-guard.js');
|
|
2163
|
-
if (!hasReadGuardHook && node_fs_1.default.existsSync(readGuardFile) && readGuardCommand) {
|
|
2164
|
-
settings.hooks[preToolEvent].push({
|
|
2165
|
-
matcher: 'Write|Edit',
|
|
2166
|
-
hooks: [
|
|
2167
|
-
{
|
|
2168
|
-
type: 'command',
|
|
2169
|
-
command: readGuardCommand,
|
|
2170
|
-
timeout: 5
|
|
2171
|
-
}
|
|
2172
|
-
]
|
|
2173
|
-
});
|
|
2174
|
-
console.log(` ${green}✓${reset} Configured read-before-edit guard hook`);
|
|
2175
|
-
}
|
|
2176
|
-
else if (!hasReadGuardHook && !node_fs_1.default.existsSync(readGuardFile)) {
|
|
2177
|
-
console.warn(` ${yellow}⚠${reset} Skipped read guard hook — gsd-read-guard.js not found at target`);
|
|
2178
|
-
}
|
|
2179
|
-
// Configure PostToolUse hook for read-time prompt injection scanning (#2201)
|
|
2180
|
-
// Scans content returned by the Read tool for injection patterns, including
|
|
2181
|
-
// summarisation-specific patterns that survive context compression.
|
|
2182
|
-
const hasReadInjectionScannerHook = settings.hooks[postToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-read-injection-scanner')));
|
|
2183
|
-
const readInjectionScannerFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-read-injection-scanner.js');
|
|
2184
|
-
if (!hasReadInjectionScannerHook && node_fs_1.default.existsSync(readInjectionScannerFile) && readInjectionScannerCommand) {
|
|
2185
|
-
settings.hooks[postToolEvent].push({
|
|
2186
|
-
matcher: 'Read',
|
|
2187
|
-
hooks: [
|
|
2188
|
-
{
|
|
2189
|
-
type: 'command',
|
|
2190
|
-
command: readInjectionScannerCommand,
|
|
2191
|
-
timeout: 5
|
|
2192
|
-
}
|
|
2193
|
-
]
|
|
2194
|
-
});
|
|
2195
|
-
console.log(` ${green}✓${reset} Configured read injection scanner hook`);
|
|
2196
|
-
}
|
|
2197
|
-
else if (!hasReadInjectionScannerHook && !node_fs_1.default.existsSync(readInjectionScannerFile)) {
|
|
2198
|
-
console.warn(` ${yellow}⚠${reset} Skipped read injection scanner hook — gsd-read-injection-scanner.js not found at target`);
|
|
2199
|
-
}
|
|
2200
|
-
// Community hooks — registered on install but opt-in at runtime.
|
|
2201
|
-
// Each hook checks .planning/config.json for hooks.community: true
|
|
2202
|
-
// and exits silently (no-op) if not enabled. This lets users enable
|
|
2203
|
-
// them per-project by adding: "hooks": { "community": true }
|
|
2204
|
-
// Configure workflow guard hook (opt-in via hooks.workflow_guard: true)
|
|
2205
|
-
// Detects file edits outside GSD workflow context and advises using
|
|
2206
|
-
// /gsd-quick or /gsd-fast for state-tracked changes. Also hard-blocks
|
|
2207
|
-
// unsafe Bash commands that violate worktree-agent isolation.
|
|
2208
|
-
const workflowGuardCommand = isGlobal
|
|
2209
|
-
? buildHookCommand(targetDir, 'gsd-workflow-guard.js', hookOpts)
|
|
2210
|
-
: localCmd('gsd-workflow-guard.js');
|
|
2211
|
-
const workflowGuardMatcher = 'Bash|Edit|Write|MultiEdit';
|
|
2212
|
-
const workflowGuardHookEntry = settings.hooks[preToolEvent].find((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-workflow-guard')));
|
|
2213
|
-
const hasWorkflowGuardHook = Boolean(workflowGuardHookEntry);
|
|
2214
|
-
const workflowGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-workflow-guard.js');
|
|
2215
|
-
if (hasWorkflowGuardHook && workflowGuardHookEntry.matcher !== workflowGuardMatcher) {
|
|
2216
|
-
workflowGuardHookEntry.matcher = workflowGuardMatcher;
|
|
2217
|
-
console.log(` ${green}✓${reset} Updated workflow guard hook matcher`);
|
|
2218
|
-
}
|
|
2219
|
-
else if (!hasWorkflowGuardHook && node_fs_1.default.existsSync(workflowGuardFile) && workflowGuardCommand) {
|
|
2220
|
-
settings.hooks[preToolEvent].push({
|
|
2221
|
-
matcher: workflowGuardMatcher,
|
|
2222
|
-
hooks: [
|
|
2223
|
-
{
|
|
2224
|
-
type: 'command',
|
|
2225
|
-
command: workflowGuardCommand,
|
|
2226
|
-
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
2227
|
-
}
|
|
2228
|
-
]
|
|
2229
|
-
});
|
|
2230
|
-
console.log(` ${green}✓${reset} Configured workflow guard hook (opt-in via hooks.workflow_guard)`);
|
|
2231
|
-
}
|
|
2232
|
-
else if (!hasWorkflowGuardHook && !node_fs_1.default.existsSync(workflowGuardFile)) {
|
|
2233
|
-
console.warn(` ${yellow}⚠${reset} Skipped workflow guard hook — gsd-workflow-guard.js not found at target`);
|
|
2234
|
-
}
|
|
2235
|
-
// Configure PreToolUse hook for worktree absolute-path safety (#260)
|
|
2236
|
-
// Hard-blocks Edit/Write/MultiEdit tool calls with absolute paths that resolve
|
|
2237
|
-
// outside the current worktree root. Prevents executor agents from
|
|
2238
|
-
// accidentally writing to the main checkout when running in isolation="worktree".
|
|
2239
|
-
const worktreePathGuardCommand = isGlobal
|
|
2240
|
-
? buildHookCommand(targetDir, 'gsd-worktree-path-guard.js', hookOpts)
|
|
2241
|
-
: localCmd('gsd-worktree-path-guard.js');
|
|
2242
|
-
const hasWorktreePathGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-worktree-path-guard')));
|
|
2243
|
-
const worktreePathGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-worktree-path-guard.js');
|
|
2244
|
-
if (!hasWorktreePathGuardHook && node_fs_1.default.existsSync(worktreePathGuardFile) && worktreePathGuardCommand) {
|
|
2245
|
-
settings.hooks[preToolEvent].push({
|
|
2246
|
-
matcher: 'Write|Edit|MultiEdit',
|
|
2247
|
-
hooks: [
|
|
2248
|
-
{
|
|
2249
|
-
type: 'command',
|
|
2250
|
-
command: worktreePathGuardCommand,
|
|
2251
|
-
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
2252
|
-
}
|
|
2253
|
-
]
|
|
2254
|
-
});
|
|
2255
|
-
console.log(` ${green}✓${reset} Configured worktree path guard hook`);
|
|
2256
|
-
}
|
|
2257
|
-
else if (!hasWorktreePathGuardHook && !node_fs_1.default.existsSync(worktreePathGuardFile)) {
|
|
2258
|
-
console.warn(` ${yellow}⚠${reset} Skipped worktree path guard hook — gsd-worktree-path-guard.js not found at target`);
|
|
2259
|
-
}
|
|
2260
|
-
// Configure PreToolUse hook for Agent-dispatch isolation (#3045)
|
|
2261
|
-
// Hard-blocks an executor Agent() dispatch (subagent_type="gsd-executor")
|
|
2262
|
-
// missing its harness isolation parameter when this project's resolved
|
|
2263
|
-
// dispatch isolation is harness-worktree. Prevents the executor from
|
|
2264
|
-
// silently running and committing in the primary checkout when the
|
|
2265
|
-
// model-authored dispatch omits isolation="worktree".
|
|
2266
|
-
const agentIsolationGuardCommand = isGlobal
|
|
2267
|
-
? buildHookCommand(targetDir, 'gsd-agent-isolation-guard.js', hookOpts)
|
|
2268
|
-
: localCmd('gsd-agent-isolation-guard.js');
|
|
2269
|
-
const hasAgentIsolationGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-agent-isolation-guard')));
|
|
2270
|
-
const agentIsolationGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-agent-isolation-guard.js');
|
|
2271
|
-
if (!hasAgentIsolationGuardHook && node_fs_1.default.existsSync(agentIsolationGuardFile) && agentIsolationGuardCommand) {
|
|
2272
|
-
settings.hooks[preToolEvent].push({
|
|
2273
|
-
// #3045 MAJOR 1: widened from "Agent"-only — hooks.json's own
|
|
2274
|
-
// PostToolUse precedent (context-monitor) already hedges both names,
|
|
2275
|
-
// and the hook itself now accepts tool_name "Task" too.
|
|
2276
|
-
matcher: 'Agent|Task',
|
|
2277
|
-
hooks: [
|
|
2278
|
-
{
|
|
2279
|
-
type: 'command',
|
|
2280
|
-
command: agentIsolationGuardCommand,
|
|
2281
|
-
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
2282
|
-
}
|
|
2283
|
-
]
|
|
2284
|
-
});
|
|
2285
|
-
console.log(` ${green}✓${reset} Configured agent isolation dispatch guard hook`);
|
|
2286
|
-
}
|
|
2287
|
-
else if (!hasAgentIsolationGuardHook && !node_fs_1.default.existsSync(agentIsolationGuardFile)) {
|
|
2288
|
-
console.warn(` ${yellow}⚠${reset} Skipped agent isolation guard hook — gsd-agent-isolation-guard.js not found at target`);
|
|
2289
|
-
}
|
|
2290
|
-
// Configure PreToolUse hook for catastrophic-shrink protection (#2255, fix 3 of #973)
|
|
2291
|
-
// Hard-blocks a whole-file Write that collapses a curated .planning/ artifact
|
|
2292
|
-
// (ROADMAP.md, milestone roadmaps, STATE.md) far below its on-disk size.
|
|
2293
|
-
// Escape hatches (both named in the block message): the single-use
|
|
2294
|
-
// sentinel .planning/.gsd-allow-shrink (workflow steps — a per-step env
|
|
2295
|
-
// cannot reach a hook) and GSD_ALLOW_PLANNING_SHRINK=1 (interactive).
|
|
2296
|
-
const writeGuardCommand = isGlobal
|
|
2297
|
-
? buildHookCommand(targetDir, 'gsd-write-guard.js', hookOpts)
|
|
2298
|
-
: localCmd('gsd-write-guard.js');
|
|
2299
|
-
const hasWriteGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-write-guard')));
|
|
2300
|
-
const writeGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-write-guard.js');
|
|
2301
|
-
if (!hasWriteGuardHook && node_fs_1.default.existsSync(writeGuardFile) && writeGuardCommand) {
|
|
2302
|
-
settings.hooks[preToolEvent].push({
|
|
2303
|
-
matcher: 'Write',
|
|
2304
|
-
hooks: [
|
|
2305
|
-
{
|
|
2306
|
-
type: 'command',
|
|
2307
|
-
command: writeGuardCommand,
|
|
2308
|
-
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
2309
|
-
}
|
|
2310
|
-
]
|
|
2311
|
-
});
|
|
2312
|
-
console.log(` ${green}✓${reset} Configured write guard hook (catastrophic-shrink protection)`);
|
|
2313
|
-
}
|
|
2314
|
-
else if (!hasWriteGuardHook && !node_fs_1.default.existsSync(writeGuardFile)) {
|
|
2315
|
-
console.warn(` ${yellow}⚠${reset} Skipped write guard hook — gsd-write-guard.js not found at target`);
|
|
2316
|
-
}
|
|
2317
|
-
// Configure PreToolUse hook for secret-file read protection (#4221).
|
|
2318
|
-
// Hard-blocks Read/Grep/Bash reads of .env, .env.<suffix> and .secrets.
|
|
2319
|
-
// Replaces the Read(.env*) permission deny rules the installer used to
|
|
2320
|
-
// write (#768): on Claude Code >= 2.1.259 ANY Read() deny rule makes every
|
|
2321
|
-
// `cd DIR && grep …` compound prompt for approval, even in auto mode; a
|
|
2322
|
-
// hook denial is not a permission rule and never arms that check.
|
|
2323
|
-
const secretReadGuardCommand = isGlobal
|
|
2324
|
-
? buildHookCommand(targetDir, 'gsd-secret-read-guard.js', hookOpts)
|
|
2325
|
-
: localCmd('gsd-secret-read-guard.js');
|
|
2326
|
-
const hasSecretReadGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-secret-read-guard')));
|
|
2327
|
-
const secretReadGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-secret-read-guard.js');
|
|
2328
|
-
if (!hasSecretReadGuardHook && node_fs_1.default.existsSync(secretReadGuardFile) && secretReadGuardCommand) {
|
|
2329
|
-
settings.hooks[preToolEvent].push({
|
|
2330
|
-
matcher: 'Read|Grep|Bash',
|
|
2331
|
-
hooks: [
|
|
2332
|
-
{
|
|
2333
|
-
type: 'command',
|
|
2334
|
-
command: secretReadGuardCommand,
|
|
2335
|
-
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
2336
|
-
}
|
|
2337
|
-
]
|
|
2338
|
-
});
|
|
2339
|
-
console.log(` ${green}✓${reset} Configured secret read guard hook (.env / .secrets read protection)`);
|
|
2340
|
-
}
|
|
2341
|
-
else if (!hasSecretReadGuardHook && !node_fs_1.default.existsSync(secretReadGuardFile)) {
|
|
2342
|
-
console.warn(` ${yellow}⚠${reset} Skipped secret read guard hook — gsd-secret-read-guard.js not found at target`);
|
|
2343
|
-
}
|
|
2344
|
-
// Configure commit validation hook (Conventional Commits enforcement, opt-in)
|
|
2345
|
-
const validateCommitCommand = isGlobal
|
|
2346
|
-
? buildHookCommand(targetDir, 'gsd-validate-commit.sh', hookOpts)
|
|
2347
|
-
: localShellCmd('gsd-validate-commit.sh');
|
|
2348
|
-
const hasValidateCommitHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-validate-commit')));
|
|
2349
|
-
// Guard: only register if the .sh file was actually installed. If the npm package
|
|
2350
|
-
// omitted the file (as happened in v1.32.0, bug #1817), registering a missing hook
|
|
2351
|
-
// causes a hook error on every Bash tool invocation.
|
|
2352
|
-
const validateCommitFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-validate-commit.sh');
|
|
2353
|
-
if (!hasValidateCommitHook && node_fs_1.default.existsSync(validateCommitFile) && validateCommitCommand) {
|
|
2354
|
-
settings.hooks[preToolEvent].push({
|
|
2355
|
-
matcher: 'Bash',
|
|
2356
|
-
hooks: [
|
|
2357
|
-
{
|
|
2358
|
-
type: 'command',
|
|
2359
|
-
command: validateCommitCommand,
|
|
2360
|
-
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
2361
|
-
}
|
|
2362
|
-
]
|
|
2363
|
-
});
|
|
2364
|
-
console.log(` ${green}✓${reset} Configured commit validation hook (opt-in via config)`);
|
|
2365
|
-
}
|
|
2366
|
-
else if (!hasValidateCommitHook && !node_fs_1.default.existsSync(validateCommitFile)) {
|
|
2367
|
-
console.warn(` ${yellow}⚠${reset} Skipped commit validation hook — gsd-validate-commit.sh not found at target`);
|
|
2368
|
-
}
|
|
2369
|
-
else if (!hasValidateCommitHook && !validateCommitCommand) {
|
|
2370
|
-
console.warn(` ${yellow}⚠${reset} Skipped commit validation hook — Bash executable path unavailable (#3393)`);
|
|
2371
|
-
}
|
|
2372
|
-
// Configure graphify auto-update hook (opt-in via graphify.auto_update; default false, #3347).
|
|
2373
|
-
// PostToolUse Bash matcher — fires after git commit/merge/pull/rebase --continue/cherry-pick
|
|
2374
|
-
// on the default branch, dispatches `graphify update .` in a detached subprocess. No-op unless
|
|
2375
|
-
// .planning/config.json has BOTH graphify.enabled=true AND graphify.auto_update=true.
|
|
2376
|
-
const graphifyUpdateCommand = isGlobal
|
|
2377
|
-
? buildHookCommand(targetDir, 'gsd-graphify-update.sh', hookOpts)
|
|
2378
|
-
: localShellCmd('gsd-graphify-update.sh');
|
|
2379
|
-
const hasGraphifyUpdateHook = settings.hooks[postToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-graphify-update')));
|
|
2380
|
-
const graphifyUpdateFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-graphify-update.sh');
|
|
2381
|
-
if (!hasGraphifyUpdateHook && node_fs_1.default.existsSync(graphifyUpdateFile) && graphifyUpdateCommand) {
|
|
2382
|
-
settings.hooks[postToolEvent].push({
|
|
2383
|
-
matcher: 'Bash',
|
|
2384
|
-
hooks: [
|
|
2385
|
-
{
|
|
2386
|
-
type: 'command',
|
|
2387
|
-
command: graphifyUpdateCommand,
|
|
2388
|
-
timeout: 5
|
|
2389
|
-
}
|
|
2390
|
-
]
|
|
2391
|
-
});
|
|
2392
|
-
console.log(` ${green}✓${reset} Configured graphify auto-update hook (opt-in via graphify.auto_update)`);
|
|
2393
|
-
}
|
|
2394
|
-
else if (!hasGraphifyUpdateHook && !node_fs_1.default.existsSync(graphifyUpdateFile)) {
|
|
2395
|
-
console.warn(` ${yellow}⚠${reset} Skipped graphify auto-update hook — gsd-graphify-update.sh not found at target`);
|
|
2396
|
-
}
|
|
2397
|
-
else if (!hasGraphifyUpdateHook && !graphifyUpdateCommand) {
|
|
2398
|
-
console.warn(` ${yellow}⚠${reset} Skipped graphify auto-update hook — Bash executable path unavailable (#3393)`);
|
|
2399
|
-
}
|
|
2400
|
-
// Configure session state orientation hook (opt-in)
|
|
2401
|
-
const sessionStateCommand = isGlobal
|
|
2402
|
-
? buildHookCommand(targetDir, 'gsd-session-state.sh', hookOpts)
|
|
2403
|
-
: localShellCmd('gsd-session-state.sh');
|
|
2404
|
-
const hasSessionStateHook = settings.hooks.SessionStart.some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-session-state')));
|
|
2405
|
-
const sessionStateFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-session-state.sh');
|
|
2406
|
-
if (!hasSessionStateHook && node_fs_1.default.existsSync(sessionStateFile) && sessionStateCommand) {
|
|
2407
|
-
settings.hooks.SessionStart.push({
|
|
2408
|
-
hooks: [
|
|
2409
|
-
{
|
|
2410
|
-
type: 'command',
|
|
2411
|
-
command: sessionStateCommand
|
|
2412
|
-
}
|
|
2413
|
-
]
|
|
2414
|
-
});
|
|
2415
|
-
console.log(` ${green}✓${reset} Configured session state orientation hook (opt-in via config)`);
|
|
2416
|
-
}
|
|
2417
|
-
else if (!hasSessionStateHook && !node_fs_1.default.existsSync(sessionStateFile)) {
|
|
2418
|
-
console.warn(` ${yellow}⚠${reset} Skipped session state hook — gsd-session-state.sh not found at target`);
|
|
2419
|
-
}
|
|
2420
|
-
else if (!hasSessionStateHook && !sessionStateCommand) {
|
|
2421
|
-
console.warn(` ${yellow}⚠${reset} Skipped session state hook — Bash executable path unavailable (#3393)`);
|
|
2422
|
-
}
|
|
2423
|
-
// Configure phase boundary detection hook (opt-in)
|
|
2424
|
-
const phaseBoundaryCommand = isGlobal
|
|
2425
|
-
? buildHookCommand(targetDir, 'gsd-phase-boundary.sh', hookOpts)
|
|
2426
|
-
: localShellCmd('gsd-phase-boundary.sh');
|
|
2427
|
-
const hasPhaseBoundaryHook = settings.hooks[postToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-phase-boundary')));
|
|
2428
|
-
const phaseBoundaryFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-phase-boundary.sh');
|
|
2429
|
-
if (!hasPhaseBoundaryHook && node_fs_1.default.existsSync(phaseBoundaryFile) && phaseBoundaryCommand) {
|
|
2430
|
-
settings.hooks[postToolEvent].push({
|
|
2431
|
-
matcher: 'Write|Edit',
|
|
2432
|
-
hooks: [
|
|
2433
|
-
{
|
|
2434
|
-
type: 'command',
|
|
2435
|
-
command: phaseBoundaryCommand,
|
|
2436
|
-
timeout: 5
|
|
2437
|
-
}
|
|
2438
|
-
]
|
|
2439
|
-
});
|
|
2440
|
-
console.log(` ${green}✓${reset} Configured phase boundary detection hook (opt-in via config)`);
|
|
2441
|
-
}
|
|
2442
|
-
else if (!hasPhaseBoundaryHook && !node_fs_1.default.existsSync(phaseBoundaryFile)) {
|
|
2443
|
-
console.warn(` ${yellow}⚠${reset} Skipped phase boundary hook — gsd-phase-boundary.sh not found at target`);
|
|
2444
|
-
}
|
|
2445
|
-
else if (!hasPhaseBoundaryHook && !phaseBoundaryCommand) {
|
|
2446
|
-
console.warn(` ${yellow}⚠${reset} Skipped phase boundary hook — Bash executable path unavailable (#3393)`);
|
|
2447
|
-
}
|
|
2448
|
-
// #3329: the four `.sh` sites above register only-if-absent, so an entry
|
|
2449
|
-
// registered by an older installer keeps its old command forever —
|
|
2450
|
-
// /gsd-update (which re-invokes the installer) never re-derived it. On
|
|
2451
|
-
// Claude/win32 that left the pre-#580/#3393 bash-runner-prefixed commands
|
|
2452
|
-
// in settings.json indefinitely. Reconcile existing managed `.sh` entries
|
|
2453
|
-
// to the command this install would generate today. Inert wherever the
|
|
2454
|
-
// bash runner is still the correct shape; scoped to exact managed
|
|
2455
|
-
// basenames so user-authored hooks are never touched.
|
|
2456
|
-
if (reconcileManagedShellHookCommands(settings, {
|
|
2457
|
-
'gsd-validate-commit.sh': validateCommitCommand,
|
|
2458
|
-
'gsd-graphify-update.sh': graphifyUpdateCommand,
|
|
2459
|
-
'gsd-session-state.sh': sessionStateCommand,
|
|
2460
|
-
'gsd-phase-boundary.sh': phaseBoundaryCommand,
|
|
2461
|
-
}, { platform: hookOpts.platform, runtime })) {
|
|
2462
|
-
console.log(` ${green}✓${reset} Reconciled managed .sh hook commands to current format (#3329)`);
|
|
2463
|
-
}
|
|
2464
|
-
// ── Extended hook events: SubagentStop / Stop / PreCompact / SubagentStart
|
|
2465
|
-
// (#788 + #770 + #2092) ────────────────────────────────────────────────
|
|
2466
|
-
// Claude Code (since #770) and Qwen Code (since #788) both support the
|
|
2467
|
-
// SubagentStop / Stop / PreCompact lifecycle events. Qwen Code additionally
|
|
2468
|
-
// supports SubagentStart (#2092 Phase B, Upgrade 2). Wire gsd-context-
|
|
2469
|
-
// monitor so agents get context-headroom warnings at subagent start,
|
|
2470
|
-
// subagent completion, model stop, and pre-compaction (the most critical
|
|
2471
|
-
// moment to surface headroom info).
|
|
2472
|
-
//
|
|
2473
|
-
// SubagentStart — subagent lifecycle start (context headroom tracking;
|
|
2474
|
-
// qwen-only today — no other runtime declares it in
|
|
2475
|
-
// extendedHookEvents)
|
|
2476
|
-
// SubagentStop — subagent lifecycle completion (context headroom tracking)
|
|
2477
|
-
// Stop — model stop / final-response moment (context headroom)
|
|
2478
|
-
// PreCompact — fires before conversation compaction (most critical
|
|
2479
|
-
// moment to surface context headroom warnings)
|
|
2480
|
-
//
|
|
2481
|
-
// Note: UserPromptSubmit is NOT wired here. That event carries the raw
|
|
2482
|
-
// user prompt text, not a tool invocation, so gsd-prompt-guard (which
|
|
2483
|
-
// exits unless tool_name is Write/Edit) would be a silent no-op. A
|
|
2484
|
-
// dedicated handler for UserPromptSubmit is deferred to a follow-on issue.
|
|
2485
|
-
// SubagentStart, SubagentStop, Stop, PreCompact — route through the context monitor.
|
|
2486
|
-
// Guard is descriptor-driven: only events present in extendedEvents are wired,
|
|
2487
|
-
// so this loop is a no-op for every runtime that doesn't list SubagentStart.
|
|
2488
|
-
{
|
|
2489
|
-
// Descriptor-driven (ADR-1239 / #2092): folded from a hardcoded
|
|
2490
|
-
// `runtime === 'qwen' ? ... : ...` ternary into a capability-title
|
|
2491
|
-
// lookup (see _capabilityTitle above).
|
|
2492
|
-
const runtimeLabel = _capabilityTitle(runtime);
|
|
2493
|
-
for (const event of ['SubagentStop', 'Stop', 'PreCompact', 'SubagentStart']) {
|
|
2494
|
-
if (!extendedEvents.includes(event))
|
|
2495
|
-
continue;
|
|
2496
|
-
if (!settings.hooks[event]) {
|
|
2497
|
-
settings.hooks[event] = [];
|
|
2498
|
-
}
|
|
2499
|
-
const alreadyHasContextMonitor = settings.hooks[event].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-context-monitor')));
|
|
2500
|
-
if (!alreadyHasContextMonitor && node_fs_1.default.existsSync(contextMonitorFile) && contextMonitorCommand) {
|
|
2501
|
-
settings.hooks[event].push({
|
|
2502
|
-
hooks: [
|
|
2503
|
-
{
|
|
2504
|
-
type: 'command',
|
|
2505
|
-
command: contextMonitorCommand,
|
|
2506
|
-
timeout: 10
|
|
2507
|
-
}
|
|
2508
|
-
]
|
|
2509
|
-
});
|
|
2510
|
-
console.log(` ${green}✓${reset} Configured ${event} context monitor hook (${runtimeLabel})`);
|
|
2511
|
-
}
|
|
2512
|
-
else if (!alreadyHasContextMonitor && !node_fs_1.default.existsSync(contextMonitorFile)) {
|
|
2513
|
-
console.warn(` ${yellow}⚠${reset} Skipped ${event} hook — gsd-context-monitor.js not found at target`);
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2345
|
+
else if (row.noCommandMessage) {
|
|
2346
|
+
console.warn(` ${yellow}⚠${reset} Skipped ${row.skipLabel} — ${row.noCommandMessage}`);
|
|
2516
2347
|
}
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
//
|
|
2537
|
-
// All three reuse gsd-context-monitor.js — no new hook files needed.
|
|
2538
|
-
// The `decision:"deny"` retry capability of AfterAgent is intentionally
|
|
2539
|
-
// left to the hook script to implement when triggered (gsd-context-monitor
|
|
2540
|
-
// exits 0 / advisory-only today; an active quality gate is a follow-on).
|
|
2541
|
-
//
|
|
2542
|
-
// Note: BeforeToolSelection is NOT wired. That event does not map to a
|
|
2543
|
-
// gsd hook use case at this time; deferred to a follow-on issue.
|
|
2544
|
-
//
|
|
2545
|
-
// Guard is now descriptor-driven: only events present in extendedEvents are wired.
|
|
2546
|
-
for (const extendedEvent of ['BeforeAgent', 'AfterAgent', 'BeforeModel']) {
|
|
2547
|
-
if (!extendedEvents.includes(extendedEvent))
|
|
2548
|
-
continue;
|
|
2549
|
-
if (!Array.isArray(settings.hooks[extendedEvent])) {
|
|
2550
|
-
settings.hooks[extendedEvent] = [];
|
|
2551
|
-
}
|
|
2552
|
-
const alreadyHasContextMonitor = settings.hooks[extendedEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-context-monitor')));
|
|
2553
|
-
if (!alreadyHasContextMonitor && node_fs_1.default.existsSync(contextMonitorFile) && contextMonitorCommand) {
|
|
2554
|
-
settings.hooks[extendedEvent].push({
|
|
2555
|
-
hooks: [
|
|
2556
|
-
{
|
|
2557
|
-
type: 'command',
|
|
2558
|
-
command: contextMonitorCommand,
|
|
2559
|
-
timeout: 10
|
|
2560
|
-
}
|
|
2561
|
-
]
|
|
2562
|
-
});
|
|
2563
|
-
console.log(` ${green}✓${reset} Configured ${extendedEvent} context monitor hook`);
|
|
2564
|
-
}
|
|
2565
|
-
else if (!alreadyHasContextMonitor && !node_fs_1.default.existsSync(contextMonitorFile)) {
|
|
2566
|
-
console.warn(` ${yellow}⚠${reset} Skipped ${extendedEvent} hook — gsd-context-monitor.js not found at target`);
|
|
2567
|
-
}
|
|
2348
|
+
}
|
|
2349
|
+
// #3329: `.sh` rows register only-if-absent, so an entry registered by an
|
|
2350
|
+
// older installer keeps its old command forever — /gsd-update (which
|
|
2351
|
+
// re-invokes the installer) never re-derived it. On Claude/win32 that left
|
|
2352
|
+
// the pre-#580/#3393 bash-runner-prefixed commands in settings.json
|
|
2353
|
+
// indefinitely. Reconcile existing managed `.sh` entries to the command this
|
|
2354
|
+
// install would generate today. Inert wherever the bash runner is still the
|
|
2355
|
+
// correct shape; scoped to exact managed basenames so user-authored hooks
|
|
2356
|
+
// are never touched.
|
|
2357
|
+
if (reconcileManagedShellHookCommands(settings, shellCommands, { platform: hookOpts.platform, runtime })) {
|
|
2358
|
+
console.log(` ${green}✓${reset} Reconciled managed .sh hook commands to current format (#3329)`);
|
|
2359
|
+
}
|
|
2360
|
+
// Descriptor-driven (ADR-1239 / #2092): the runtime's capability title.
|
|
2361
|
+
const runtimeLabel = _capabilityTitle(runtime);
|
|
2362
|
+
for (const row of tables.extendedRows) {
|
|
2363
|
+
if (!extendedEvents.includes(row.event))
|
|
2364
|
+
continue;
|
|
2365
|
+
if (!Array.isArray(settings.hooks[row.event])) {
|
|
2366
|
+
settings.hooks[row.event] = [];
|
|
2568
2367
|
}
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
if (!settings.hooks.FileChanged) {
|
|
2583
|
-
settings.hooks.FileChanged = [];
|
|
2584
|
-
}
|
|
2585
|
-
const configReloadFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-config-reload.js');
|
|
2586
|
-
const alreadyHasConfigReload = settings.hooks.FileChanged.some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-config-reload')));
|
|
2587
|
-
if (!alreadyHasConfigReload && node_fs_1.default.existsSync(configReloadFile) && configReloadCommand) {
|
|
2588
|
-
settings.hooks.FileChanged.push({
|
|
2589
|
-
matcher: 'config.json',
|
|
2590
|
-
hooks: [
|
|
2591
|
-
{
|
|
2592
|
-
type: 'command',
|
|
2593
|
-
command: configReloadCommand,
|
|
2594
|
-
timeout: 8
|
|
2595
|
-
}
|
|
2596
|
-
]
|
|
2597
|
-
});
|
|
2598
|
-
console.log(` ${green}✓${reset} Configured FileChanged config-reload hook (Claude Code)`);
|
|
2599
|
-
}
|
|
2600
|
-
else if (!alreadyHasConfigReload && !node_fs_1.default.existsSync(configReloadFile)) {
|
|
2601
|
-
console.warn(` ${yellow}⚠${reset} Skipped FileChanged hook — gsd-config-reload.js not found at target`);
|
|
2602
|
-
}
|
|
2603
|
-
else if (!alreadyHasConfigReload && !configReloadCommand) {
|
|
2604
|
-
console.warn(` ${yellow}⚠${reset} Skipped FileChanged hook — Node executable path unavailable`);
|
|
2605
|
-
}
|
|
2368
|
+
const entries = settings.hooks[row.event];
|
|
2369
|
+
if (entries.some((entry) => groupReferencesHook(entry, hookStem(row.file))))
|
|
2370
|
+
continue;
|
|
2371
|
+
const command = opts[row.command.opts];
|
|
2372
|
+
if (!node_fs_1.default.existsSync(node_path_1.default.join(targetDir, 'hooks', row.file))) {
|
|
2373
|
+
console.warn(` ${yellow}⚠${reset} Skipped ${row.event} hook — ${row.file} not found at target`);
|
|
2374
|
+
}
|
|
2375
|
+
else if (command) {
|
|
2376
|
+
pushHookGroup(entries, row.matcher, command, row.timeout);
|
|
2377
|
+
console.log(` ${green}✓${reset} ${row.configuredMessage}${row.runtimeLabelSuffix ? ` (${runtimeLabel})` : ''}`);
|
|
2378
|
+
}
|
|
2379
|
+
else if (row.noCommandMessage) {
|
|
2380
|
+
console.warn(` ${yellow}⚠${reset} Skipped ${row.event} hook — ${row.noCommandMessage}`);
|
|
2606
2381
|
}
|
|
2607
|
-
// ── end FileChanged hook ────────────────────────────────────────────────────
|
|
2608
2382
|
}
|
|
2609
|
-
/* eslint-enable @typescript-eslint/no-unsafe-member-access,
|
|
2610
|
-
@typescript-eslint/no-unsafe-call,
|
|
2611
|
-
@typescript-eslint/no-unsafe-assignment */
|
|
2612
2383
|
}
|
|
2384
|
+
function applySettingsJsonHooks(settings, opts) {
|
|
2385
|
+
applySettingsJsonHookTables(settings, opts, { rows: SETTINGS_JSON_HOOK_ROWS, extendedRows: SETTINGS_JSON_EXTENDED_ROWS });
|
|
2386
|
+
}
|
|
2387
|
+
/* eslint-enable @typescript-eslint/no-unsafe-member-access,
|
|
2388
|
+
@typescript-eslint/no-unsafe-call,
|
|
2389
|
+
@typescript-eslint/no-unsafe-assignment,
|
|
2390
|
+
@typescript-eslint/no-explicit-any */
|
|
2613
2391
|
// ---------------------------------------------------------------------------
|
|
2614
2392
|
// Kimi hooks.toml (#2095 EoS/kimi Upgrade 1 — native hook bus)
|
|
2615
2393
|
//
|
|
@@ -2682,13 +2460,13 @@ function buildKimiHooksTomlBlock(targetDir, opts) {
|
|
|
2682
2460
|
{ event: 'SessionStart', command: cmd('gsd-check-update.js') },
|
|
2683
2461
|
{ event: 'SessionStart', command: cmd('gsd-session-state.sh') },
|
|
2684
2462
|
// PreToolUse
|
|
2685
|
-
{ event: 'PreToolUse', command: cmd('gsd-prompt-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout:
|
|
2463
|
+
{ event: 'PreToolUse', command: cmd('gsd-prompt-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: kimiBlockingGuardTimeout('gsd-prompt-guard.js') },
|
|
2686
2464
|
{ event: 'PreToolUse', command: cmd('gsd-read-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
|
|
2687
|
-
{ event: 'PreToolUse', command: cmd('gsd-worktree-path-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout:
|
|
2688
|
-
{ event: 'PreToolUse', command: cmd('gsd-write-guard.js'), matcher: 'WriteFile', timeout:
|
|
2689
|
-
{ event: 'PreToolUse', command: cmd('gsd-secret-read-guard.js'), matcher: 'ReadFile|Grep|Shell', timeout:
|
|
2690
|
-
{ event: 'PreToolUse', command: cmd('gsd-workflow-guard.js'), matcher: 'Shell|WriteFile|StrReplaceFile', timeout:
|
|
2691
|
-
{ event: 'PreToolUse', command: cmd('gsd-validate-commit.sh'), matcher: 'Shell', timeout:
|
|
2465
|
+
{ event: 'PreToolUse', command: cmd('gsd-worktree-path-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: kimiBlockingGuardTimeout('gsd-worktree-path-guard.js') },
|
|
2466
|
+
{ event: 'PreToolUse', command: cmd('gsd-write-guard.js'), matcher: 'WriteFile', timeout: kimiBlockingGuardTimeout('gsd-write-guard.js') },
|
|
2467
|
+
{ event: 'PreToolUse', command: cmd('gsd-secret-read-guard.js'), matcher: 'ReadFile|Grep|Shell', timeout: kimiBlockingGuardTimeout('gsd-secret-read-guard.js') },
|
|
2468
|
+
{ event: 'PreToolUse', command: cmd('gsd-workflow-guard.js'), matcher: 'Shell|WriteFile|StrReplaceFile', timeout: kimiBlockingGuardTimeout('gsd-workflow-guard.js') },
|
|
2469
|
+
{ event: 'PreToolUse', command: cmd('gsd-validate-commit.sh'), matcher: 'Shell', timeout: kimiBlockingGuardTimeout('gsd-validate-commit.sh') },
|
|
2692
2470
|
// PostToolUse
|
|
2693
2471
|
{ event: 'PostToolUse', command: cmd('gsd-context-monitor.js'), timeout: 10 },
|
|
2694
2472
|
{ event: 'PostToolUse', command: cmd('gsd-phase-boundary.sh'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
|
|
@@ -2761,13 +2539,21 @@ function stripKimiHooksTomlBlock(content) {
|
|
|
2761
2539
|
* touch the file's mtime for no reason.
|
|
2762
2540
|
*/
|
|
2763
2541
|
function writeKimiHooksToml(configPath, targetDir, opts) {
|
|
2542
|
+
const configuredEntrypoints = [];
|
|
2543
|
+
const trackedOpts = {
|
|
2544
|
+
hookOpts: {
|
|
2545
|
+
...opts.hookOpts,
|
|
2546
|
+
configPath,
|
|
2547
|
+
configuredEntrypoints,
|
|
2548
|
+
},
|
|
2549
|
+
};
|
|
2764
2550
|
const existing = node_fs_1.default.existsSync(configPath) ? node_fs_1.default.readFileSync(configPath, 'utf8') : '';
|
|
2765
2551
|
const stripped = stripKimiHooksTomlBlock(existing) ?? '';
|
|
2766
|
-
const block = buildKimiHooksTomlBlock(targetDir,
|
|
2552
|
+
const block = buildKimiHooksTomlBlock(targetDir, trackedOpts);
|
|
2767
2553
|
const entryCount = block ? (block.match(/\[\[hooks\]\]/g) || []).length : 0;
|
|
2768
2554
|
if (!block) {
|
|
2769
2555
|
if (stripped === existing)
|
|
2770
|
-
return { changed: false, path: configPath, entryCount: 0 };
|
|
2556
|
+
return { changed: false, path: configPath, entryCount: 0, configuredEntrypoints };
|
|
2771
2557
|
if (stripped.trim() === '') {
|
|
2772
2558
|
if (node_fs_1.default.existsSync(configPath))
|
|
2773
2559
|
node_fs_1.default.unlinkSync(configPath);
|
|
@@ -2776,15 +2562,15 @@ function writeKimiHooksToml(configPath, targetDir, opts) {
|
|
|
2776
2562
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(configPath), { recursive: true });
|
|
2777
2563
|
atomicWriteFileSync(configPath, stripped, 'utf8');
|
|
2778
2564
|
}
|
|
2779
|
-
return { changed: true, path: configPath, entryCount: 0 };
|
|
2565
|
+
return { changed: true, path: configPath, entryCount: 0, configuredEntrypoints };
|
|
2780
2566
|
}
|
|
2781
2567
|
const separator = stripped.trim() === '' ? '' : (stripped.endsWith('\n') ? '\n' : '\n\n');
|
|
2782
2568
|
const next = stripped.trim() === '' ? `${block}\n` : `${stripped}${separator}${block}\n`;
|
|
2783
2569
|
if (next === existing)
|
|
2784
|
-
return { changed: false, path: configPath, entryCount };
|
|
2570
|
+
return { changed: false, path: configPath, entryCount, configuredEntrypoints };
|
|
2785
2571
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(configPath), { recursive: true });
|
|
2786
2572
|
atomicWriteFileSync(configPath, next, 'utf8');
|
|
2787
|
-
return { changed: true, path: configPath, entryCount };
|
|
2573
|
+
return { changed: true, path: configPath, entryCount, configuredEntrypoints };
|
|
2788
2574
|
}
|
|
2789
2575
|
/**
|
|
2790
2576
|
* Uninstall-time counterpart to writeKimiHooksToml: strips the GSD block and
|
|
@@ -2806,6 +2592,45 @@ function removeKimiHooksToml(configPath) {
|
|
|
2806
2592
|
return { changed: true };
|
|
2807
2593
|
}
|
|
2808
2594
|
// ---------------------------------------------------------------------------
|
|
2595
|
+
// Blocking-guard host budget (#3981, #5180)
|
|
2596
|
+
//
|
|
2597
|
+
// Claude Code treats a timed-out hook as NON-blocking — the tool call
|
|
2598
|
+
// continues through the normal permission flow. The blocking PreToolUse
|
|
2599
|
+
// guards therefore need a budget a host stall cannot exceed, not one sized to
|
|
2600
|
+
// the hook's own ~0.1 s runtime. Observed stalls reached 84.3 s; 120 s is the
|
|
2601
|
+
// top of the issue's prescribed 60–120 range and returns every observed
|
|
2602
|
+
// verdict. ONE constant and ONE name list, shared by every registration
|
|
2603
|
+
// surface that carries a host timeout (settings.json install + migration,
|
|
2604
|
+
// Kimi config.toml) so a future change cannot diverge between them.
|
|
2605
|
+
// ---------------------------------------------------------------------------
|
|
2606
|
+
const BLOCKING_GUARD_TIMEOUT_S = 120;
|
|
2607
|
+
const BLOCKING_GUARD_NAMES = [
|
|
2608
|
+
'gsd-prompt-guard',
|
|
2609
|
+
'gsd-workflow-guard',
|
|
2610
|
+
'gsd-worktree-path-guard',
|
|
2611
|
+
'gsd-agent-isolation-guard',
|
|
2612
|
+
'gsd-write-guard',
|
|
2613
|
+
'gsd-secret-read-guard',
|
|
2614
|
+
'gsd-validate-commit',
|
|
2615
|
+
];
|
|
2616
|
+
// Kimi registers every blocking guard EXCEPT the ones named here: Claude's
|
|
2617
|
+
// Agent|Task (subagent-dispatch) matcher segment has no confirmed Kimi tool
|
|
2618
|
+
// name, so gsd-agent-isolation-guard has nothing to attach to there (see the
|
|
2619
|
+
// buildKimiHooksTomlBlock doc). The divergence from the installer's set is
|
|
2620
|
+
// intentional and explicit, not an accident of two hand-kept lists.
|
|
2621
|
+
const KIMI_UNREGISTERED_BLOCKING_GUARDS = ['gsd-agent-isolation-guard'];
|
|
2622
|
+
const KIMI_BLOCKING_GUARD_NAMES = BLOCKING_GUARD_NAMES.filter((name) => !KIMI_UNREGISTERED_BLOCKING_GUARDS.includes(name));
|
|
2623
|
+
// Host budget for a Kimi blocking-guard entry, by script file name. Throws for
|
|
2624
|
+
// a script outside KIMI_BLOCKING_GUARD_NAMES so a spec cannot claim the
|
|
2625
|
+
// blocking budget for a hook the shared list does not name.
|
|
2626
|
+
function kimiBlockingGuardTimeout(scriptFile) {
|
|
2627
|
+
const name = scriptFile.replace(/\.(?:js|sh)$/, '');
|
|
2628
|
+
if (!KIMI_BLOCKING_GUARD_NAMES.includes(name)) {
|
|
2629
|
+
throw new Error(`runtime-hooks-surface: ${scriptFile} is not a Kimi blocking guard (KIMI_BLOCKING_GUARD_NAMES)`);
|
|
2630
|
+
}
|
|
2631
|
+
return BLOCKING_GUARD_TIMEOUT_S;
|
|
2632
|
+
}
|
|
2633
|
+
// ---------------------------------------------------------------------------
|
|
2809
2634
|
// referencesHook
|
|
2810
2635
|
//
|
|
2811
2636
|
// Pure predicate — checks whether a hook entry object references a managed
|
|
@@ -2830,6 +2655,68 @@ function referencesHook(h, hookName) {
|
|
|
2830
2655
|
(Array.isArray(args) && args.some(a => typeof a === 'string' && a.includes(hookName))) ||
|
|
2831
2656
|
(typeof url === 'string' && url.includes(hookName));
|
|
2832
2657
|
}
|
|
2658
|
+
function validateConfiguredEntrypoints(entries, deps = {}) {
|
|
2659
|
+
const statSync = deps.statSync ?? node_fs_1.default.statSync;
|
|
2660
|
+
const accessSync = deps.accessSync ?? node_fs_1.default.accessSync;
|
|
2661
|
+
const resolve = deps.resolveExecutableBinary ?? resolveExecutableBinary;
|
|
2662
|
+
const invalid = [];
|
|
2663
|
+
for (const entry of entries) {
|
|
2664
|
+
let scriptOk = false;
|
|
2665
|
+
try {
|
|
2666
|
+
scriptOk = statSync(entry.scriptPath).isFile();
|
|
2667
|
+
if (!scriptOk) {
|
|
2668
|
+
invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'script', path: entry.scriptPath, reason: 'wrong-file-type' });
|
|
2669
|
+
}
|
|
2670
|
+
else {
|
|
2671
|
+
// #4249: statSync only needs search permission on the parent dirs, so
|
|
2672
|
+
// it succeeds even for a chmod-000 file — the EACCES catch below
|
|
2673
|
+
// never fires for that case. Read permission on the file itself must
|
|
2674
|
+
// be checked explicitly: an interpreter opens the script directly,
|
|
2675
|
+
// and even a self-executable shebang script is opened and read by
|
|
2676
|
+
// its kernel-invoked interpreter, not just exec'd — X_OK alone does
|
|
2677
|
+
// not prove it's readable.
|
|
2678
|
+
try {
|
|
2679
|
+
accessSync(entry.scriptPath, node_fs_1.default.constants.R_OK);
|
|
2680
|
+
}
|
|
2681
|
+
catch {
|
|
2682
|
+
scriptOk = false;
|
|
2683
|
+
invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'script', path: entry.scriptPath, reason: 'unreadable' });
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
catch (statErr) {
|
|
2688
|
+
// #4249 Nit: EACCES means a parent directory couldn't be searched —
|
|
2689
|
+
// a real (if rare) permission problem, distinct from ENOENT's "missing".
|
|
2690
|
+
// EPERM: Windows' equivalent permission-denied code for a directory a
|
|
2691
|
+
// parent path couldn't be traversed into.
|
|
2692
|
+
const code = statErr?.code;
|
|
2693
|
+
const reason = code === 'EACCES' || code === 'EPERM' ? 'unreadable' : 'missing';
|
|
2694
|
+
invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'script', path: entry.scriptPath, reason });
|
|
2695
|
+
}
|
|
2696
|
+
// #4249: selfExecutable is the sole source of truth for whether the OS
|
|
2697
|
+
// execs scriptPath directly via its own shebang (set explicitly by every
|
|
2698
|
+
// producer that needs it — a Windows-Claude .sh hook, Codex's Windows
|
|
2699
|
+
// .cmd shim, Cline's hybrid `env node` hook — rather than inferred from
|
|
2700
|
+
// the absence of interpreterCandidates, which Cline's hybrid case also
|
|
2701
|
+
// carries). Skip on win32 like resolveExecutableBinary's own X_OK
|
|
2702
|
+
// carve-out does: POSIX mode bits don't mean executable on Windows, and a
|
|
2703
|
+
// real accessSync(X_OK) there would fail a .cmd shim under a test that
|
|
2704
|
+
// simulates win32 on a POSIX runner (Node's own no-op only protects an
|
|
2705
|
+
// actual Windows machine). Cline is the only producer where this runs.
|
|
2706
|
+
if (scriptOk && entry.selfExecutable && (entry.platform ?? process.platform) !== 'win32') {
|
|
2707
|
+
try {
|
|
2708
|
+
accessSync(entry.scriptPath, node_fs_1.default.constants.X_OK);
|
|
2709
|
+
}
|
|
2710
|
+
catch {
|
|
2711
|
+
invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'script', path: entry.scriptPath, reason: 'not-executable' });
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
if (entry.interpreterCandidates && !entry.interpreterCandidates.some(candidate => resolve(candidate, { platform: entry.platform, requireExecutable: true }) !== null)) {
|
|
2715
|
+
invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'interpreter', path: entry.interpreterCandidates.join(' | '), reason: 'unresolved-interpreter' });
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
return invalid.length === 0 ? { ok: true } : { ok: false, invalid };
|
|
2719
|
+
}
|
|
2833
2720
|
module.exports = {
|
|
2834
2721
|
// Cline
|
|
2835
2722
|
buildClineRulesBody,
|
|
@@ -2891,16 +2778,27 @@ module.exports = {
|
|
|
2891
2778
|
removeKimiHooksToml,
|
|
2892
2779
|
KIMI_HOOKS_TOML_MARKER_BEGIN,
|
|
2893
2780
|
KIMI_HOOKS_TOML_MARKER_END,
|
|
2781
|
+
// Blocking-guard host budget (#3981, #5180)
|
|
2782
|
+
BLOCKING_GUARD_TIMEOUT_S,
|
|
2783
|
+
BLOCKING_GUARD_NAMES,
|
|
2784
|
+
KIMI_UNREGISTERED_BLOCKING_GUARDS,
|
|
2785
|
+
KIMI_BLOCKING_GUARD_NAMES,
|
|
2894
2786
|
// Shared
|
|
2895
2787
|
stageTransitiveHookLibs,
|
|
2896
2788
|
buildHookCommand,
|
|
2789
|
+
recordConfiguredHookCommand,
|
|
2897
2790
|
applySettingsJsonHooks,
|
|
2791
|
+
applySettingsJsonHookTables,
|
|
2792
|
+
SETTINGS_JSON_HOOK_ROWS,
|
|
2793
|
+
SETTINGS_JSON_EXTENDED_ROWS,
|
|
2794
|
+
validateConfiguredEntrypoints,
|
|
2898
2795
|
referencesHook,
|
|
2899
2796
|
rewriteLegacyManagedNodeHookCommands,
|
|
2900
2797
|
reconcileManagedShellHookCommands,
|
|
2901
2798
|
normalizeNodePath,
|
|
2902
2799
|
resolveNodeRunner,
|
|
2903
2800
|
buildNodeRunnerChainToken,
|
|
2801
|
+
resolveBashExecutable,
|
|
2904
2802
|
resolveBashRunner,
|
|
2905
2803
|
NODE_RUNNER_RESOLVER_HOOK,
|
|
2906
2804
|
// Atomic write seam (shared with bin/install.js so all writes participate
|