@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
|
@@ -7,11 +7,78 @@
|
|
|
7
7
|
#
|
|
8
8
|
# OPT-IN: This hook is a no-op unless config.json has hooks.community: true.
|
|
9
9
|
# Enable with: "hooks": { "community": true } in .planning/config.json
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
# Temp files created below for subprocess stderr capture (config read, command
|
|
13
|
+
# extraction, classifier). A single EXIT trap replaces three hand-rolled
|
|
14
|
+
# mktemp/rm-f pairs so an early or unexpected exit path can never leak one —
|
|
15
|
+
# and a future fourth check does not need its own copy (#3911 review).
|
|
16
|
+
# Idempotent and failure-proof by construction: unset vars expand to "" (a
|
|
17
|
+
# no-op rm -f target), and `|| true` guarantees the trap itself never changes
|
|
18
|
+
# the script's exit status.
|
|
19
|
+
ENABLED_ERR=""
|
|
20
|
+
CMD_ERR=""
|
|
21
|
+
CLASSIFY_ERR=""
|
|
22
|
+
cleanup_temp_files() {
|
|
23
|
+
rm -f "${ENABLED_ERR:-}" "${CMD_ERR:-}" "${CLASSIFY_ERR:-}" 2>/dev/null || true
|
|
24
|
+
}
|
|
25
|
+
trap cleanup_temp_files EXIT
|
|
26
|
+
|
|
27
|
+
# The 10 built-in Conventional Commits types — the SINGLE declaration (#3811
|
|
28
|
+
# review finding: this was previously hand-typed a second time inside the
|
|
29
|
+
# node -e script below, a generative-fix-divergence risk per CLAUDE.md's
|
|
30
|
+
# known-defect list). Threaded into node via an env var; reused directly by
|
|
31
|
+
# bash below when building COMMIT_TYPES.
|
|
32
|
+
BUILTIN_COMMIT_TYPES=(feat fix docs style refactor perf test build ci chore)
|
|
10
33
|
|
|
11
34
|
# Check opt-in config — exit silently if not enabled
|
|
12
35
|
if [ -f .planning/config.json ]; then
|
|
13
|
-
|
|
36
|
+
ENABLED_ERR=$(mktemp)
|
|
37
|
+
# Single node invocation reads BOTH hooks.community (line 1: '1'/'0') and
|
|
38
|
+
# hooks.commit_types (remaining lines: one sanitized extra type per line) —
|
|
39
|
+
# see #3811. Sanitizing here, not in bash, keeps the safe-token check in one
|
|
40
|
+
# place and guarantees only [a-z][a-z0-9-]* strings ever reach the regex
|
|
41
|
+
# built below, so a configured value can never alter the compiled pattern's
|
|
42
|
+
# structure.
|
|
43
|
+
BUILTIN_COMMIT_TYPES_CSV=$(IFS=,; echo "${BUILTIN_COMMIT_TYPES[*]}")
|
|
44
|
+
CONFIG_OUT=$(GSD_BUILTIN_COMMIT_TYPES="$BUILTIN_COMMIT_TYPES_CSV" node -e "
|
|
45
|
+
try{
|
|
46
|
+
const c=require('./.planning/config.json');
|
|
47
|
+
process.stdout.write(c.hooks?.community===true?'1':'0');
|
|
48
|
+
process.stdout.write('\n');
|
|
49
|
+
const raw=c.hooks?.commit_types;
|
|
50
|
+
const list=Array.isArray(raw)?raw:[];
|
|
51
|
+
const seen=new Set((process.env.GSD_BUILTIN_COMMIT_TYPES||'').split(',').filter(Boolean));
|
|
52
|
+
for (const t of list){
|
|
53
|
+
if (typeof t!=='string') continue;
|
|
54
|
+
if (!/^[a-z][a-z0-9-]*\$/.test(t)) continue;
|
|
55
|
+
if (seen.has(t)) continue;
|
|
56
|
+
seen.add(t);
|
|
57
|
+
process.stdout.write(t+'\n');
|
|
58
|
+
}
|
|
59
|
+
}catch(e){
|
|
60
|
+
process.stderr.write('CONFIG_READ_FAILED: '+(e&&e.message?e.message:String(e)));
|
|
61
|
+
process.exit(3);
|
|
62
|
+
}
|
|
63
|
+
" 2>"$ENABLED_ERR") || CONFIG_STATUS=$?
|
|
64
|
+
CONFIG_STATUS=${CONFIG_STATUS:-0}
|
|
65
|
+
if [ "$CONFIG_STATUS" != "0" ]; then
|
|
66
|
+
# Could not determine the opt-in flag at all (node missing, JSON parse
|
|
67
|
+
# error other than absence, etc.) — distinct from ".planning/config.json
|
|
68
|
+
# exists and legitimately disables the hook". Say so and pass, per #3838.
|
|
69
|
+
echo "gsd-validate-commit.sh: could not read .planning/config.json (opt-in check) — validator disabled for this call. $(cat "$ENABLED_ERR")" >&2
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
ENABLED=$(printf '%s\n' "$CONFIG_OUT" | head -1)
|
|
14
73
|
if [ "$ENABLED" != "1" ]; then exit 0; fi
|
|
74
|
+
# Remaining lines (if any) are the sanitized, deduped configured commit
|
|
75
|
+
# types beyond the 10 built-ins (#3811). Read into a bash-3.2-safe array —
|
|
76
|
+
# `mapfile`/`readarray` are bash 4+ only and this hook is tested against
|
|
77
|
+
# bash 3.2.57 (macOS default).
|
|
78
|
+
EXTRA_COMMIT_TYPES=()
|
|
79
|
+
while IFS= read -r _extra_type; do
|
|
80
|
+
[ -n "$_extra_type" ] && EXTRA_COMMIT_TYPES+=("$_extra_type")
|
|
81
|
+
done < <(printf '%s\n' "$CONFIG_OUT" | tail -n +2)
|
|
15
82
|
else
|
|
16
83
|
exit 0
|
|
17
84
|
fi
|
|
@@ -19,32 +86,476 @@ fi
|
|
|
19
86
|
INPUT=$(cat)
|
|
20
87
|
|
|
21
88
|
# Extract command from JSON using Node (handles escaping correctly, no jq needed)
|
|
22
|
-
|
|
89
|
+
CMD_ERR=$(mktemp)
|
|
90
|
+
CMD=$(echo "$INPUT" | node -e "
|
|
91
|
+
let d='';
|
|
92
|
+
process.stdin.on('data',c=>d+=c);
|
|
93
|
+
process.stdin.on('end',()=>{
|
|
94
|
+
try{
|
|
95
|
+
process.stdout.write(JSON.parse(d).tool_input?.command||'');
|
|
96
|
+
}catch(e){
|
|
97
|
+
process.stderr.write('COMMAND_EXTRACTION_FAILED: '+(e&&e.message?e.message:String(e)));
|
|
98
|
+
process.exit(3);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
" 2>"$CMD_ERR") || CMD_STATUS=$?
|
|
102
|
+
CMD_STATUS=${CMD_STATUS:-0}
|
|
103
|
+
if [ "$CMD_STATUS" != "0" ]; then
|
|
104
|
+
# Could not extract tool_input.command at all (node missing, malformed
|
|
105
|
+
# JSON, etc.) — distinct from "there is genuinely no command field". Say
|
|
106
|
+
# so and pass, per #3838.
|
|
107
|
+
echo "gsd-validate-commit.sh: could not extract tool_input.command from the hook payload — validator disabled for this call. $(cat "$CMD_ERR")" >&2
|
|
108
|
+
exit 0
|
|
109
|
+
fi
|
|
23
110
|
|
|
24
111
|
# Only check git commit commands.
|
|
25
112
|
# Delegates to hooks/lib/git-cmd.js isGitSubcommand() — the canonical token-walk
|
|
26
113
|
# classifier that handles env-prefix, -C path, and full-path git invocations.
|
|
27
114
|
# A naive `^git\s+commit` regex misses all three; this guard fixes that (#3129).
|
|
28
115
|
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
116
|
+
CLASSIFY_ERR=$(mktemp)
|
|
117
|
+
GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" node -e "
|
|
118
|
+
try {
|
|
119
|
+
const {isGitSubcommand}=require(process.env.GIT_CMD_LIB);
|
|
120
|
+
process.exit(isGitSubcommand(process.argv[1],'commit')?0:1);
|
|
121
|
+
} catch(e) {
|
|
122
|
+
process.stderr.write('CLASSIFIER_THREW: '+(e&&e.message?e.message:String(e)));
|
|
123
|
+
process.exit(3);
|
|
124
|
+
}
|
|
125
|
+
" "$CMD" 2>"$CLASSIFY_ERR" || CLASSIFY_STATUS=$?
|
|
126
|
+
CLASSIFY_STATUS=${CLASSIFY_STATUS:-0}
|
|
127
|
+
if [ "$CLASSIFY_STATUS" != "0" ] && [ "$CLASSIFY_STATUS" != "1" ]; then
|
|
128
|
+
# 0 = is a git commit (validate below); 1 = genuinely not a git commit
|
|
129
|
+
# (real negative, pass silently) — the ONLY intentional non-zero exit the
|
|
130
|
+
# script above ever produces on success. Any other status — 127 node
|
|
131
|
+
# missing, or 3 from the try/catch above when the git-cmd.js require chain
|
|
132
|
+
# throws (e.g. its built dependency, gsd-core/bin/lib/token-scanner.cjs, is
|
|
133
|
+
# a gitignored build artifact and absent on a fresh checkout — run
|
|
134
|
+
# `npm run build:lib`) — means the classifier could not run at all. Say so
|
|
135
|
+
# on stderr and pass (#3838): PreToolUse stderr does not disturb the JSON
|
|
136
|
+
# protocol.
|
|
137
|
+
echo "gsd-validate-commit.sh: could not classify the command via hooks/lib/git-cmd.js (exit $CLASSIFY_STATUS) — validator disabled for this call. If this persists, run \`npm run build:lib\`. $(cat "$CLASSIFY_ERR")" >&2
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
140
|
+
if [ "$CLASSIFY_STATUS" = "0" ]; then
|
|
141
|
+
# Extract message from -m flag.
|
|
142
|
+
#
|
|
143
|
+
# MSG_QUOTE records WHICH arm matched. bash treats the two arms differently
|
|
144
|
+
# and the subject step below depends on that difference — see the resolver
|
|
145
|
+
# gate (review of #3816, round 4).
|
|
34
146
|
MSG=""
|
|
147
|
+
MSG_QUOTE=""
|
|
148
|
+
MSG_MATCH=""
|
|
35
149
|
if [[ "$CMD" =~ -m[[:space:]]+\"([^\"]+)\" ]]; then
|
|
36
150
|
MSG="${BASH_REMATCH[1]}"
|
|
151
|
+
MSG_QUOTE=dq
|
|
152
|
+
MSG_MATCH="${BASH_REMATCH[0]}"
|
|
37
153
|
elif [[ "$CMD" =~ -m[[:space:]]+\'([^\']+)\' ]]; then
|
|
38
154
|
MSG="${BASH_REMATCH[1]}"
|
|
155
|
+
MSG_QUOTE=sq
|
|
156
|
+
MSG_MATCH="${BASH_REMATCH[0]}"
|
|
39
157
|
fi
|
|
40
158
|
|
|
41
159
|
if [ -n "$MSG" ]; then
|
|
42
|
-
|
|
160
|
+
# Subject = first line of the message, EXCEPT for the command-substituted
|
|
161
|
+
# heredoc form, where the first line is the opener rather than the message:
|
|
162
|
+
#
|
|
163
|
+
# git commit -m "$(cat <<'EOF'
|
|
164
|
+
# feat(auth): add login flow
|
|
165
|
+
# EOF
|
|
166
|
+
# )"
|
|
167
|
+
#
|
|
168
|
+
# The capture above spans it whole, because bash `[^"]` matches newlines, so
|
|
169
|
+
# `head -1` yielded the literal `$(cat <<'EOF'` and EVERY heredoc-form commit
|
|
170
|
+
# was blocked regardless of its message (#3802).
|
|
171
|
+
#
|
|
172
|
+
# Selection of WHICH argument is the message is unchanged above — only the
|
|
173
|
+
# subject-from-message step is delegated. Falls back to the previous `head -1`
|
|
174
|
+
# if node or the library is unavailable, so a broken extractor degrades to the
|
|
175
|
+
# old behavior instead of becoming a new silent-allow path.
|
|
176
|
+
#
|
|
177
|
+
# SINGLE-QUOTE GATE (review of #3816, round 4 — BLOCKER). The resolver may
|
|
178
|
+
# only run on the DOUBLE-quoted arm. Inside `-m '...'` bash performs NO
|
|
179
|
+
# command substitution, so `$(cat <<'EOF'` is literal text and git's real
|
|
180
|
+
# subject is that opener line — resolving the body there validates a
|
|
181
|
+
# message git never receives. Measured against the real hook, all four
|
|
182
|
+
# spellings (`<<'E'`, `<<"E"`, `<<\E`, `<<E`) went base=2 -> head=0: a
|
|
183
|
+
# net-new bypass reachable by the ordinary authoring slip of typing `'`
|
|
184
|
+
# for `"`. The sq arm therefore keeps the pre-fix `head -1`, which is exact
|
|
185
|
+
# base parity.
|
|
186
|
+
#
|
|
187
|
+
# ADJACENCY GUARD (review of #3816): text glued to the CLOSING quote —
|
|
188
|
+
# `-m "$(cat <<'EOF' ... )"suffix` — is concatenated by bash into the SAME
|
|
189
|
+
# argument, so the capture above holds only a PREFIX of the real message.
|
|
190
|
+
# Resolving a heredoc from a prefix hands the length gate a fraction of the
|
|
191
|
+
# real subject: a net-new bypass relative to base, which measured the
|
|
192
|
+
# opener line and blocked. When the quote is not followed by whitespace or
|
|
193
|
+
# the end of the command, skip the resolver and keep the pre-fix subject
|
|
194
|
+
# (first captured line): the heredoc form then fails the format gate
|
|
195
|
+
# exactly as it did on base, and the plain single-line form keeps base
|
|
196
|
+
# behavior unchanged. The guard is tested against the arm that MATCHED,
|
|
197
|
+
# not against both: testing both let a double-quoted heredoc whose BODY
|
|
198
|
+
# mentions a glued single-quoted token (`-m "... -m 'foo'bar ..."`) trip
|
|
199
|
+
# the sq arm and lose the fix for a message that never had a prefix
|
|
200
|
+
# problem (review of #3816, round 4, Minor 1).
|
|
201
|
+
# RESOLVER PRECONDITIONS. The resolver may run only where the captured text
|
|
202
|
+
# is provably the subject git receives. Each guard names an input where it
|
|
203
|
+
# is not; every refusal falls back to `head -1`, the pre-fix subject, which
|
|
204
|
+
# fails the format gate exactly as this whole form did before the fix.
|
|
205
|
+
RESOLVE=0
|
|
206
|
+
if [ "$MSG_QUOTE" = dq ]; then
|
|
207
|
+
RESOLVE=1
|
|
208
|
+
# Text before the message we matched. The heredoc BODY always sits after
|
|
209
|
+
# the match, so this window cannot be contaminated by message content —
|
|
210
|
+
# which is what lets the two guards below scan for tokens that would also
|
|
211
|
+
# be legal inside a commit message.
|
|
212
|
+
MSG_PREFIX="${CMD%%"$MSG_MATCH"*}"
|
|
213
|
+
# Text after it. Together, PREFIX and SUFFIX are the whole command MINUS
|
|
214
|
+
# the message — the window a guard must use when the token it scans for
|
|
215
|
+
# is also legal English inside a commit message, but may legally appear
|
|
216
|
+
# on EITHER side of the message on the command line.
|
|
217
|
+
MSG_SUFFIX="${CMD#*"$MSG_MATCH"}"
|
|
218
|
+
# LINE CONTINUATIONS ARE NOT SEPARATORS (review of #3816, rounds 8 and 9).
|
|
219
|
+
# `git commit \` newline ` -m "$(cat <<'EOF' …` is an ordinary way to
|
|
220
|
+
# spread an invocation over lines, and every guard below reads a newline in
|
|
221
|
+
# a window as a command separator, so the whole form was refused. That was
|
|
222
|
+
# disclosed as a fail-closed limit in round 8 because "is this newline a
|
|
223
|
+
# continuation" looked like the segmentation question this file has
|
|
224
|
+
# reverted twice. It is not: bash's rule is local and character-level. A
|
|
225
|
+
# newline preceded by an ODD run of backslashes is a continuation and bash
|
|
226
|
+
# removes both; an EVEN run (`\\` then newline) is a literal backslash
|
|
227
|
+
# followed by a real newline, which IS a separator. So the windows are
|
|
228
|
+
# joined the way bash joins them, in three bash-3.2-safe steps: every `\\`
|
|
229
|
+
# pair is parked on \x01, a byte no real command line carries, any
|
|
230
|
+
# backslash-newline that remains is a lone (odd) one and is removed, then
|
|
231
|
+
# the pairs are restored. Applied to BOTH windows, BEFORE the dequote
|
|
232
|
+
# copies are derived, so every scan sees the joined text.
|
|
233
|
+
#
|
|
234
|
+
# KNOWN OVER-BLOCK, fail-closed: a literal \x01 that IS present in the
|
|
235
|
+
# command is restored as `\\`, so an option-shaped token carrying one
|
|
236
|
+
# (`-\x01m`) reads as `-\\m`, dequotes to `-m`, and refuses where it did
|
|
237
|
+
# not before (independent review, round 9). Refusing is the recoverable
|
|
238
|
+
# direction; a control byte in an option name is not a spelling anyone
|
|
239
|
+
# types, and it is not a hole in the accept direction.
|
|
240
|
+
#
|
|
241
|
+
# Direction check: a continuation glued to the closing quote
|
|
242
|
+
# (`"$(…)"\` newline `suffix`) joins to `"$(…)"suffix`, which the glue
|
|
243
|
+
# guard refuses exactly as bash would have glued it; `\\` + newline keeps
|
|
244
|
+
# its newline and is still refused by the separator guard. Measured on
|
|
245
|
+
# bash 3.2.57 and 5.3.15 in tests/hooks-opt-in.test.cjs.
|
|
246
|
+
CONT_PARK=$'\x01'
|
|
247
|
+
MSG_PREFIX="${MSG_PREFIX//\\\\/$CONT_PARK}"
|
|
248
|
+
MSG_PREFIX="${MSG_PREFIX//\\$'\n'/}"
|
|
249
|
+
MSG_PREFIX="${MSG_PREFIX//$CONT_PARK/\\\\}"
|
|
250
|
+
MSG_SUFFIX="${MSG_SUFFIX//\\\\/$CONT_PARK}"
|
|
251
|
+
MSG_SUFFIX="${MSG_SUFFIX//\\$'\n'/}"
|
|
252
|
+
MSG_SUFFIX="${MSG_SUFFIX//$CONT_PARK/\\\\}"
|
|
253
|
+
|
|
254
|
+
# QUOTE-SPLICED SPELLINGS (independent review of #3816, round 6). Bash
|
|
255
|
+
# removes quotes before git ever sees an argument, so the same option has
|
|
256
|
+
# unboundedly many spellings on the command line: `--clean""up=verbatim`
|
|
257
|
+
# IS `--cleanup=verbatim` to git, and `-""m` IS `-m`. Both matched no
|
|
258
|
+
# literal and were measured ACCEPTING a 75-byte subject the length gate
|
|
259
|
+
# had recorded as 72. The guards below therefore scan a copy of their
|
|
260
|
+
# window with quote characters removed, which is what bash does to it.
|
|
261
|
+
# Only the two OPTION-NAME scans use it; the adjacency test deliberately
|
|
262
|
+
# does not, because it asks about a literal character position, and the
|
|
263
|
+
# message span itself is excluded from both windows either way.
|
|
264
|
+
MSG_PREFIX_DEQ="${MSG_PREFIX//[\"\']/}"
|
|
265
|
+
MSG_SUFFIX_DEQ="${MSG_SUFFIX//[\"\']/}"
|
|
266
|
+
# BACKSLASH-SPLICED SPELLINGS (independent review of #3816, round 7).
|
|
267
|
+
# Quote removal alone was not "the command as bash hands it to git": bash
|
|
268
|
+
# also removes syntactic backslashes, so `-\m WIP` IS `-m WIP` and
|
|
269
|
+
# `--clean\up=verbatim` IS `--cleanup=verbatim` to git, and both matched
|
|
270
|
+
# no literal. Measured: `-\m WIP -m <conforming heredoc>` accepted the
|
|
271
|
+
# heredoc while git recorded `WIP`, and a trailing `--clean\up=verbatim`
|
|
272
|
+
# accepted a 75-byte subject the length gate measured as 72. Stripped in a
|
|
273
|
+
# second pass so the class is unambiguous.
|
|
274
|
+
MSG_PREFIX_DEQ="${MSG_PREFIX_DEQ//\\/}"
|
|
275
|
+
MSG_SUFFIX_DEQ="${MSG_SUFFIX_DEQ//\\/}"
|
|
276
|
+
# DOLLAR-QUOTED SPELLINGS (independent review of #3816, round 8). The two
|
|
277
|
+
# passes above still were not "the command as bash hands it to git": bash
|
|
278
|
+
# has TWO more quoting forms whose introducer is a `$`, and removing the
|
|
279
|
+
# quote characters alone leaves that `$` stranded in the middle of the
|
|
280
|
+
# option name. `-$"m"` became `-$m` here while bash passes a real `-m` to
|
|
281
|
+
# git, and `--mes$'sage'=WIP` became `--mes$sage=WIP`; neither matched any
|
|
282
|
+
# literal, so the first-message guard below never fired. Measured on bash
|
|
283
|
+
# 3.2.57 and 5.3.15 against a real repository: the hook allowed
|
|
284
|
+
# `-$"m" WIP -m <conforming heredoc>` (exit 0) while `git cat-file -p`
|
|
285
|
+
# recorded the subject `WIP` — the same command spelled `-m WIP` is
|
|
286
|
+
# refused (exit 2). Stripping `$` closes both dollar-quote forms.
|
|
287
|
+
#
|
|
288
|
+
# RESIDUAL, and not fixable from a string: an option name assembled by an
|
|
289
|
+
# EXPANSION — `-${x}m`, `-$(printf m)` — is not knowable without running
|
|
290
|
+
# the command, the same limit this file already documents for expanded
|
|
291
|
+
# heredoc bodies. Stripping `$` makes those spellings collapse toward the
|
|
292
|
+
# literal too, which over-matches, and over-matching only refuses more.
|
|
293
|
+
MSG_PREFIX_DEQ="${MSG_PREFIX_DEQ//\$/}"
|
|
294
|
+
MSG_SUFFIX_DEQ="${MSG_SUFFIX_DEQ//\$/}"
|
|
295
|
+
|
|
296
|
+
# ADJACENCY GUARD (review of #3816): text glued to the CLOSING quote —
|
|
297
|
+
# `-m "$(cat <<'EOF' ... )"suffix` — is concatenated by bash into the SAME
|
|
298
|
+
# argument, so the capture holds only a PREFIX of the real message, and
|
|
299
|
+
# the length gate would measure a fraction of the real subject.
|
|
300
|
+
# SCOPE (review of #3816, round 6 — MAJOR). Glue is a property of the ONE
|
|
301
|
+
# character following the MATCHED span, so that character is the whole
|
|
302
|
+
# window. Scanning $CMD for the shape anywhere refused any conforming
|
|
303
|
+
# commit whose command merely CONTAINED a glued `-m` elsewhere —
|
|
304
|
+
# `git commit -m "<heredoc>" && echo -m "test"z` stayed blocked with
|
|
305
|
+
# CONVENTIONAL_COMMITS_VIOLATION. Base blocks it too, because base blocks
|
|
306
|
+
# EVERY heredoc form (that is #3802): this was the fix not reaching the
|
|
307
|
+
# shape, measured base=2 -> pre=2 -> post=0, not a regression.
|
|
308
|
+
# The separators and redirections are excluded because bash does NOT
|
|
309
|
+
# concatenate across them: in `-m "msg"&& echo hi` the argument ends at
|
|
310
|
+
# the quote, so there is no truncated capture to defend against.
|
|
311
|
+
# The class is held in a VARIABLE, not written inline. Inline, every
|
|
312
|
+
# member needs a backslash to get past the `[[ ]]` parser (`;`, `&` and
|
|
313
|
+
# `|` are metacharacters there) — and on bash 3.2, the system /bin/bash on
|
|
314
|
+
# macOS, those backslashes are passed THROUGH to the regex engine instead
|
|
315
|
+
# of being consumed by the shell, silently adding a literal `\` to the
|
|
316
|
+
# class. Unquoted expansion of a variable on the right of `=~` is the one
|
|
317
|
+
# spelling that is a plain regex on 3.2 and 5.x alike (review of #3816,
|
|
318
|
+
# round 8). Writing `[^[:space:];&|()<>]` inline is NOT the fix: it is a
|
|
319
|
+
# bash syntax error on both versions.
|
|
320
|
+
GLUE_CLASS='^[^[:space:];&|()<>]'
|
|
321
|
+
if [[ "$MSG_SUFFIX" =~ $GLUE_CLASS ]]; then RESOLVE=0; fi
|
|
322
|
+
|
|
323
|
+
# FIRST-MESSAGE GUARD (Codex review of #3816, round 4 — BLOCKER). The
|
|
324
|
+
# capture is a SEARCH over the whole command and the double-quoted arm is
|
|
325
|
+
# tried first, so it can select a `-m` that is not git's subject at all:
|
|
326
|
+
#
|
|
327
|
+
# git commit -m 'WIP first' -m "$(cat <<'EOF' -> git concatenates; the
|
|
328
|
+
# git commit -m WIP -m "$(cat <<'EOF' subject is `WIP first`
|
|
329
|
+
# git commit -m WIP -- -m "$(cat <<'EOF' -> after --, not a message
|
|
330
|
+
# git commit -m WIP && echo -m "$(cat <<'EOF' -> belongs to `echo`
|
|
331
|
+
#
|
|
332
|
+
# All four measured base=2 -> head=0, with git recording the FIRST message
|
|
333
|
+
# as the subject (verified against real commits, not the man page). The
|
|
334
|
+
# mis-selection is pre-existing; resolving it is what turned it into an
|
|
335
|
+
# enforcement bypass. Resolve only when nothing before the match could
|
|
336
|
+
# have been an earlier message, an end-of-options marker, or another
|
|
337
|
+
# command.
|
|
338
|
+
# BUNDLED SHORT OPTIONS (independent review of #3816, round 6). git splits
|
|
339
|
+
# `-am 'WIP first'` into `-a -m`, so the real subject is `WIP first` and
|
|
340
|
+
# the heredoc is git's SECOND message — measured accepting the heredoc's
|
|
341
|
+
# subject while git recorded `WIP first`. A standalone `-m` is therefore
|
|
342
|
+
# not the only spelling that claims the message; any short-option cluster
|
|
343
|
+
# ending in `m` does.
|
|
344
|
+
# ATTACHED VALUES AND --message ABBREVIATIONS (independent review of
|
|
345
|
+
# #3816, round 7). The scan required a space or `=` after the option name,
|
|
346
|
+
# so two spellings git accepts matched nothing: an ATTACHED short-option
|
|
347
|
+
# value (`-mWIP`, which git reads as `-m WIP`) and a long-option
|
|
348
|
+
# abbreviation (`--mes=WIP`), the same abbreviation behaviour this file
|
|
349
|
+
# already models for `--cleanup`. Both were measured accepting a later
|
|
350
|
+
# conforming heredoc while git recorded `WIP` as the subject — confirmed
|
|
351
|
+
# against the raw commit object, not `git log --pretty=%s`. The short arm
|
|
352
|
+
# therefore drops its trailing requirement entirely: a `-` followed by
|
|
353
|
+
# letters ending in `m` claims the message however it is spelled. Wider
|
|
354
|
+
# than git's own abbreviation set on purpose — over-matching only refuses
|
|
355
|
+
# more, which is the recoverable direction.
|
|
356
|
+
# Variable-held for the same bash-3.2 reason as GLUE_CLASS above.
|
|
357
|
+
# AN OPTION NAME BUILT BY A COMMAND SUBSTITUTION IS UNRESOLVABLE
|
|
358
|
+
# (independent review of #3816, round 8). Stripping `$` above collapses the
|
|
359
|
+
# two dollar-QUOTE forms onto their literals, but `--clean$(printf up)=`
|
|
360
|
+
# is a different thing: bash RUNS a program to finish the option name, so
|
|
361
|
+
# the argv git receives is not derivable from this string at all. Measured
|
|
362
|
+
# accepting a 75-byte subject the length gate had recorded as 72.
|
|
363
|
+
#
|
|
364
|
+
# SCOPED TO THE NAME, NOT THE VALUE. The class is a `-`-leading token whose
|
|
365
|
+
# characters up to the substitution contain no `=` — an option NAME being
|
|
366
|
+
# assembled. `--author="$(git config user.name)"` and `--author "$(…)"`
|
|
367
|
+
# both put the substitution in the VALUE, which this file never models and
|
|
368
|
+
# which stays allowed; only `-…$(` before any `=` refuses. Scanned on the
|
|
369
|
+
# RAW windows on purpose: the dequoted copies have had their `$` removed,
|
|
370
|
+
# so the shape is no longer visible there.
|
|
371
|
+
#
|
|
372
|
+
# This is a SHAPE, not a segmentation: it never tries to decide where
|
|
373
|
+
# git's own command ends. Two attempts at that were reverted for opening
|
|
374
|
+
# accept-direction holes, and the reasoning above still stands.
|
|
375
|
+
# WIDENED, and the strategy changed with it (independent review, round 9).
|
|
376
|
+
# The `$(`-only spelling above was the fourth patch in a row that tried to
|
|
377
|
+
# EMULATE what bash does to an argument before git sees it -- round 6
|
|
378
|
+
# removed quotes, round 7 backslashes, round 8 the `$` of a dollar-quote,
|
|
379
|
+
# and each time review found another transform that had been missed. Round
|
|
380
|
+
# 9 found four more, all measured accepting `WIP` as the real subject on
|
|
381
|
+
# bash 3.2.57 and 5.3.15 while the plain spelling of the same command is
|
|
382
|
+
# refused:
|
|
383
|
+
#
|
|
384
|
+
# -$'\155' WIP ANSI-C octal escape decodes to `m`
|
|
385
|
+
# -$'\x6d' WIP ANSI-C hex escape decodes to `m`
|
|
386
|
+
# -`printf m` WIP command substitution, backtick spelling
|
|
387
|
+
# x= … -${x}m WIP parameter expansion
|
|
388
|
+
# -? WIP pathname expansion, with a file named `-m`
|
|
389
|
+
#
|
|
390
|
+
# The last two settle the strategy: an option name finished by a PARAMETER
|
|
391
|
+
# expansion depends on a variable's runtime value, and one finished by a
|
|
392
|
+
# PATHNAME expansion depends on the contents of the working directory.
|
|
393
|
+
# Neither is derivable from the command string at any level of effort, so
|
|
394
|
+
# emulation cannot be completed -- not "has not been completed yet".
|
|
395
|
+
#
|
|
396
|
+
# So the rule is no longer "normalise it and match the literal". It is: an
|
|
397
|
+
# option NAME containing a shell expansion or quoting construct is
|
|
398
|
+
# UNRESOLVABLE, and unresolvable refuses. One rule covers every spelling
|
|
399
|
+
# above, and every spelling nobody has thought of yet, in the fail-closed
|
|
400
|
+
# direction. The dequoting passes above are kept: they still normalise the
|
|
401
|
+
# deterministic removals so the guards RECOGNISE `--clean""up=` and `-\m`
|
|
402
|
+
# rather than merely refusing them, which keeps the existing rows honest.
|
|
403
|
+
#
|
|
404
|
+
# SCOPED TO THE NAME, NOT THE VALUE, exactly as before: the class is a
|
|
405
|
+
# `-`-leading token whose characters up to the substitution contain no `=`.
|
|
406
|
+
# `--author="$(git config user.name)"` and `--author "$(…)"` put the
|
|
407
|
+
# construct in the VALUE and still resolve, pinned in both directions.
|
|
408
|
+
# Scanned on the RAW windows, because the dequoted copies have had `$` and
|
|
409
|
+
# the quote characters removed and the shape is no longer visible there.
|
|
410
|
+
#
|
|
411
|
+
# The class is bracket-only and holds no backslash, per round 8: a POSIX
|
|
412
|
+
# bracket expression has no escape mechanism, and a backslash written
|
|
413
|
+
# inside one becomes a literal member on bash 3.2.
|
|
414
|
+
SUBST_NAME_CLASS='(^|[[:space:]])-[^[:space:]=]*[$`?*[]'
|
|
415
|
+
if [[ "$MSG_PREFIX" =~ $SUBST_NAME_CLASS ]] \
|
|
416
|
+
|| [[ "$MSG_SUFFIX" =~ $SUBST_NAME_CLASS ]]; then RESOLVE=0; fi
|
|
417
|
+
SEP_CLASS='[;&|]'
|
|
418
|
+
if [[ "$MSG_PREFIX_DEQ" =~ (^|[[:space:]])(-[a-zA-Z]*m|--m[a-z]*([=[:space:]]|$)) ]] \
|
|
419
|
+
|| [[ "$MSG_PREFIX" =~ (^|[[:space:]])--([[:space:]]|$) ]] \
|
|
420
|
+
|| [[ "$MSG_PREFIX" =~ $SEP_CLASS ]] \
|
|
421
|
+
|| [[ "$MSG_PREFIX" == *$'\n'* ]]; then RESOLVE=0; fi
|
|
422
|
+
# NEWLINE IS A COMMAND SEPARATOR TOO (independent review of #3816, round
|
|
423
|
+
# 7) — the test above. The separator scan covered `;`, `&` and `|` but not
|
|
424
|
+
# a literal newline, so a LATER command's heredoc-shaped `-m` was taken
|
|
425
|
+
# for this commit's message:
|
|
426
|
+
#
|
|
427
|
+
# git commit --amend --no-edit
|
|
428
|
+
# echo -m "$(cat <<'EOF'
|
|
429
|
+
# fix: conforming text unrelated to the commit
|
|
430
|
+
# EOF
|
|
431
|
+
# )"
|
|
432
|
+
#
|
|
433
|
+
# The classifier recognises the leading commit, the capture reaches across
|
|
434
|
+
# the newline into `echo`'s argument, and a conforming string with no
|
|
435
|
+
# relationship to the commit was validated and allowed. Tested as a glob
|
|
436
|
+
# rather than folded into the bracket class, because a literal newline
|
|
437
|
+
# inside a bash regex bracket expression is not portably expressible.
|
|
438
|
+
|
|
439
|
+
# CLEANUP-MODE GUARD (Codex review of #3816, round 4 — BLOCKER). The
|
|
440
|
+
# resolver skips leading blank lines and strips trailing whitespace
|
|
441
|
+
# because git's DEFAULT cleanup=whitespace does. Under
|
|
442
|
+
# `--cleanup=verbatim` git does neither, so a 72-char subject plus three
|
|
443
|
+
# trailing spaces is committed as a 75-byte subject while the hook
|
|
444
|
+
# measured 72 — COMMIT_SUBJECT_TOO_LONG dodged (measured base=2 -> head=0;
|
|
445
|
+
# confirmed by reading the raw commit object, since `git log --pretty=%s`
|
|
446
|
+
# strips trailing whitespace in its own output and hides it).
|
|
447
|
+
# Any named mode other than `whitespace` refuses. A mode set persistently
|
|
448
|
+
# in git config is invisible here and stays a documented residual limit.
|
|
449
|
+
# SCOPE (review of #3816, round 5 — BLOCKER). This scan must exclude the
|
|
450
|
+
# message. `--cleanup=` and `commit.cleanup=` are ordinary English inside
|
|
451
|
+
# a commit message — this repository's own hooks and docs discuss them
|
|
452
|
+
# constantly — and the heredoc BODY sits verbatim inside $CMD, so
|
|
453
|
+
# scanning $CMD refused to resolve any conforming message that merely
|
|
454
|
+
# MENTIONED the token, blocking it with CONVENTIONAL_COMMITS_VIOLATION.
|
|
455
|
+
# Scanning $MSG_PREFIX alone (the fix as first prescribed) would reopen
|
|
456
|
+
# the bypass this guard exists for: git accepts the flag on either side
|
|
457
|
+
# of -m, and `git commit -m "<heredoc>" --cleanup=verbatim` is caught
|
|
458
|
+
# today only because the scan is command-wide. PREFIX + SUFFIX keeps both
|
|
459
|
+
# positions covered while excluding the one span that is message text.
|
|
460
|
+
# The two are joined with a space so a token cannot be forged across the
|
|
461
|
+
# seam out of a prefix tail and a suffix head.
|
|
462
|
+
# KNOWN LIMIT, deliberately fail-closed (#3816, round 6). This window is
|
|
463
|
+
# the whole command minus the message, so a `--cleanup=` that belongs to a
|
|
464
|
+
# DIFFERENT command — `git commit -m "<heredoc>" && echo --cleanup=verbatim`
|
|
465
|
+
# — also refuses, and a conforming commit git would accept stays blocked.
|
|
466
|
+
# Narrowing it to git's own segment was tried and reverted: deciding where
|
|
467
|
+
# git's command ends needs a shell parse, and a substring scan is not one.
|
|
468
|
+
# Trimming at the first `;&|` cut the window short whenever a separator sat
|
|
469
|
+
# inside an ordinary argument — `--author "a&b"`, and equally `--author
|
|
470
|
+
# a\&b` — which hid a REAL trailing `--cleanup=verbatim` and ACCEPTED a
|
|
471
|
+
# 75-byte subject the length gate had measured as 72. Two successive
|
|
472
|
+
# narrowings each reopened that hole on a shape the previous one missed, so
|
|
473
|
+
# the scan stays wide: refusing a commit git would take is recoverable,
|
|
474
|
+
# accepting an over-long subject is not.
|
|
475
|
+
# ABBREVIATIONS (independent review of #3816, round 6). git accepts any
|
|
476
|
+
# unambiguous prefix of a long option, so `--cle=verbatim` sets the mode
|
|
477
|
+
# while matching no literal `--cleanup` — measured accepting a 75-byte
|
|
478
|
+
# subject recorded as 72. The class is deliberately wider than git's own
|
|
479
|
+
# abbreviation set: over-matching only refuses more, which is the safe
|
|
480
|
+
# direction, and no other `--cl` option exists for git commit.
|
|
481
|
+
# LAST DIRECTIVE WINS, AND ONE MATCH CANNOT SEE IT (independent review of
|
|
482
|
+
# #3816, round 7). A bash regex yields ONE BASH_REMATCH, so only the
|
|
483
|
+
# FIRST cleanup directive was inspected — and git applies the LAST one.
|
|
484
|
+
# `--cleanup=whitespace -m <heredoc> --cleanup=verbatim` therefore read as
|
|
485
|
+
# mode=whitespace, resolution stayed enabled, and a 72-character subject
|
|
486
|
+
# plus trailing spaces was accepted while git recorded 75 bytes with the
|
|
487
|
+
# whitespace preserved (confirmed against the raw commit object). Deciding
|
|
488
|
+
# WHICH directive is last needs an argv order this substring scan does not
|
|
489
|
+
# have, so multiplicity itself refuses: more than one directive is
|
|
490
|
+
# unresolvable, not "probably fine". Single-directive behaviour is
|
|
491
|
+
# unchanged.
|
|
492
|
+
CLEANUP_WINDOW="$MSG_PREFIX_DEQ $MSG_SUFFIX_DEQ"
|
|
493
|
+
# `|| true` is load-bearing: this script runs under `set -euo pipefail`,
|
|
494
|
+
# and grep exits 1 when it matches NOTHING — which is the common case, a
|
|
495
|
+
# command with no cleanup directive at all. Without it the pipeline's
|
|
496
|
+
# non-zero status killed the hook outright (exit 1, no verdict) for every
|
|
497
|
+
# ordinary commit. Caught by running the real hook rather than the scan.
|
|
498
|
+
CLEANUP_HITS=$( { printf '%s' "$CLEANUP_WINDOW" | grep -oE '(--cl[a-z]*|commit\.cleanup)[=[:space:]]+[^[:space:]]+' || true; } | wc -l | tr -d ' ')
|
|
499
|
+
if [ "${CLEANUP_HITS:-0}" -gt 1 ]; then
|
|
500
|
+
RESOLVE=0
|
|
501
|
+
elif [[ "$CLEANUP_WINDOW" =~ (--cl[a-z]*|commit\.cleanup)[=[:space:]]+([^[:space:]]+) ]]; then
|
|
502
|
+
if [ "${BASH_REMATCH[2]}" != "whitespace" ]; then RESOLVE=0; fi
|
|
503
|
+
fi
|
|
504
|
+
|
|
505
|
+
# GIT-GENERATED SUBJECTS (independent review of #3816, round 7). With
|
|
506
|
+
# `--squash=<commit>` or `--fixup=<commit>` git composes the subject
|
|
507
|
+
# itself — measured recording `squash! base: something` while a conforming
|
|
508
|
+
# heredoc supplied via -m sailed through. The supplied message is not the
|
|
509
|
+
# subject in these modes at all, so there is nothing here worth measuring
|
|
510
|
+
# and resolution is refused outright. Abbreviations included for the same
|
|
511
|
+
# reason as --cleanup's. Deliberately NOT extended to the other
|
|
512
|
+
# message-SOURCE options (-C/--reuse-message, -c/--reedit-message,
|
|
513
|
+
# -F/--file, -t/--template): `-c` is also a git GLOBAL option that legally
|
|
514
|
+
# precedes the subcommand, so a scan for it would refuse ordinary
|
|
515
|
+
# `git -c k=v commit` invocations. Those remain a disclosed gap rather
|
|
516
|
+
# than a guessed guard.
|
|
517
|
+
if [[ "$MSG_PREFIX_DEQ $MSG_SUFFIX_DEQ" =~ (^|[[:space:]])--(squash|fixup|sq[a-z]*|fix[a-z]*)[=[:space:]] ]]; then RESOLVE=0; fi
|
|
518
|
+
fi
|
|
519
|
+
|
|
520
|
+
if [ "$RESOLVE" = 1 ]; then
|
|
521
|
+
SUBJECT=$(GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" MSG="$MSG" node -e "
|
|
522
|
+
const {resolveCommitSubject}=require(process.env.GIT_CMD_LIB);
|
|
523
|
+
process.stdout.write(resolveCommitSubject(process.env.MSG));
|
|
524
|
+
" 2>/dev/null) || SUBJECT=$(echo "$MSG" | head -1)
|
|
525
|
+
else
|
|
526
|
+
SUBJECT=$(echo "$MSG" | head -1)
|
|
527
|
+
fi
|
|
528
|
+
# Single source of truth for the accepted commit-type list (#3811): the
|
|
529
|
+
# 10 built-ins plus whatever passed the safe-token filter above. Both the
|
|
530
|
+
# regex alternation and the human-readable error text below are derived
|
|
531
|
+
# from this ONE array — no hand-synced second copy.
|
|
532
|
+
#
|
|
533
|
+
# The `"${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}"` form (not
|
|
534
|
+
# plain `"${EXTRA_COMMIT_TYPES[@]}"`) is required: on bash 3.2.57 (this
|
|
535
|
+
# repo's macOS test target), expanding `[@]` on an array that is declared
|
|
536
|
+
# but has zero elements throws "unbound variable" under `set -u` (which
|
|
537
|
+
# this script has via `set -euo pipefail`). Verified directly against
|
|
538
|
+
# /bin/bash 3.2.57 on macOS. The `${arr[@]+word}` form is the
|
|
539
|
+
# nounset-safe idiom for "expand if set, empty otherwise" on empty arrays.
|
|
540
|
+
COMMIT_TYPES=("${BUILTIN_COMMIT_TYPES[@]}" "${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}")
|
|
541
|
+
COMMIT_TYPE_ALT=$(IFS='|'; echo "${COMMIT_TYPES[*]}")
|
|
542
|
+
COMMIT_TYPE_LIST=$(printf '%s, ' "${COMMIT_TYPES[@]}")
|
|
543
|
+
COMMIT_TYPE_LIST="${COMMIT_TYPE_LIST%, }"
|
|
544
|
+
# Typed `valid_types` array (#3811 review finding): CONTRIBUTING.md bans
|
|
545
|
+
# substring/prose matching on `reason` in tests — a test needing to
|
|
546
|
+
# verify the accepted-type set must have a typed field, not grep prose.
|
|
547
|
+
# Safe to build with a bare printf (no JSON-escaping needed): every
|
|
548
|
+
# element of COMMIT_TYPES has already passed the `^[a-z][a-z0-9-]*$`
|
|
549
|
+
# safe-token filter (or is a literal built-in), so none can contain `"`
|
|
550
|
+
# or `\`.
|
|
551
|
+
COMMIT_TYPES_JSON=$(printf '"%s",' "${COMMIT_TYPES[@]}")
|
|
552
|
+
COMMIT_TYPES_JSON="[${COMMIT_TYPES_JSON%,}]"
|
|
43
553
|
# Validate Conventional Commits format
|
|
44
|
-
if ! [[ "$SUBJECT" =~ ^(
|
|
45
|
-
# Emit
|
|
46
|
-
# on the stable code string
|
|
47
|
-
|
|
554
|
+
if ! [[ "$SUBJECT" =~ ^($COMMIT_TYPE_ALT)(\(.+\))?:[[:space:]].+ ]]; then
|
|
555
|
+
# Emit typed `code` and `valid_types` fields alongside `reason` (#2974,
|
|
556
|
+
# #3811). Tests assert on the stable code string and the typed array;
|
|
557
|
+
# the reason is the human-readable copy, never grepped by tests.
|
|
558
|
+
echo "{\"decision\": \"block\", \"code\": \"CONVENTIONAL_COMMITS_VIOLATION\", \"valid_types\": $COMMIT_TYPES_JSON, \"reason\": \"Commit message must follow Conventional Commits: <type>(<scope>): <subject>. Valid types: $COMMIT_TYPE_LIST. Subject must be <=72 chars, lowercase, imperative mood, no trailing period.\"}"
|
|
48
559
|
exit 2
|
|
49
560
|
fi
|
|
50
561
|
if [ ${#SUBJECT} -gt 72 ]; then
|
|
@@ -47,6 +47,16 @@
|
|
|
47
47
|
|
|
48
48
|
'use strict';
|
|
49
49
|
|
|
50
|
+
const { allow, deny } = require('./lib/hook-exit.js');
|
|
51
|
+
|
|
52
|
+
// #3911 (ADR-3889 Phase 7): the exit(2) call site (block(), below) is
|
|
53
|
+
// migrated to hook-exit.js's deny(undefined, reason) now that
|
|
54
|
+
// hooks/lib/cli-exit.js's terminateNow emits fd 1 and fd 2 (stderrPayload)
|
|
55
|
+
// in INDEPENDENT try/catch blocks — `payload=undefined` cleanly skips the
|
|
56
|
+
// fd 1 write (preserving "nothing written to stdout") instead of throwing
|
|
57
|
+
// into a shared catch that used to also swallow the fd 2 write, which is
|
|
58
|
+
// what silently dropped the deny reason before this fix.
|
|
59
|
+
|
|
50
60
|
// No realistic destructive command comes anywhere close to this length.
|
|
51
61
|
const MAX_COMMAND_LENGTH = 4096;
|
|
52
62
|
|
|
@@ -244,16 +254,11 @@ function destructiveReason(cmd) {
|
|
|
244
254
|
}
|
|
245
255
|
|
|
246
256
|
function block(reason) {
|
|
247
|
-
|
|
248
|
-
process.exit(2);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function allow() {
|
|
252
|
-
process.exit(0);
|
|
257
|
+
deny(undefined, `GSD windsurf pre_run_command guard: ${reason}\n`);
|
|
253
258
|
}
|
|
254
259
|
|
|
255
260
|
let input = '';
|
|
256
|
-
const stdinTimeout = setTimeout(() =>
|
|
261
|
+
const stdinTimeout = setTimeout(() => allow(undefined), 10000);
|
|
257
262
|
process.stdin.setEncoding('utf8');
|
|
258
263
|
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
259
264
|
process.stdin.on('end', () => {
|
|
@@ -262,14 +267,14 @@ process.stdin.on('end', () => {
|
|
|
262
267
|
const data = JSON.parse(input || '{}');
|
|
263
268
|
const toolInfo = (data && typeof data.tool_info === 'object' && data.tool_info) || {};
|
|
264
269
|
const commandLine = typeof toolInfo.command_line === 'string' ? toolInfo.command_line : '';
|
|
265
|
-
if (!commandLine) { allow(); return; }
|
|
266
|
-
if (commandLine.length > MAX_COMMAND_LENGTH) { allow(); return; }
|
|
270
|
+
if (!commandLine) { allow(undefined); return; }
|
|
271
|
+
if (commandLine.length > MAX_COMMAND_LENGTH) { allow(undefined); return; }
|
|
267
272
|
|
|
268
273
|
const reason = destructiveReason(commandLine);
|
|
269
274
|
if (reason) { block(reason); return; }
|
|
270
|
-
allow();
|
|
275
|
+
allow(undefined);
|
|
271
276
|
} catch {
|
|
272
277
|
// Silent fail-open — never block a valid tool call due to a hook bug.
|
|
273
|
-
allow();
|
|
278
|
+
allow(undefined);
|
|
274
279
|
}
|
|
275
280
|
});
|