@opengsd/gsd-core 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +12 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +78 -42
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +0 -1
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +3 -1
- package/agents/gsd-plan-checker.md +91 -112
- package/agents/gsd-planner.md +20 -4
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +82 -7
- package/agents/gsd-ui-researcher.md +70 -3
- package/agents/gsd-verifier.md +24 -2
- package/bin/install.js +847 -200
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/quick.md +8 -4
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +761 -100
- package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
- package/gsd-core/bin/lib/api-coverage.cjs +30 -9
- package/gsd-core/bin/lib/artifacts.cjs +2 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
- package/gsd-core/bin/lib/audit.cjs +163 -41
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +785 -144
- package/gsd-core/bin/lib/capability-state.cjs +25 -4
- package/gsd-core/bin/lib/capability-validator.cjs +321 -18
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +229 -6
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +877 -54
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +121 -29
- package/gsd-core/bin/lib/config.cjs +92 -2
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +118 -14
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +23 -2
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +975 -326
- package/gsd-core/bin/lib/gap-checker.cjs +41 -8
- package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
- package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
- package/gsd-core/bin/lib/host-integration.cjs +96 -11
- package/gsd-core/bin/lib/init-command-router.cjs +132 -21
- package/gsd-core/bin/lib/init.cjs +252 -56
- package/gsd-core/bin/lib/install-engine.cjs +252 -15
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
- package/gsd-core/bin/lib/install-profiles.cjs +100 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +195 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +175 -4
- package/gsd-core/bin/lib/milestone.cjs +112 -7
- package/gsd-core/bin/lib/model-catalog.cjs +177 -19
- package/gsd-core/bin/lib/model-resolver.cjs +10 -28
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +321 -13
- package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
- package/gsd-core/bin/lib/phase-locator.cjs +138 -17
- package/gsd-core/bin/lib/phase.cjs +1175 -115
- package/gsd-core/bin/lib/plan-document.cjs +273 -0
- package/gsd-core/bin/lib/plan-scan.cjs +13 -2
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +165 -34
- package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
- package/gsd-core/bin/lib/probe-core.cjs +4 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
- package/gsd-core/bin/lib/roadmap.cjs +248 -64
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -31
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +47 -18
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +216 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
- package/gsd-core/bin/lib/state-transition.cjs +850 -145
- package/gsd-core/bin/lib/state.cjs +1629 -287
- package/gsd-core/bin/lib/surface.cjs +33 -10
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +2542 -387
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
- package/gsd-core/bin/lib/unusable-input.cjs +13 -0
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/vendor/README.md +43 -5
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -13
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +441 -56
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +6 -5
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +37 -19
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/edge-probe.md +17 -5
- package/gsd-core/references/execute-mvp-tdd.md +18 -18
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +3 -3
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +22 -0
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/mvp-concepts.md +2 -2
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +76 -3
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +39 -9
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +15 -12
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +2 -2
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +11 -11
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/phase-prompt.md +7 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +3 -1
- package/gsd-core/workflows/add-phase.md +5 -3
- package/gsd-core/workflows/add-tests.md +4 -9
- package/gsd-core/workflows/add-todo.md +2 -2
- package/gsd-core/workflows/ai-integration-phase.md +5 -10
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +14 -3
- package/gsd-core/workflows/audit-milestone.md +11 -9
- package/gsd-core/workflows/audit-uat.md +19 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +12 -26
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +3 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
- package/gsd-core/workflows/code-review-fix.md +3 -1
- package/gsd-core/workflows/code-review.md +192 -69
- package/gsd-core/workflows/complete-milestone.md +28 -14
- package/gsd-core/workflows/debug.md +6 -4
- package/gsd-core/workflows/diagnose-issues.md +17 -7
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +46 -19
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +3 -1
- package/gsd-core/workflows/eval-review.md +5 -10
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +29 -5
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +4 -2
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +68 -66
- package/gsd-core/workflows/execute-plan.md +25 -20
- package/gsd-core/workflows/explore.md +3 -1
- package/gsd-core/workflows/extract-learnings.md +3 -1
- package/gsd-core/workflows/fast.md +8 -2
- package/gsd-core/workflows/forensics.md +3 -1
- package/gsd-core/workflows/graduation.md +6 -6
- package/gsd-core/workflows/health.md +4 -7
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +17 -14
- package/gsd-core/workflows/inbox.md +5 -6
- package/gsd-core/workflows/ingest-docs.md +45 -12
- package/gsd-core/workflows/insert-phase.md +7 -5
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +7 -3
- package/gsd-core/workflows/list-workspaces.md +3 -1
- package/gsd-core/workflows/manager.md +15 -26
- package/gsd-core/workflows/map-codebase.md +3 -1
- package/gsd-core/workflows/milestone-summary.md +3 -1
- package/gsd-core/workflows/mvp-phase.md +3 -3
- package/gsd-core/workflows/new-milestone.md +10 -22
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +17 -29
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +4 -2
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +20 -5
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
- package/gsd-core/workflows/plan-phase.md +251 -54
- package/gsd-core/workflows/plan-review-convergence.md +148 -19
- package/gsd-core/workflows/plant-seed.md +3 -3
- package/gsd-core/workflows/pr-branch.md +195 -51
- package/gsd-core/workflows/profile-user.md +17 -15
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +52 -15
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +33 -32
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +6 -4
- package/gsd-core/workflows/remove-workspace.md +3 -3
- package/gsd-core/workflows/resume-project.md +14 -14
- package/gsd-core/workflows/review.md +404 -21
- package/gsd-core/workflows/scan.md +3 -1
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +9 -9
- package/gsd-core/workflows/settings-integrations.md +66 -32
- package/gsd-core/workflows/settings.md +4 -6
- package/gsd-core/workflows/ship.md +22 -16
- package/gsd-core/workflows/sketch-wrap-up.md +13 -17
- package/gsd-core/workflows/sketch.md +13 -19
- package/gsd-core/workflows/smart-entry.md +4 -6
- package/gsd-core/workflows/spec-phase.md +31 -4
- package/gsd-core/workflows/spike-wrap-up.md +9 -11
- package/gsd-core/workflows/spike.md +21 -32
- package/gsd-core/workflows/stats.md +4 -2
- package/gsd-core/workflows/sync-skills.md +13 -5
- package/gsd-core/workflows/thread.md +13 -7
- package/gsd-core/workflows/transition.md +7 -5
- package/gsd-core/workflows/ui-phase.md +36 -21
- package/gsd-core/workflows/ui-review.md +7 -11
- package/gsd-core/workflows/ultraplan-phase.md +7 -13
- package/gsd-core/workflows/undo.md +9 -17
- package/gsd-core/workflows/update.md +47 -48
- package/gsd-core/workflows/validate-phase.md +3 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +106 -21
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +302 -22
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +77 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +46 -12
- package/hooks/dist/gsd-read-guard.js +18 -7
- package/hooks/dist/gsd-read-injection-scanner.js +22 -13
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +222 -29
- package/hooks/dist/gsd-validate-commit.sh +523 -12
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +36 -17
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +4 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +302 -22
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +3 -1
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +28 -23
- package/hooks/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +77 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +46 -12
- package/hooks/gsd-read-guard.js +18 -7
- package/hooks/gsd-read-injection-scanner.js +22 -13
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +222 -29
- package/hooks/gsd-validate-commit.sh +523 -12
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +36 -17
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/hooks.json +6 -0
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +4 -0
- package/package.json +14 -9
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +12 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +52 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +406 -0
- package/scripts/gen-capability-registry.cjs +8 -6
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-loop-host-contract.cjs +189 -4
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +32 -2
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +45 -14
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-response-language-coverage.cjs +524 -0
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +124 -4
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +51 -64
- package/scripts/lint-vendored-deps.cjs +208 -35
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +83 -14
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +22 -3
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -109
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/state-write-path-drift-baseline.json +0 -19
|
@@ -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, } = shellCmdProjection;
|
|
41
|
+
const { isManagedHookBasename, isManagedHookCommand, projectLegacySettingsHookCommand, projectManagedHookCommand, projectPortableHookBaseDir, projectCodexHookTomlCommand, shellHookOmitsBashRunner, escapeTomlDoubleQuotedString, escapePosixDoubleQuoted, } = shellCmdProjection;
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
43
43
|
// Terminal color constants (mirrors install.js for console output parity)
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
@@ -136,23 +136,64 @@ function _capabilityTitle(runtime) {
|
|
|
136
136
|
let __atomicWriteCounter = 0;
|
|
137
137
|
// Set<string> — absolute paths of .tmp-<pid>-<n> files this process created.
|
|
138
138
|
const __atomicWrittenTmps = new Set();
|
|
139
|
+
// Retry budget for the EEXIST (squatted temp path) branch in atomicWriteFileSync.
|
|
140
|
+
const MAX_TEMP_FILE_ATTEMPTS = 4;
|
|
139
141
|
function atomicWriteFileSync(target, data, options) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
// A pre-existing target's permission bits must survive the rewrite:
|
|
143
|
+
// rename() swaps the temp file's inode into place, so without an explicit
|
|
144
|
+
// carry a user-hardened chmod (e.g. 600 on a secrets-bearing settings.json)
|
|
145
|
+
// would silently reset to the umask default.
|
|
146
|
+
let priorMode;
|
|
143
147
|
try {
|
|
144
|
-
node_fs_1.default.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
const st = node_fs_1.default.statSync(target);
|
|
149
|
+
if (st.isFile())
|
|
150
|
+
priorMode = st.mode & 0o7777;
|
|
151
|
+
}
|
|
152
|
+
catch { /* no pre-existing target: default creation mode applies */ }
|
|
153
|
+
// 'wx' (O_EXCL) refuses to follow a symlink pre-planted at the predictable
|
|
154
|
+
// temp path and refuses to reuse a foreign file already sitting there; on
|
|
155
|
+
// EEXIST the write retries under a fresh counter value.
|
|
156
|
+
const exclusiveOptions = typeof options === 'string' || options == null
|
|
157
|
+
? { encoding: options ?? null, flag: 'wx' }
|
|
158
|
+
: { ...options, flag: 'wx' };
|
|
159
|
+
for (let attempt = 0;; attempt++) {
|
|
160
|
+
__atomicWriteCounter += 1;
|
|
161
|
+
const tmp = `${target}.tmp-${process.pid}-${__atomicWriteCounter}`;
|
|
162
|
+
__atomicWrittenTmps.add(tmp);
|
|
151
163
|
try {
|
|
152
|
-
node_fs_1.default.
|
|
164
|
+
node_fs_1.default.writeFileSync(tmp, data, exclusiveOptions);
|
|
153
165
|
}
|
|
154
|
-
catch {
|
|
155
|
-
|
|
166
|
+
catch (e) {
|
|
167
|
+
if (e.code === 'EEXIST') {
|
|
168
|
+
// The file at tmp is not ours — never rmSync it.
|
|
169
|
+
if (attempt < MAX_TEMP_FILE_ATTEMPTS)
|
|
170
|
+
continue;
|
|
171
|
+
throw e;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
node_fs_1.default.rmSync(tmp, { force: true });
|
|
175
|
+
}
|
|
176
|
+
catch { /* ignore */ }
|
|
177
|
+
throw e;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
// chmod rather than options.mode: open(2) masks mode with the process
|
|
181
|
+
// umask, chmod applies the preserved bits exactly.
|
|
182
|
+
if (priorMode !== undefined)
|
|
183
|
+
node_fs_1.default.chmodSync(tmp, priorMode);
|
|
184
|
+
shellCmdProjection.retryRenameSync(tmp, target);
|
|
185
|
+
// Successful rename: the tmp path no longer exists, but leave it in the
|
|
186
|
+
// Set so _cleanTmpFiles can recognise it as installer-owned if it somehow
|
|
187
|
+
// lingers (e.g. a rename succeeded but left a stale entry on some FS).
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
try {
|
|
191
|
+
node_fs_1.default.rmSync(tmp, { force: true });
|
|
192
|
+
}
|
|
193
|
+
catch { /* ignore */ }
|
|
194
|
+
throw e;
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
156
197
|
}
|
|
157
198
|
}
|
|
158
199
|
// ---------------------------------------------------------------------------
|
|
@@ -333,20 +374,41 @@ function parseTomlValue(text, i) {
|
|
|
333
374
|
}
|
|
334
375
|
throw new Error(`parseTomlValue: unexpected character '${ch}' at position ${i}`);
|
|
335
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Normalize a directory that will be joined with `/…` — posix separators, no
|
|
379
|
+
* trailing slash. `FNM_DIR=/custom/fnm/` would otherwise bake
|
|
380
|
+
* `/custom/fnm//aliases/default/bin/node` (#3704 review). Cosmetic — `existsSync`
|
|
381
|
+
* resolves the doubled separator and every shell collapses it — but the value is
|
|
382
|
+
* written into a user's settings.json and read by humans.
|
|
383
|
+
*
|
|
384
|
+
* Shared by both fnm branches deliberately: they build the same alias paths from
|
|
385
|
+
* different roots, and a trim applied to only one is a difference with no reason
|
|
386
|
+
* behind it.
|
|
387
|
+
*/
|
|
388
|
+
function normalizeRootDir(dir) {
|
|
389
|
+
return shellCmdProjection.posixNormalize(dir).replace(/\/+$/, '');
|
|
390
|
+
}
|
|
336
391
|
function normalizeNodePath(execPath, opts) {
|
|
337
392
|
if (!execPath)
|
|
338
393
|
return execPath;
|
|
339
394
|
const env = (opts && opts.env) || process.env;
|
|
340
395
|
const existsSync = (opts && opts.existsSync) || node_fs_1.default.existsSync;
|
|
341
396
|
const normalizedForMatch = shellCmdProjection.posixNormalize(execPath);
|
|
342
|
-
|
|
397
|
+
// #977: fnm's Windows shim IS `process.execPath` there — Windows does not
|
|
398
|
+
// realpath through it — so this branch stays. #3704 adds `(?:bin\/)?`: fnm's
|
|
399
|
+
// POSIX shim is `<multishell>/bin/node`, so the original pattern could not match
|
|
400
|
+
// it even when a caller hands one in explicitly (#3662's `execPath` option
|
|
401
|
+
// exists to do exactly that, normalizing a path from another environment).
|
|
402
|
+
if (/\/fnm_multishells\/[0-9]+_[0-9]+\/(?:bin\/)?node(\.exe)?$/i.test(normalizedForMatch)) {
|
|
343
403
|
const candidates = [];
|
|
344
404
|
if (env.FNM_DIR) {
|
|
345
|
-
|
|
346
|
-
candidates.push(`${
|
|
405
|
+
const fnmRoot = normalizeRootDir(env.FNM_DIR);
|
|
406
|
+
candidates.push(`${fnmRoot}/aliases/default/node.exe`);
|
|
407
|
+
candidates.push(`${fnmRoot}/aliases/default/bin/node`);
|
|
347
408
|
}
|
|
348
|
-
|
|
349
|
-
|
|
409
|
+
const appdata = shellCmdProjection.envGet(env, 'APPDATA');
|
|
410
|
+
if (appdata) {
|
|
411
|
+
candidates.push(`${normalizeRootDir(appdata)}/fnm/aliases/default/node.exe`);
|
|
350
412
|
}
|
|
351
413
|
for (const candidate of candidates) {
|
|
352
414
|
if (candidate && existsSync(candidate))
|
|
@@ -354,6 +416,42 @@ function normalizeNodePath(execPath, opts) {
|
|
|
354
416
|
}
|
|
355
417
|
return execPath;
|
|
356
418
|
}
|
|
419
|
+
// #3704: fnm pins a concrete version at
|
|
420
|
+
// <FNM_DIR>/node-versions/<ver>/installation/bin/node (Windows:
|
|
421
|
+
// .../installation/node.exe). On macOS/Linux this — not the shim above — is what
|
|
422
|
+
// `process.execPath` reports, because Node realpaths through
|
|
423
|
+
// `fnm_multishells`. So the shim branch above is unreachable on POSIX and the
|
|
424
|
+
// raw versioned path was baked into every managed hook: `fnm uninstall <ver>`,
|
|
425
|
+
// or fnm's own pruning, then 404s every hook — the ephemeral-path failure #977
|
|
426
|
+
// exists to prevent, and the same one #1619 (mise) and #2185 (Homebrew) fixed
|
|
427
|
+
// for their managers by matching the VERSIONED path rather than a shim.
|
|
428
|
+
//
|
|
429
|
+
// The stable alias is <FNM_DIR>/aliases/default/..., which fnm repoints on
|
|
430
|
+
// `fnm default`. Derive <FNM_DIR> from execPath first (#2185's rule — the path
|
|
431
|
+
// IS the install location, and FNM_DIR may be unset or point at a different
|
|
432
|
+
// install), keeping the env as a secondary candidate. Rewrite only when the
|
|
433
|
+
// alias exists; otherwise fall through to the raw execPath, exactly like the
|
|
434
|
+
// mise and volta branches, so a rewrite never turns a stale-but-working pin
|
|
435
|
+
// into an immediately broken one.
|
|
436
|
+
const fnmVersioned = normalizedForMatch.match(/^(.*)\/node-versions\/[^/]+\/installation\/(?:bin\/)?node(\.exe)?$/i);
|
|
437
|
+
if (fnmVersioned) {
|
|
438
|
+
const isExe = Boolean(fnmVersioned[2]);
|
|
439
|
+
const roots = [fnmVersioned[1]];
|
|
440
|
+
if (env.FNM_DIR)
|
|
441
|
+
roots.push(normalizeRootDir(env.FNM_DIR));
|
|
442
|
+
const fnmAliasCandidates = [];
|
|
443
|
+
for (const root of roots) {
|
|
444
|
+
// Probe the spelling matching the input first, then the other — a layout
|
|
445
|
+
// is one or the other, and guessing wrong would skip a real alias.
|
|
446
|
+
const leaf = isExe ? ['node.exe', 'bin/node'] : ['bin/node', 'node.exe'];
|
|
447
|
+
for (const tail of leaf)
|
|
448
|
+
fnmAliasCandidates.push(`${root}/aliases/default/${tail}`);
|
|
449
|
+
}
|
|
450
|
+
for (const candidate of fnmAliasCandidates) {
|
|
451
|
+
if (existsSync(candidate))
|
|
452
|
+
return candidate;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
357
455
|
// Homebrew (macOS Intel /usr/local, Apple Silicon /opt/homebrew, Linuxbrew
|
|
358
456
|
// /home/linuxbrew/.linuxbrew, and any custom HOMEBREW_PREFIX) pins node at
|
|
359
457
|
// <prefix>/Cellar/node(<@ver>)?/<ver>/bin/node, then deletes prior versions on
|
|
@@ -399,12 +497,76 @@ function normalizeNodePath(execPath, opts) {
|
|
|
399
497
|
return execPath;
|
|
400
498
|
}
|
|
401
499
|
function resolveNodeRunner(opts) {
|
|
402
|
-
const execPath = typeof process.execPath === 'string' ? process.execPath : '';
|
|
500
|
+
const execPath = (opts && opts.execPath) || (typeof process.execPath === 'string' ? process.execPath : '');
|
|
403
501
|
if (!execPath)
|
|
404
502
|
return null;
|
|
405
503
|
const stablePath = normalizeNodePath(execPath, opts);
|
|
406
504
|
return JSON.stringify(shellCmdProjection.posixNormalize(stablePath));
|
|
407
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* #3662 — the runtime-resolving node runner token for managed JS hooks.
|
|
508
|
+
*
|
|
509
|
+
* A bake-time absolute runner (`resolveNodeRunner`) only works in the
|
|
510
|
+
* environment that ran the installer; a config root shared across
|
|
511
|
+
* environments (the `--portable-hooks` scenario, or any settings.json under a
|
|
512
|
+
* mounted `$HOME`) carries a path that 404s with exit 127 everywhere else.
|
|
513
|
+
* This token is a POSIX `sh` command substitution that resolves node at
|
|
514
|
+
* hook-fire time, trying IN ORDER:
|
|
515
|
+
*
|
|
516
|
+
* 1. the baked installer path (absolute — keeps the #2979/#3002/#3017/#3022
|
|
517
|
+
* minimal-PATH guarantee: where the baked path exists it still wins,
|
|
518
|
+
* under any PATH, GUI launch included);
|
|
519
|
+
* 2. `command -v node` (quoted — one word even with spaces in the result);
|
|
520
|
+
* 3. the well-known stable layouts (`/usr/local/bin/node`, `/usr/bin/node`).
|
|
521
|
+
*
|
|
522
|
+
* The FIRST executable candidate wins; if none resolves the substitution
|
|
523
|
+
* yields an empty word and the hook fails exactly as a stale absolute path
|
|
524
|
+
* does today — no bare `node` token is ever emitted or depended on.
|
|
525
|
+
*
|
|
526
|
+
* One shape for every platform: emitted hook commands execute via POSIX `sh`
|
|
527
|
+
* (Claude-on-win32 runs Git Bash per #166/#580; `hookCommandNeedsPowerShellCallOperator`
|
|
528
|
+
* is an unused opt-in), and the baked path is posixNormalize'd before escaping
|
|
529
|
+
* (escapePosixDoubleQuoted — the Shell Command Projection seam owns quoting).
|
|
530
|
+
* The portable resolver script (hooks/gsd-node-runner.sh) resolves through a
|
|
531
|
+
* SUPERSET of this candidate list — keep the two lists consistent.
|
|
532
|
+
*/
|
|
533
|
+
function buildNodeRunnerChainToken(opts) {
|
|
534
|
+
const execPath = (opts && opts.execPath) || (typeof process.execPath === 'string' ? process.execPath : '');
|
|
535
|
+
if (!execPath)
|
|
536
|
+
return null;
|
|
537
|
+
const stablePath = shellCmdProjection.posixNormalize(normalizeNodePath(execPath, opts));
|
|
538
|
+
const baked = escapePosixDoubleQuoted(stablePath);
|
|
539
|
+
// Absolute candidates only (leading / or a win32 drive letter): a relative
|
|
540
|
+
// `command -v node` hit (legal under a relative PATH entry) must never
|
|
541
|
+
// promote repo-cwd content into the runner slot. The gate uses parameter
|
|
542
|
+
// expansion + [ ] — deliberately NO `case` (its `)` terminates the command
|
|
543
|
+
// substitution under macOS's stock bash 3.2 /bin/sh, breaking the hook).
|
|
544
|
+
// \${…} below stays a literal shell parameter expansion, not TS interpolation.
|
|
545
|
+
return `"$(for n in "${baked}" "$(command -v node)" /usr/local/bin/node /usr/bin/node; do [ -x "$n" ] && { [ "\${n#/}" != "$n" ] || [ "\${n#?:}" != "$n" ]; } && printf '%s' "$n" && break; done)"`;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* #3662 — the install-time node path as a shell-safe QUOTED token, carrying
|
|
549
|
+
* the same double-quote escaping as the chain token (`escapePosixDoubleQuoted`
|
|
550
|
+
* — $ ` " \), NOT bare JSON quoting. The portable resolver's first argument
|
|
551
|
+
* is executed by the host shell before the resolver sees argv, so a path
|
|
552
|
+
* containing shell metacharacters must arrive escaped.
|
|
553
|
+
*/
|
|
554
|
+
function buildBakedNodeToken(opts) {
|
|
555
|
+
const execPath = (opts && opts.execPath) || (typeof process.execPath === 'string' ? process.execPath : '');
|
|
556
|
+
if (!execPath)
|
|
557
|
+
return null;
|
|
558
|
+
const stablePath = shellCmdProjection.posixNormalize(normalizeNodePath(execPath, opts));
|
|
559
|
+
return `"${escapePosixDoubleQuoted(stablePath)}"`;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* #3662 — basename of the portable node resolver staged into the install's
|
|
563
|
+
* hooks/ directory. Under `--portable-hooks`, managed JS hook commands route
|
|
564
|
+
* through it (`bash "<hooks>/gsd-node-runner.sh" "<baked-node>" "<script>.js"`)
|
|
565
|
+
* so the SAME staged file works for every install: the install-time node path
|
|
566
|
+
* travels as the resolver's first argument (tried first — the minimal-PATH
|
|
567
|
+
* guarantee), ahead of `command -v node` and the well-known fallback list.
|
|
568
|
+
*/
|
|
569
|
+
const NODE_RUNNER_RESOLVER_HOOK = 'gsd-node-runner.sh';
|
|
408
570
|
function resolveBashRunner(opts) {
|
|
409
571
|
const platform = (opts && opts.platform) || process.platform;
|
|
410
572
|
if (platform !== 'win32')
|
|
@@ -429,8 +591,13 @@ function resolveBashRunner(opts) {
|
|
|
429
591
|
}
|
|
430
592
|
return null;
|
|
431
593
|
}
|
|
432
|
-
|
|
433
|
-
|
|
594
|
+
// #3662 — recognize the two runtime-resolving command shapes the installer
|
|
595
|
+
// emits, so the rewriter never churns (or un-does) an entry that already
|
|
596
|
+
// works in every environment sharing the config root.
|
|
597
|
+
const CHAIN_RUNNER_COMMAND = /^"\$\(for n in [\s\S]*?printf '%s' "\$n" && break; done\)"\s+\S/;
|
|
598
|
+
const RESOLVER_RUNNER_COMMAND = /^(?:"[^"]*bash(\.exe)?"|bash)\s+"[^"]*gsd-node-runner\.sh"\s+"[^"]*"\s+\S/;
|
|
599
|
+
function rewriteLegacyManagedNodeHookCommands(settings, runnerToken, opts) {
|
|
600
|
+
if (!settings || !settings.hooks || !runnerToken)
|
|
434
601
|
return false;
|
|
435
602
|
if (!opts)
|
|
436
603
|
opts = {};
|
|
@@ -452,29 +619,31 @@ function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
|
|
|
452
619
|
if (hadPowerShellCallOperator) {
|
|
453
620
|
trimmed = trimmed.replace(/^&\s+/, '').trim();
|
|
454
621
|
}
|
|
622
|
+
if (CHAIN_RUNNER_COMMAND.test(trimmed) || RESOLVER_RUNNER_COMMAND.test(trimmed))
|
|
623
|
+
continue;
|
|
455
624
|
const m = trimmed.match(/^node\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/) ||
|
|
456
625
|
trimmed.match(/^("([^"]+)"|'([^']+)'|(\S+))\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/);
|
|
457
626
|
if (!m)
|
|
458
627
|
continue;
|
|
459
|
-
let
|
|
628
|
+
let scriptToken, scriptPath;
|
|
460
629
|
if (/^node\s+/.test(trimmed)) {
|
|
461
|
-
_runnerToken = 'node';
|
|
462
630
|
scriptToken = m[1];
|
|
463
631
|
scriptPath = m[2] || m[3] || m[4] || '';
|
|
464
632
|
}
|
|
465
633
|
else {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
634
|
+
// #3662: the pre-fix two-token shape baked an absolute node path at
|
|
635
|
+
// install time. A foreign-but-stable runner (valid in the
|
|
636
|
+
// environment that wrote it, absent here) used to be SKIPPED — the
|
|
637
|
+
// exact mechanism behind the mixed state where no environment can
|
|
638
|
+
// run all hooks. Every two-token managed entry now re-projects onto
|
|
639
|
+
// the runtime-resolving runner, whatever environment baked it.
|
|
471
640
|
scriptToken = m[5];
|
|
472
641
|
scriptPath = m[6] || m[7] || m[8] || '';
|
|
473
642
|
}
|
|
474
643
|
if (!isManagedHookBasename(scriptPath, { surface: 'settings-json' }))
|
|
475
644
|
continue;
|
|
476
645
|
const projectedCommand = projectLegacySettingsHookCommand({
|
|
477
|
-
|
|
646
|
+
runnerToken,
|
|
478
647
|
scriptPath,
|
|
479
648
|
scriptToken,
|
|
480
649
|
runtime: opts.runtime || 'generic',
|
|
@@ -863,27 +1032,75 @@ function buildHookCommand(configDir, hookName, opts) {
|
|
|
863
1032
|
}
|
|
864
1033
|
return JSON.stringify(shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName);
|
|
865
1034
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1035
|
+
// .sh hooks keep the pre-#3662 shape everywhere: the bash runner resolves
|
|
1036
|
+
// at install time like today, and `bash` itself is a PATH-stable binary
|
|
1037
|
+
// (the absolute Git-Bash discovery covers win32 — #580/#3393).
|
|
1038
|
+
if (isShellHook) {
|
|
1039
|
+
const runner = resolveBashRunner(opts);
|
|
1040
|
+
if (runner === null)
|
|
1041
|
+
return null;
|
|
1042
|
+
if (opts.portableHooks) {
|
|
1043
|
+
const portableBaseDir = projectPortableHookBaseDir({
|
|
1044
|
+
configDir,
|
|
1045
|
+
homeDir: node_os_1.default.homedir(),
|
|
1046
|
+
});
|
|
1047
|
+
return projectManagedHookCommand({
|
|
1048
|
+
absoluteRunner: runner,
|
|
1049
|
+
scriptPath: `${portableBaseDir}/hooks/${hookName}`,
|
|
1050
|
+
runtime: opts.runtime || 'generic',
|
|
1051
|
+
platform,
|
|
1052
|
+
hookShell,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
const hooksPath = shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName;
|
|
1056
|
+
return projectManagedHookCommand({
|
|
1057
|
+
absoluteRunner: runner,
|
|
1058
|
+
scriptPath: hooksPath,
|
|
1059
|
+
runtime,
|
|
1060
|
+
platform,
|
|
1061
|
+
hookShell,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
// JS hooks (#3662): the node runner is resolved at hook-fire time, never
|
|
1065
|
+
// baked as a bare absolute path — an install-environment absolute path is
|
|
1066
|
+
// exactly what breaks with exit 127 when the config root is shared across
|
|
1067
|
+
// environments with different node layouts.
|
|
870
1068
|
if (opts.portableHooks) {
|
|
1069
|
+
// Portable installs route through the staged resolver: the baked absolute
|
|
1070
|
+
// path travels as the resolver's FIRST argument (tried first, so the
|
|
1071
|
+
// minimal-PATH guarantee holds), then `command -v node`, then the
|
|
1072
|
+
// well-known list — one staged file, no per-install templating. The
|
|
1073
|
+
// token is shell-escaped like the chain (the host shell expands the
|
|
1074
|
+
// argument before bash sees argv), not merely JSON-quoted.
|
|
1075
|
+
const bakedToken = buildBakedNodeToken(opts);
|
|
1076
|
+
if (bakedToken === null)
|
|
1077
|
+
return null;
|
|
871
1078
|
const portableBaseDir = projectPortableHookBaseDir({
|
|
872
1079
|
configDir,
|
|
873
1080
|
homeDir: node_os_1.default.homedir(),
|
|
874
1081
|
});
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1082
|
+
// Absolute Git-Bash discovery on win32 when available (#580); `bash` on
|
|
1083
|
+
// PATH otherwise — the same assumption .sh hooks already make.
|
|
1084
|
+
const resolverRunner = resolveBashRunner(opts) || 'bash';
|
|
1085
|
+
return shellCmdProjection.projectShellCommandText({
|
|
1086
|
+
runnerToken: resolverRunner,
|
|
1087
|
+
argTokens: [
|
|
1088
|
+
JSON.stringify(`${portableBaseDir}/hooks/${NODE_RUNNER_RESOLVER_HOOK}`),
|
|
1089
|
+
bakedToken,
|
|
1090
|
+
JSON.stringify(`${portableBaseDir}/hooks/${hookName}`),
|
|
1091
|
+
],
|
|
1092
|
+
runtime,
|
|
879
1093
|
platform,
|
|
880
1094
|
hookShell,
|
|
881
1095
|
});
|
|
882
1096
|
}
|
|
1097
|
+
const chainRunner = buildNodeRunnerChainToken(opts);
|
|
1098
|
+
if (chainRunner === null)
|
|
1099
|
+
return null;
|
|
883
1100
|
const hooksPath = shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName;
|
|
884
|
-
return
|
|
885
|
-
|
|
886
|
-
|
|
1101
|
+
return shellCmdProjection.projectShellCommandText({
|
|
1102
|
+
runnerToken: chainRunner,
|
|
1103
|
+
argTokens: [JSON.stringify(hooksPath)],
|
|
887
1104
|
runtime,
|
|
888
1105
|
platform,
|
|
889
1106
|
hookShell,
|
|
@@ -1094,6 +1311,128 @@ function reconcileCursorHooksJson(hooksJsonPath, managedEntries) {
|
|
|
1094
1311
|
}
|
|
1095
1312
|
return { changed: changed, wrote: shouldWrite, path: hooksJsonPath };
|
|
1096
1313
|
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Stage the `hooks/lib/` helpers a set of staged hook scripts require, walking
|
|
1316
|
+
* the require graph TRANSITIVELY to a fixed point.
|
|
1317
|
+
*
|
|
1318
|
+
* Extracted from writeCursorHooksJson (#3911 review, commit 704859e9c) so the
|
|
1319
|
+
* reduced Codex hook bundle can share one implementation instead of growing a
|
|
1320
|
+
* second, divergent copy (#4087 / #4098). Both reduced bundles hand-pick which
|
|
1321
|
+
* hook SCRIPTS they ship, and neither can hand-pick their helpers correctly for
|
|
1322
|
+
* long: `hooks/lib/hook-exit.js` requires `./cli-exit.js`, which requires
|
|
1323
|
+
* `./exit-code-registry.js` — with NO `./lib/` prefix, because from inside
|
|
1324
|
+
* `lib/` the sibling is already local. A single-pass scan for the `./lib/…`
|
|
1325
|
+
* spelling used FROM a hook script stages hook-exit.js and stops, and the
|
|
1326
|
+
* installed hook then dies on MODULE_NOT_FOUND at load, before its own
|
|
1327
|
+
* try/catch, on every event it is registered for.
|
|
1328
|
+
*
|
|
1329
|
+
* Scans CONTENT rather than paths so a caller can seed from whatever it staged,
|
|
1330
|
+
* transformed or not, without this helper knowing the caller's layout.
|
|
1331
|
+
*
|
|
1332
|
+
* @returns the lib filenames actually staged, in staging order.
|
|
1333
|
+
*/
|
|
1334
|
+
function stageTransitiveHookLibs(opts) {
|
|
1335
|
+
const { seedSources, srcLibDir, destLibDir, runtimeLabel, transform } = opts;
|
|
1336
|
+
const requiredLibFiles = new Set();
|
|
1337
|
+
const scannedLibFiles = new Set();
|
|
1338
|
+
const staged = [];
|
|
1339
|
+
// `./X` means DIFFERENT things depending on where the scanned file lives, and
|
|
1340
|
+
// conflating them stages the wrong file. From a hook SCRIPT in hooks/, a bare
|
|
1341
|
+
// `./X` is a sibling hook-level artifact — Codex's gsd-check-update-worker.js
|
|
1342
|
+
// requires `./managed-hooks-registry.cjs`, which lives in hooks/, not
|
|
1343
|
+
// hooks/lib/ — so only the explicit `./lib/X` spelling is a lib requirement.
|
|
1344
|
+
// From inside a LIB file, the sibling is already local, so `./X` IS a lib
|
|
1345
|
+
// requirement (hook-exit.js -> ./cli-exit.js -> ./exit-code-registry.js); that
|
|
1346
|
+
// is the case 704859e9c added and it must keep working. Cursor never exposed
|
|
1347
|
+
// the difference because none of its staged scripts has a bare sibling
|
|
1348
|
+
// require; Codex's does, and the fail-loud guard below caught it immediately
|
|
1349
|
+
// by demanding managed-hooks-registry.cjs out of hooks/dist/lib.
|
|
1350
|
+
// Fresh per call: a module-level /g regex carries lastIndex across calls and
|
|
1351
|
+
// would silently skip matches on the second install in one process.
|
|
1352
|
+
const seedRequireRe = /require\(\s*['"]\.\/lib\/([A-Za-z0-9._-]+)['"]\s*\)/g;
|
|
1353
|
+
const libRequireRe = /require\(\s*['"]\.\/(?:lib\/)?([A-Za-z0-9._-]+)['"]\s*\)/g;
|
|
1354
|
+
// A NESTED helper path is outside the flat layout hooks/lib/ has and the
|
|
1355
|
+
// build emits, and the character classes above cannot express it — so it
|
|
1356
|
+
// would be a SILENT miss, staging nothing and shipping a hook that dies at
|
|
1357
|
+
// load. Detected separately and refused loudly instead: a silent miss is the
|
|
1358
|
+
// failure mode this whole function exists to remove (review of #4087).
|
|
1359
|
+
const nestedRequireRe = /require\(\s*['"]\.\/lib\/[A-Za-z0-9._-]+\/[^'"]*['"]\s*\)/;
|
|
1360
|
+
const scanForLibRequires = (source, fromLib) => {
|
|
1361
|
+
if (nestedRequireRe.test(source)) {
|
|
1362
|
+
throw new Error(`A staged ${runtimeLabel} hook requires a NESTED hooks/lib path. hooks/lib/ is flat and `
|
|
1363
|
+
+ 'this stager only resolves flat helper names, so the nested helper would never be '
|
|
1364
|
+
+ 'staged and the hook would throw MODULE_NOT_FOUND at load. Flatten the helper or '
|
|
1365
|
+
+ 'extend this stager deliberately.');
|
|
1366
|
+
}
|
|
1367
|
+
const re = fromLib ? libRequireRe : seedRequireRe;
|
|
1368
|
+
re.lastIndex = 0;
|
|
1369
|
+
let m;
|
|
1370
|
+
while ((m = re.exec(source)) !== null) {
|
|
1371
|
+
const candidate = m[1];
|
|
1372
|
+
// A capture with no alphanumeric character is not a module name — it is
|
|
1373
|
+
// prose. This scan reads whole file text, comments included, and
|
|
1374
|
+
// hooks/lib/injection-patterns.js's own header documents this mechanism
|
|
1375
|
+
// with the literal string `require('./lib/...')`, which captures `...`
|
|
1376
|
+
// and would send the resolver hunting for `hooks/lib/...` and fail the
|
|
1377
|
+
// install (measured; that helper is not staged for either reduced bundle
|
|
1378
|
+
// today, so it is latent rather than live).
|
|
1379
|
+
//
|
|
1380
|
+
// KNOWN LIMIT, disclosed rather than papered over: this does NOT make the
|
|
1381
|
+
// scan comment-aware. A comment naming a REAL helper — `require(
|
|
1382
|
+
// './lib/git-cmd.js')` in prose — still registers it and would over-stage
|
|
1383
|
+
// that helper. Closing that needs a comment-stripping pass; the
|
|
1384
|
+
// line-based stripper in scripts/lint-hooks-runtime-build-seam.cjs is the
|
|
1385
|
+
// precedent (its header explains why the naive two-regex strip corrupts
|
|
1386
|
+
// these very files), but promoting a lint-script helper into installer
|
|
1387
|
+
// runtime code is a larger change than this fix.
|
|
1388
|
+
if (!/[A-Za-z0-9]/.test(candidate))
|
|
1389
|
+
continue;
|
|
1390
|
+
requiredLibFiles.add(candidate);
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
for (const source of seedSources)
|
|
1394
|
+
scanForLibRequires(source, false);
|
|
1395
|
+
if (requiredLibFiles.size === 0)
|
|
1396
|
+
return staged;
|
|
1397
|
+
node_fs_1.default.mkdirSync(destLibDir, { recursive: true });
|
|
1398
|
+
// Iterate to a fixed point: staging a lib file can add MORE required lib
|
|
1399
|
+
// files (its own requires), which must themselves be staged and scanned.
|
|
1400
|
+
let libFile = [...requiredLibFiles].find((f) => !scannedLibFiles.has(f));
|
|
1401
|
+
while (libFile !== undefined) {
|
|
1402
|
+
scannedLibFiles.add(libFile);
|
|
1403
|
+
// Node's own extension resolution: `require('./lib/x')` is a valid, working
|
|
1404
|
+
// CommonJS spelling today, and matching only the extension-bearing form
|
|
1405
|
+
// resolved `x` literally, found nothing, and failed the install on a
|
|
1406
|
+
// legitimate require (review of #4087). Try the bare name first so an
|
|
1407
|
+
// extension-bearing capture still wins, then .js/.cjs.
|
|
1408
|
+
let resolvedName;
|
|
1409
|
+
for (const candidate of [libFile, `${libFile}.js`, `${libFile}.cjs`]) {
|
|
1410
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(srcLibDir, candidate))) {
|
|
1411
|
+
resolvedName = candidate;
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
const libSrc = node_path_1.default.join(srcLibDir, resolvedName ?? libFile);
|
|
1416
|
+
if (resolvedName === undefined) {
|
|
1417
|
+
// FAIL LOUD. Skipping here would ship hook scripts whose top-level
|
|
1418
|
+
// require() throws before their own try/catch, wedging every session —
|
|
1419
|
+
// and the install would still exit 0, so nobody would know until a user
|
|
1420
|
+
// hit it. A missing helper source is a packaging bug; surface it.
|
|
1421
|
+
throw new Error(`hooks/lib/${libFile} is required by a staged ${runtimeLabel} hook but is missing from ${srcLibDir}. `
|
|
1422
|
+
+ 'Installing would ship a hook that throws MODULE_NOT_FOUND at load.');
|
|
1423
|
+
}
|
|
1424
|
+
let libContent = node_fs_1.default.readFileSync(libSrc, 'utf8');
|
|
1425
|
+
if (transform)
|
|
1426
|
+
libContent = transform(libContent);
|
|
1427
|
+
// Written under its RESOLVED name so an extensionless require still lands a
|
|
1428
|
+
// file Node can resolve at the destination.
|
|
1429
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(destLibDir, resolvedName), libContent);
|
|
1430
|
+
staged.push(resolvedName);
|
|
1431
|
+
scanForLibRequires(libContent, true);
|
|
1432
|
+
libFile = [...requiredLibFiles].find((f) => !scannedLibFiles.has(f));
|
|
1433
|
+
}
|
|
1434
|
+
return staged;
|
|
1435
|
+
}
|
|
1097
1436
|
function writeCursorHooksJson(targetDir, src, opts) {
|
|
1098
1437
|
opts = opts || {};
|
|
1099
1438
|
const hooksDir = node_path_1.default.join(targetDir, 'hooks');
|
|
@@ -1119,42 +1458,25 @@ function writeCursorHooksJson(targetDir, src, opts) {
|
|
|
1119
1458
|
installedScripts.add(script);
|
|
1120
1459
|
}
|
|
1121
1460
|
}
|
|
1122
|
-
// Stage the hooks/lib/ helpers the staged scripts require (#2587)
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1128
|
-
|
|
1129
|
-
for
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const destLibDir = node_path_1.default.join(hooksDir, 'lib');
|
|
1142
|
-
node_fs_1.default.mkdirSync(destLibDir, { recursive: true });
|
|
1143
|
-
for (const libFile of requiredLibFiles) {
|
|
1144
|
-
const libSrc = node_path_1.default.join(srcLibDir, libFile);
|
|
1145
|
-
if (!node_fs_1.default.existsSync(libSrc)) {
|
|
1146
|
-
// FAIL LOUD. Skipping here would ship hook scripts whose top-level
|
|
1147
|
-
// require() throws before their own try/catch, wedging every session —
|
|
1148
|
-
// and the install would still exit 0, so nobody would know until a user
|
|
1149
|
-
// hit it. A missing helper source is a packaging bug; surface it.
|
|
1150
|
-
throw new Error(`hooks/lib/${libFile} is required by a staged Cursor hook but is missing from ${srcLibDir}. `
|
|
1151
|
-
+ 'Installing would ship a hook that throws MODULE_NOT_FOUND at load.');
|
|
1152
|
-
}
|
|
1153
|
-
let libContent = node_fs_1.default.readFileSync(libSrc, 'utf8');
|
|
1154
|
-
libContent = libContent.replace(/gsd:/gi, 'gsd-');
|
|
1155
|
-
node_fs_1.default.writeFileSync(node_path_1.default.join(destLibDir, libFile), libContent);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1461
|
+
// Stage the hooks/lib/ helpers the staged scripts require (#2587), TRANSITIVELY
|
|
1462
|
+
// (#3911 review): a lib helper can itself require a sibling under lib/ (e.g.
|
|
1463
|
+
// hooks/lib/hook-exit.js requires './cli-exit.js', which requires
|
|
1464
|
+
// './exit-code-registry.js') — a require with NO './lib/' prefix, because from
|
|
1465
|
+
// inside lib/ the sibling is already local. The original single-pass scan only
|
|
1466
|
+
// ever matched the "./lib/…" spelling used FROM a hook script, so it staged
|
|
1467
|
+
// hook-exit.js but never walked hook-exit.js's own requires, and an installed
|
|
1468
|
+
// Cursor hook wedged on MODULE_NOT_FOUND for './cli-exit.js' at load — before
|
|
1469
|
+
// its own try/catch. This walks a worklist: hook scripts seed it with their
|
|
1470
|
+
// "./lib/X" requires, and every lib file staged is itself scanned for further
|
|
1471
|
+
// "./lib/X" OR bare "./X" (sibling-within-lib) requires, so the requirement
|
|
1472
|
+
// graph is derived to a fixed point instead of one hand-tuned level deep.
|
|
1473
|
+
stageTransitiveHookLibs({
|
|
1474
|
+
seedSources: [...installedScripts].map((script) => node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8')),
|
|
1475
|
+
srcLibDir: node_path_1.default.join(srcHooksDir, 'lib'),
|
|
1476
|
+
destLibDir: node_path_1.default.join(hooksDir, 'lib'),
|
|
1477
|
+
runtimeLabel: 'Cursor',
|
|
1478
|
+
transform: (content) => content.replace(/gsd:/gi, 'gsd-'),
|
|
1479
|
+
});
|
|
1158
1480
|
// #2717: write the CommonJS marker into hooks/ alongside the staged .js
|
|
1159
1481
|
// scripts. Cursor sets skipSharedHooksInstall, so it never reaches
|
|
1160
1482
|
// installSharedHooksBundle (the only other writer of this marker); without
|
|
@@ -1357,6 +1679,26 @@ function writeWindsurfHooksJson(targetDir, src, opts) {
|
|
|
1357
1679
|
installedScripts.add(script);
|
|
1358
1680
|
}
|
|
1359
1681
|
}
|
|
1682
|
+
// Stage the hooks/lib/ helpers these scripts require (#4087 review). Windsurf
|
|
1683
|
+
// sets hostBehaviors.skipSharedHooksInstall, so like Cursor it never reaches
|
|
1684
|
+
// installSharedHooksBundle — the only other stager of hooks/lib — and it was
|
|
1685
|
+
// staging neither. Both Cascade guards require helpers at module load:
|
|
1686
|
+
// gsd-windsurf-pre-write.js requires ./lib/hook-exit.js and ./lib/git-probe.js,
|
|
1687
|
+
// gsd-windsurf-pre-command.js requires ./lib/hook-exit.js. Measured against a
|
|
1688
|
+
// real `--windsurf --global` install before this call existed: the installer
|
|
1689
|
+
// exited 0, hooks/ held only the two scripts, and running either one exited 1
|
|
1690
|
+
// with "Cannot find module './lib/hook-exit.js'" — the same failure #4087
|
|
1691
|
+
// reports for Codex, on every pre_write_code / pre_run_command event.
|
|
1692
|
+
//
|
|
1693
|
+
// The transform matches the one applied to the scripts above: a helper must be
|
|
1694
|
+
// rewritten the same way as its caller or the two disagree on the spelling.
|
|
1695
|
+
stageTransitiveHookLibs({
|
|
1696
|
+
seedSources: [...installedScripts].map((script) => node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8')),
|
|
1697
|
+
srcLibDir: node_path_1.default.join(srcHooksDir, 'lib'),
|
|
1698
|
+
destLibDir: node_path_1.default.join(hooksDir, 'lib'),
|
|
1699
|
+
runtimeLabel: 'Windsurf',
|
|
1700
|
+
transform: (content) => content.replace(/gsd:/gi, 'gsd-'),
|
|
1701
|
+
});
|
|
1360
1702
|
// #2717: write the CommonJS marker into hooks/ alongside the staged .js
|
|
1361
1703
|
// scripts. Windsurf sets skipSharedHooksInstall, so it never reaches
|
|
1362
1704
|
// installSharedHooksBundle (the only other writer of this marker); without
|
|
@@ -1507,6 +1849,37 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1507
1849
|
if (!settings.hooks.SessionStart) {
|
|
1508
1850
|
settings.hooks.SessionStart = [];
|
|
1509
1851
|
}
|
|
1852
|
+
// #3981: Claude Code treats a timed-out hook as NON-blocking — the tool
|
|
1853
|
+
// call continues through the normal permission flow. The blocking
|
|
1854
|
+
// PreToolUse guards therefore need a budget a host stall cannot exceed,
|
|
1855
|
+
// not one sized to the hook's own ~0.1 s runtime. Observed stalls reached
|
|
1856
|
+
// 84.3 s; 120 s is the top of the issue's prescribed 60–120 range and
|
|
1857
|
+
// returns every observed verdict. Registration below uses this constant,
|
|
1858
|
+
// and the migration pass right here raises existing managed entries.
|
|
1859
|
+
const BLOCKING_GUARD_TIMEOUT_S = 120;
|
|
1860
|
+
const blockingGuardNames = [
|
|
1861
|
+
'gsd-prompt-guard',
|
|
1862
|
+
'gsd-workflow-guard',
|
|
1863
|
+
'gsd-worktree-path-guard',
|
|
1864
|
+
'gsd-agent-isolation-guard',
|
|
1865
|
+
'gsd-write-guard',
|
|
1866
|
+
'gsd-secret-read-guard',
|
|
1867
|
+
'gsd-validate-commit',
|
|
1868
|
+
];
|
|
1869
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
1870
|
+
if (!Array.isArray(entries))
|
|
1871
|
+
continue;
|
|
1872
|
+
for (const entry of entries) {
|
|
1873
|
+
if (!entry || !Array.isArray(entry.hooks))
|
|
1874
|
+
continue;
|
|
1875
|
+
for (const h of entry.hooks) {
|
|
1876
|
+
if (blockingGuardNames.some((name) => referencesHook(h, name)) &&
|
|
1877
|
+
h.timeout === 5) {
|
|
1878
|
+
h.timeout = BLOCKING_GUARD_TIMEOUT_S;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1510
1883
|
const hasGsdUpdateHook = settings.hooks.SessionStart.some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-check-update')));
|
|
1511
1884
|
// Guard: only register if the hook file was actually installed (#1754).
|
|
1512
1885
|
// When hooks/dist/ is missing from the npm package (as in v1.32.0), the
|
|
@@ -1587,7 +1960,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1587
1960
|
{
|
|
1588
1961
|
type: 'command',
|
|
1589
1962
|
command: promptGuardCommand,
|
|
1590
|
-
timeout:
|
|
1963
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
1591
1964
|
}
|
|
1592
1965
|
]
|
|
1593
1966
|
});
|
|
@@ -1664,7 +2037,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1664
2037
|
{
|
|
1665
2038
|
type: 'command',
|
|
1666
2039
|
command: workflowGuardCommand,
|
|
1667
|
-
timeout:
|
|
2040
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
1668
2041
|
}
|
|
1669
2042
|
]
|
|
1670
2043
|
});
|
|
@@ -1689,7 +2062,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1689
2062
|
{
|
|
1690
2063
|
type: 'command',
|
|
1691
2064
|
command: worktreePathGuardCommand,
|
|
1692
|
-
timeout:
|
|
2065
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
1693
2066
|
}
|
|
1694
2067
|
]
|
|
1695
2068
|
});
|
|
@@ -1719,7 +2092,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1719
2092
|
{
|
|
1720
2093
|
type: 'command',
|
|
1721
2094
|
command: agentIsolationGuardCommand,
|
|
1722
|
-
timeout:
|
|
2095
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
1723
2096
|
}
|
|
1724
2097
|
]
|
|
1725
2098
|
});
|
|
@@ -1746,7 +2119,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1746
2119
|
{
|
|
1747
2120
|
type: 'command',
|
|
1748
2121
|
command: writeGuardCommand,
|
|
1749
|
-
timeout:
|
|
2122
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
1750
2123
|
}
|
|
1751
2124
|
]
|
|
1752
2125
|
});
|
|
@@ -1755,6 +2128,33 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1755
2128
|
else if (!hasWriteGuardHook && !node_fs_1.default.existsSync(writeGuardFile)) {
|
|
1756
2129
|
console.warn(` ${yellow}⚠${reset} Skipped write guard hook — gsd-write-guard.js not found at target`);
|
|
1757
2130
|
}
|
|
2131
|
+
// Configure PreToolUse hook for secret-file read protection (#4221).
|
|
2132
|
+
// Hard-blocks Read/Grep/Bash reads of .env, .env.<suffix> and .secrets.
|
|
2133
|
+
// Replaces the Read(.env*) permission deny rules the installer used to
|
|
2134
|
+
// write (#768): on Claude Code >= 2.1.259 ANY Read() deny rule makes every
|
|
2135
|
+
// `cd DIR && grep …` compound prompt for approval, even in auto mode; a
|
|
2136
|
+
// hook denial is not a permission rule and never arms that check.
|
|
2137
|
+
const secretReadGuardCommand = isGlobal
|
|
2138
|
+
? buildHookCommand(targetDir, 'gsd-secret-read-guard.js', hookOpts)
|
|
2139
|
+
: localCmd('gsd-secret-read-guard.js');
|
|
2140
|
+
const hasSecretReadGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-secret-read-guard')));
|
|
2141
|
+
const secretReadGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-secret-read-guard.js');
|
|
2142
|
+
if (!hasSecretReadGuardHook && node_fs_1.default.existsSync(secretReadGuardFile) && secretReadGuardCommand) {
|
|
2143
|
+
settings.hooks[preToolEvent].push({
|
|
2144
|
+
matcher: 'Read|Grep|Bash',
|
|
2145
|
+
hooks: [
|
|
2146
|
+
{
|
|
2147
|
+
type: 'command',
|
|
2148
|
+
command: secretReadGuardCommand,
|
|
2149
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
2150
|
+
}
|
|
2151
|
+
]
|
|
2152
|
+
});
|
|
2153
|
+
console.log(` ${green}✓${reset} Configured secret read guard hook (.env / .secrets read protection)`);
|
|
2154
|
+
}
|
|
2155
|
+
else if (!hasSecretReadGuardHook && !node_fs_1.default.existsSync(secretReadGuardFile)) {
|
|
2156
|
+
console.warn(` ${yellow}⚠${reset} Skipped secret read guard hook — gsd-secret-read-guard.js not found at target`);
|
|
2157
|
+
}
|
|
1758
2158
|
// Configure commit validation hook (Conventional Commits enforcement, opt-in)
|
|
1759
2159
|
const validateCommitCommand = isGlobal
|
|
1760
2160
|
? buildHookCommand(targetDir, 'gsd-validate-commit.sh', hookOpts)
|
|
@@ -1771,7 +2171,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1771
2171
|
{
|
|
1772
2172
|
type: 'command',
|
|
1773
2173
|
command: validateCommitCommand,
|
|
1774
|
-
timeout:
|
|
2174
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
1775
2175
|
}
|
|
1776
2176
|
]
|
|
1777
2177
|
});
|
|
@@ -2100,6 +2500,7 @@ function buildKimiHooksTomlBlock(targetDir, opts) {
|
|
|
2100
2500
|
{ event: 'PreToolUse', command: cmd('gsd-read-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
|
|
2101
2501
|
{ event: 'PreToolUse', command: cmd('gsd-worktree-path-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
|
|
2102
2502
|
{ event: 'PreToolUse', command: cmd('gsd-write-guard.js'), matcher: 'WriteFile', timeout: 5 },
|
|
2503
|
+
{ event: 'PreToolUse', command: cmd('gsd-secret-read-guard.js'), matcher: 'ReadFile|Grep|Shell', timeout: 5 },
|
|
2103
2504
|
{ event: 'PreToolUse', command: cmd('gsd-workflow-guard.js'), matcher: 'Shell|WriteFile|StrReplaceFile', timeout: 5 },
|
|
2104
2505
|
{ event: 'PreToolUse', command: cmd('gsd-validate-commit.sh'), matcher: 'Shell', timeout: 5 },
|
|
2105
2506
|
// PostToolUse
|
|
@@ -2302,6 +2703,7 @@ module.exports = {
|
|
|
2302
2703
|
KIMI_HOOKS_TOML_MARKER_BEGIN,
|
|
2303
2704
|
KIMI_HOOKS_TOML_MARKER_END,
|
|
2304
2705
|
// Shared
|
|
2706
|
+
stageTransitiveHookLibs,
|
|
2305
2707
|
buildHookCommand,
|
|
2306
2708
|
applySettingsJsonHooks,
|
|
2307
2709
|
referencesHook,
|
|
@@ -2309,7 +2711,9 @@ module.exports = {
|
|
|
2309
2711
|
reconcileManagedShellHookCommands,
|
|
2310
2712
|
normalizeNodePath,
|
|
2311
2713
|
resolveNodeRunner,
|
|
2714
|
+
buildNodeRunnerChainToken,
|
|
2312
2715
|
resolveBashRunner,
|
|
2716
|
+
NODE_RUNNER_RESOLVER_HOOK,
|
|
2313
2717
|
// Atomic write seam (shared with bin/install.js so all writes participate
|
|
2314
2718
|
// in install.js's _cleanTmpFiles() scoped temp-cleanup).
|
|
2315
2719
|
atomicWriteFileSync,
|