@opengsd/gsd-core 1.11.0 → 1.12.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/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 +17 -9
- 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 +15 -55
- package/agents/gsd-planner.md +6 -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 +63 -4
- package/agents/gsd-ui-researcher.md +41 -3
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +609 -134
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/quick.md +8 -4
- package/gsd-core/bin/gsd-tools.cjs +567 -51
- 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-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +336 -95
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-validator.cjs +205 -18
- package/gsd-core/bin/lib/check-command-router.cjs +145 -5
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- 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-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +543 -44
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +118 -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 +84 -7
- package/gsd-core/bin/lib/edge-probe.cjs +9 -1
- 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/frontmatter.cjs +840 -305
- package/gsd-core/bin/lib/gap-checker.cjs +27 -3
- package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
- 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 +39 -6
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +120 -41
- package/gsd-core/bin/lib/install-engine.cjs +68 -3
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
- package/gsd-core/bin/lib/install-profiles.cjs +78 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -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 +160 -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/markdown-table.cjs +52 -4
- package/gsd-core/bin/lib/milestone.cjs +90 -5
- 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-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +70 -4
- 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 +405 -84
- package/gsd-core/bin/lib/plan-document.cjs +263 -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 +18 -14
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
- 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/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 +9 -9
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
- package/gsd-core/bin/lib/roadmap.cjs +74 -19
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
- 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 +71 -8
- package/gsd-core/bin/lib/smart-entry.cjs +12 -22
- 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 +186 -0
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +517 -101
- package/gsd-core/bin/lib/state.cjs +946 -163
- package/gsd-core/bin/lib/surface.cjs +10 -2
- 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/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +1376 -125
- 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/validate-command-router.cjs +2 -2
- 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.cjs +14 -1
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +95 -40
- 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 +177 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -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/references/agent-contracts.md +3 -2
- 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 +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +1 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/failing-direction.md +78 -0
- 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/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/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-revision.md +1 -1
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +37 -8
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +1 -3
- 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/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 +3 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +3 -3
- package/gsd-core/workflows/add-tests.md +3 -8
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +4 -9
- package/gsd-core/workflows/audit-fix.md +12 -3
- package/gsd-core/workflows/audit-milestone.md +9 -9
- package/gsd-core/workflows/audit-uat.md +17 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +10 -26
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +1 -1
- package/gsd-core/workflows/code-review.md +121 -40
- package/gsd-core/workflows/complete-milestone.md +15 -10
- package/gsd-core/workflows/debug.md +5 -3
- package/gsd-core/workflows/diagnose-issues.md +12 -6
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
- package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +3 -6
- package/gsd-core/workflows/docs-update.md +5 -4
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -9
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -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 +22 -4
- 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 +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase.md +38 -54
- package/gsd-core/workflows/execute-plan.md +17 -12
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +2 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +3 -6
- package/gsd-core/workflows/import.md +14 -11
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +44 -11
- package/gsd-core/workflows/insert-phase.md +5 -5
- package/gsd-core/workflows/list-seeds.md +5 -3
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +12 -23
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +2 -2
- package/gsd-core/workflows/new-milestone.md +9 -21
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +12 -26
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -2
- package/gsd-core/workflows/pause-work.md +1 -1
- 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 +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
- package/gsd-core/workflows/plan-phase.md +121 -42
- package/gsd-core/workflows/plan-review-convergence.md +46 -9
- package/gsd-core/workflows/plant-seed.md +2 -2
- package/gsd-core/workflows/pr-branch.md +187 -51
- package/gsd-core/workflows/profile-user.md +16 -14
- package/gsd-core/workflows/progress.md +27 -12
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +20 -29
- package/gsd-core/workflows/remove-phase.md +4 -4
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +8 -12
- package/gsd-core/workflows/review.md +193 -15
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +2 -2
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +3 -5
- package/gsd-core/workflows/ship.md +12 -6
- package/gsd-core/workflows/sketch-wrap-up.md +11 -17
- package/gsd-core/workflows/sketch.md +12 -18
- package/gsd-core/workflows/smart-entry.md +3 -5
- package/gsd-core/workflows/spec-phase.md +23 -1
- package/gsd-core/workflows/spike-wrap-up.md +7 -11
- package/gsd-core/workflows/spike.md +20 -31
- package/gsd-core/workflows/stats.md +2 -2
- package/gsd-core/workflows/sync-skills.md +1 -1
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +5 -5
- package/gsd-core/workflows/ui-phase.md +10 -16
- package/gsd-core/workflows/ui-review.md +6 -10
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +6 -10
- package/gsd-core/workflows/validate-phase.md +2 -2
- 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 +57 -18
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +19 -10
- 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 +76 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +16 -7
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +17 -8
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +215 -26
- package/hooks/dist/gsd-validate-commit.sh +80 -6
- 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 +34 -16
- 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-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +19 -10
- 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 +76 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +16 -7
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +17 -8
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +215 -26
- package/hooks/gsd-validate-commit.sh +80 -6
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +34 -16
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +12 -7
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +5 -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 +45 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +396 -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 +134 -1
- 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/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 +193 -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 +21 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-removed-but-needed.cjs +184 -16
- 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 +122 -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/mutation-matrix.cjs +599 -50
- package/scripts/prompt-injection-scan.sh +75 -14
- 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-import/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- 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
|
@@ -9,6 +9,14 @@ const os = require('os');
|
|
|
9
9
|
// Namespace (not destructured) so tests can inject spawn failures by
|
|
10
10
|
// monkeypatching childProcess.execFileSync.
|
|
11
11
|
const childProcess = require('child_process');
|
|
12
|
+
const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
|
|
13
|
+
|
|
14
|
+
// This hook's build-seam outer catch (require.main guard just below) has
|
|
15
|
+
// always exited 0 (fail open — the statusline renders on EVERY prompt, so a
|
|
16
|
+
// build failure must degrade to a blank line rather than break Claude Code's
|
|
17
|
+
// per-render hook). Declared ONCE so that catch's crash() call states its
|
|
18
|
+
// policy explicitly rather than inheriting a default (#3911).
|
|
19
|
+
const ON_CRASH = HOOK_ON_CRASH.ALLOW;
|
|
12
20
|
// #3582: gsd-core/bin/lib/*.cjs (semver-compare.cjs, state-document.cjs,
|
|
13
21
|
// active-workstream-store.cjs, planning-workspace.cjs — required below) and
|
|
14
22
|
// package-identity.cjs are tsc build artifacts (ADR-457), gitignored and
|
|
@@ -23,8 +31,12 @@ if (require.main === module) {
|
|
|
23
31
|
const { ensureRuntimeBuild } = require('../gsd-core/bin/ensure-runtime-build.cjs');
|
|
24
32
|
ensureRuntimeBuild();
|
|
25
33
|
} catch (e) {
|
|
26
|
-
|
|
27
|
-
process.exit(0)
|
|
34
|
+
// #3911: crash(ON_CRASH, ...) with an undefined payload preserves the
|
|
35
|
+
// pre-migration `process.stdout.write(''); process.exit(0);` byte-for-
|
|
36
|
+
// byte — undefined makes terminateNow's stdout JSON.stringify throw
|
|
37
|
+
// internally (swallowed there), so fd 1 stays untouched, same as writing
|
|
38
|
+
// an explicit empty string did.
|
|
39
|
+
crash(ON_CRASH, undefined);
|
|
28
40
|
}
|
|
29
41
|
}
|
|
30
42
|
const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
|
|
@@ -169,13 +181,27 @@ function readStateFileOrNull(statePath) {
|
|
|
169
181
|
* - null when no .planning marker is found at all (GSD not present), or
|
|
170
182
|
* when a workstream DOES resolve but its STATE.md doesn't exist yet
|
|
171
183
|
* (negative space: mirrors flat-mode's own silent pre-STATE.md window)
|
|
184
|
+
*
|
|
185
|
+
* @param {string} dir
|
|
186
|
+
* @param {{ stateFreshness?: boolean }} [opts] — #2734, additive/default-off.
|
|
187
|
+
* When true and the resolved state carries a truthy `.stateHead`, attaches
|
|
188
|
+
* `state.freshness` (deriveStateFreshness) before returning — `current` at
|
|
189
|
+
* that point is the project root the walk resolved, which is what AC-4's
|
|
190
|
+
* repo-pinning check needs. Never derived when false or the stamp is
|
|
191
|
+
* absent, so existing callers (default opts) spend zero extra spawns.
|
|
172
192
|
*/
|
|
173
|
-
function readGsdState(dir) {
|
|
193
|
+
function readGsdState(dir, opts = {}) {
|
|
194
|
+
const { stateFreshness = false } = opts;
|
|
174
195
|
const home = os.homedir();
|
|
175
196
|
let current = dir;
|
|
176
197
|
for (let i = 0; i < 10; i++) {
|
|
177
198
|
const flatState = readStateFileOrNull(path.join(current, '.planning', 'STATE.md'));
|
|
178
|
-
if (flatState !== null)
|
|
199
|
+
if (flatState !== null) {
|
|
200
|
+
if (stateFreshness && flatState.stateHead) {
|
|
201
|
+
flatState.freshness = deriveStateFreshness(current, flatState.stateHead);
|
|
202
|
+
}
|
|
203
|
+
return flatState;
|
|
204
|
+
}
|
|
179
205
|
|
|
180
206
|
if (listAvailableWorkstreams(current).length > 0) {
|
|
181
207
|
let resolvedWs = null;
|
|
@@ -187,7 +213,11 @@ function readGsdState(dir) {
|
|
|
187
213
|
|
|
188
214
|
if (!resolvedWs) return { noActiveWorkstream: true };
|
|
189
215
|
|
|
190
|
-
|
|
216
|
+
const wsState = readStateFileOrNull(planningPaths(current, resolvedWs).state);
|
|
217
|
+
if (wsState !== null && stateFreshness && wsState.stateHead) {
|
|
218
|
+
wsState.freshness = deriveStateFreshness(current, wsState.stateHead);
|
|
219
|
+
}
|
|
220
|
+
return wsState;
|
|
191
221
|
}
|
|
192
222
|
|
|
193
223
|
const parent = path.dirname(current);
|
|
@@ -238,6 +268,10 @@ function parseStateMd(content) {
|
|
|
238
268
|
if (key === 'active_phase') state.activePhase = (v === 'null' || v === '') ? null : v;
|
|
239
269
|
// next_action: recommended command when idle (discuss-phase / plan-phase / execute-phase / verify-phase)
|
|
240
270
|
if (key === 'next_action') state.nextAction = (v === 'null' || v === '') ? null : v;
|
|
271
|
+
// #2734: state_head — the commit STATE.md was written against, consumed
|
|
272
|
+
// by deriveStateFreshness() below. Mirrors active_phase/next_action's
|
|
273
|
+
// null/empty handling exactly.
|
|
274
|
+
if (key === 'state_head') state.stateHead = (v === 'null' || v === '') ? null : v;
|
|
241
275
|
}
|
|
242
276
|
// next_phases supports both flow array and block-list YAML forms.
|
|
243
277
|
const npFlowMatch = fm.match(/^next_phases:\s*\[([^\]]*)\]/m);
|
|
@@ -371,6 +405,10 @@ function formatGsdState(s) {
|
|
|
371
405
|
}
|
|
372
406
|
}
|
|
373
407
|
|
|
408
|
+
// #2734: STATE.md freshness marker — opt-in, appended last.
|
|
409
|
+
const fresh = formatStateFreshness(s.freshness);
|
|
410
|
+
if (fresh) parts.push(fresh);
|
|
411
|
+
|
|
374
412
|
return parts.join(' · ');
|
|
375
413
|
}
|
|
376
414
|
|
|
@@ -476,6 +514,10 @@ function formatGsdStateCompact(s) {
|
|
|
476
514
|
}
|
|
477
515
|
}
|
|
478
516
|
|
|
517
|
+
// #2734: STATE.md freshness marker \u2014 opt-in, appended last.
|
|
518
|
+
const fresh = formatStateFreshness(s.freshness);
|
|
519
|
+
if (fresh) parts.push(fresh);
|
|
520
|
+
|
|
479
521
|
return parts.join(' \u00b7 ');
|
|
480
522
|
}
|
|
481
523
|
|
|
@@ -572,13 +614,153 @@ function buildGitSegment(info) {
|
|
|
572
614
|
return ` │ \x1b[2m${info.branch}\x1b[0m${state}`;
|
|
573
615
|
}
|
|
574
616
|
|
|
617
|
+
// --- STATE.md freshness marker (opt-in, #2734) --------------------------------
|
|
618
|
+
//
|
|
619
|
+
// Opt-in via `statusline.show_state_freshness: true`. Renders `state ~N
|
|
620
|
+
// commits back` inside the GSD-state segment when STATE.md's `state_head`
|
|
621
|
+
// stamp (#2573) is at least STATE_HEAD_ADVISORY_COMMITS commits behind HEAD.
|
|
622
|
+
// Same impure-reader -> pure-IR -> pure-formatter shape as the git segment
|
|
623
|
+
// above. See .gsd/phase/feat-2734-statusline-state-freshness/40-design.md.
|
|
624
|
+
|
|
625
|
+
// Deliberate mirror of the fence in src/state.cts (STATE_HEAD_HASH_RE) — kept
|
|
626
|
+
// hook-side rather than requiring state.cjs on the per-render path (measured
|
|
627
|
+
// ~20ms; see design doc "Laws that apply"). tests/gsd-statusline.test.cjs
|
|
628
|
+
// asserts behavioral parity against readStateHeadFreshness rather than
|
|
629
|
+
// comparing source (local/no-source-grep forbids the latter anyway).
|
|
630
|
+
const STATE_HEAD_HASH_RE = /^[0-9a-f]{4,40}$/i;
|
|
631
|
+
|
|
632
|
+
// Mirror of the constant verify.cts's W024 health check thresholds on
|
|
633
|
+
// (STATE_HEAD_ADVISORY_COMMITS). A test asserts equality with verify.cjs's
|
|
634
|
+
// export so the two copies can't drift.
|
|
635
|
+
const STATE_HEAD_ADVISORY_COMMITS = 20;
|
|
636
|
+
|
|
637
|
+
// Same bound class as GIT_STATUS_TIMEOUT_MS above.
|
|
638
|
+
const STATE_FRESHNESS_GIT_TIMEOUT_MS = 1500;
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Pure function: does raw pass the state_head hash fence? Must run BEFORE any
|
|
642
|
+
* value from STATE.md reaches a git argv slot.
|
|
643
|
+
*/
|
|
644
|
+
function isValidStateHeadStamp(raw) {
|
|
645
|
+
return typeof raw === 'string' && STATE_HEAD_HASH_RE.test(raw.trim());
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Run `git rev-list --left-right --count <stamp>...HEAD` in root. Returns raw
|
|
650
|
+
* stdout, or null when git is missing, root isn't a repo, the stamp is
|
|
651
|
+
* unknown, or the call times out. Never throws. Only call with a stamp that
|
|
652
|
+
* already passed isValidStateHeadStamp/the hash fence above.
|
|
653
|
+
*/
|
|
654
|
+
function readStateHeadCommits(root, stamp) {
|
|
655
|
+
try {
|
|
656
|
+
return childProcess.execFileSync('git',
|
|
657
|
+
['-C', root, 'rev-list', '--left-right', '--count', `${stamp}...HEAD`],
|
|
658
|
+
{ encoding: 'utf8', timeout: STATE_FRESHNESS_GIT_TIMEOUT_MS,
|
|
659
|
+
stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
660
|
+
} catch (e) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Pure function: parse `git rev-list --left-right --count A...B` output
|
|
667
|
+
* ("<left>\t<right>"). Returns { left, right } as non-negative integers, or
|
|
668
|
+
* null when text isn't a matching string (covers null, '', 'garbage', '1',
|
|
669
|
+
* 'a\tb', '\t', and any other unparseable shape).
|
|
670
|
+
*/
|
|
671
|
+
function parseRevListCounts(text) {
|
|
672
|
+
if (typeof text !== 'string') return null;
|
|
673
|
+
const m = text.match(/^(\d+)\s+(\d+)\s*$/);
|
|
674
|
+
if (!m) return null;
|
|
675
|
+
return { left: parseInt(m[1], 10), right: parseInt(m[2], 10) };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Impure -> pure IR: derive the freshness signal for a recorded state_head
|
|
680
|
+
* stamp. Returns { state_head, commits_behind, commit_stale } — never throws,
|
|
681
|
+
* every unresolvable input degrades to the all-null-but-state_head shape.
|
|
682
|
+
*
|
|
683
|
+
* Order (each failure returns immediately, no further work):
|
|
684
|
+
* a. hash fence — malformed/absent stamp never reaches a spawn
|
|
685
|
+
* b. repo pinning — root must own its own .git (mirrors projectOwnsItsRepo
|
|
686
|
+
* in src/state.cts: a filesystem-identity check, not a --show-toplevel
|
|
687
|
+
* string compare, which is unreliable on macOS /private/var and Windows
|
|
688
|
+
* 8.3 paths). Costs no subprocess.
|
|
689
|
+
* c. sub_repos guard — a planning.sub_repos workspace's outer HEAD never
|
|
690
|
+
* advances when code lands in nested children, so a "fresh" answer here
|
|
691
|
+
* would be a confident lie. Costs no subprocess.
|
|
692
|
+
* d. one bounded git spawn: rev-list --left-right --count answers ancestry
|
|
693
|
+
* and distance together. left > 0 means the stamp is not an ancestor of
|
|
694
|
+
* HEAD (reset/rebase/force-push) -> unknown, never "fresh".
|
|
695
|
+
*/
|
|
696
|
+
function deriveStateFreshness(root, stamp, deps = {}) {
|
|
697
|
+
const { existsSync = fs.existsSync, readConfig = readGsdConfig, readCounts = readStateHeadCommits } = deps;
|
|
698
|
+
|
|
699
|
+
const raw = typeof stamp === 'string' ? stamp.trim() : '';
|
|
700
|
+
const valid = STATE_HEAD_HASH_RE.test(raw);
|
|
701
|
+
const state_head = valid ? raw.slice(0, 7) : null;
|
|
702
|
+
const nullResult = { state_head, commits_behind: null, commit_stale: null };
|
|
703
|
+
if (!valid || !root) return nullResult;
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
if (!existsSync(path.join(root, '.git'))) return nullResult;
|
|
707
|
+
} catch (e) {
|
|
708
|
+
return nullResult;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
const cfg = readConfig(root);
|
|
713
|
+
const sub = getConfigValue(cfg, 'planning.sub_repos') ?? getConfigValue(cfg, 'sub_repos');
|
|
714
|
+
if (Array.isArray(sub) && sub.length > 0) return nullResult;
|
|
715
|
+
} catch (e) {
|
|
716
|
+
return nullResult;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const counts = parseRevListCounts(readCounts(root, raw));
|
|
720
|
+
if (!counts || counts.left > 0) return nullResult;
|
|
721
|
+
|
|
722
|
+
return { state_head, commits_behind: counts.right, commit_stale: counts.right > 0 };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Pure function: format the freshness IR into the marker text, or '' below
|
|
727
|
+
* STATE_HEAD_ADVISORY_COMMITS (including when commits_behind is absent/null —
|
|
728
|
+
* the unknown case must never render, never mind alarm on it).
|
|
729
|
+
*/
|
|
730
|
+
function formatStateFreshness(fresh) {
|
|
731
|
+
if (!fresh || typeof fresh.commits_behind !== 'number' || fresh.commits_behind < STATE_HEAD_ADVISORY_COMMITS) return '';
|
|
732
|
+
return `state ~${fresh.commits_behind} commits back`;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Pure function: single source of truth for statusline config resolution.
|
|
737
|
+
* `runStatusline()` and `renderStatusline()` previously read this config
|
|
738
|
+
* independently, which had drifted into a live divergence between the two
|
|
739
|
+
* entry points — this collapses both onto one resolver.
|
|
740
|
+
*
|
|
741
|
+
* @param {object} cfg — parsed .planning/config.json (readGsdConfig())
|
|
742
|
+
* @returns {{ showLastCommand: boolean, position: 'end'|'front', stateFormat: 'full'|'compact', showGit: boolean, showStateFreshness: boolean }}
|
|
743
|
+
*/
|
|
744
|
+
function resolveStatuslineOptions(cfg) {
|
|
745
|
+
const showLastCommand = getConfigValue(cfg, 'statusline.show_last_command') === true;
|
|
746
|
+
const cfgPos = getConfigValue(cfg, 'statusline.context_position');
|
|
747
|
+
// Clamp any non-'front' value (including absent/null) to 'end' — the single
|
|
748
|
+
// source of truth for this default; composeStatusline's own coercion stays
|
|
749
|
+
// as belt-and-suspenders defense for direct callers.
|
|
750
|
+
const position = cfgPos === 'front' ? 'front' : 'end';
|
|
751
|
+
const stateFormat = getConfigValue(cfg, 'statusline.state_format') === 'compact' ? 'compact' : 'full';
|
|
752
|
+
const showGit = getConfigValue(cfg, 'statusline.show_git') === true;
|
|
753
|
+
const showStateFreshness = getConfigValue(cfg, 'statusline.show_state_freshness') === true;
|
|
754
|
+
return { showLastCommand, position, stateFormat, showGit, showStateFreshness };
|
|
755
|
+
}
|
|
756
|
+
|
|
575
757
|
// --- stdin ------------------------------------------------------------------
|
|
576
758
|
|
|
577
759
|
function runStatusline() {
|
|
578
760
|
let input = '';
|
|
579
761
|
// Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on
|
|
580
762
|
// Windows/Git Bash), exit silently instead of hanging. See #775.
|
|
581
|
-
const stdinTimeout = setTimeout(() =>
|
|
763
|
+
const stdinTimeout = setTimeout(() => allow(undefined), 3000);
|
|
582
764
|
process.stdin.setEncoding('utf8');
|
|
583
765
|
process.stdin.on('data', chunk => input += chunk);
|
|
584
766
|
process.stdin.on('end', () => {
|
|
@@ -717,31 +899,34 @@ function runStatusline() {
|
|
|
717
899
|
// Last-slash-command suffix and context_position config (#2538, #2937).
|
|
718
900
|
// Reads the active session transcript for the most recent <command-name> tag.
|
|
719
901
|
// Failure here must never break the statusline — wrap the entire lookup.
|
|
902
|
+
// #2734: config resolution moved to resolveStatuslineOptions() — the single
|
|
903
|
+
// source of truth shared with renderStatusline() below. The two entry
|
|
904
|
+
// points duplicated this resolution byte-for-byte; one copy is what keeps
|
|
905
|
+
// a new key from reaching only one of them.
|
|
720
906
|
let lastCmdSuffix = '';
|
|
721
|
-
let position = 'end';
|
|
722
|
-
let stateFormat = 'full';
|
|
723
907
|
let gitSuffix = '';
|
|
908
|
+
const options = resolveStatuslineOptions(cfg);
|
|
724
909
|
try {
|
|
725
|
-
if (
|
|
910
|
+
if (options.showLastCommand) {
|
|
726
911
|
const transcriptPath = data.transcript_path;
|
|
727
912
|
const lastCmd = readLastSlashCommand(transcriptPath);
|
|
728
913
|
if (lastCmd) {
|
|
729
914
|
lastCmdSuffix = ` │ \x1b[2mlast: /${lastCmd}\x1b[0m`;
|
|
730
915
|
}
|
|
731
916
|
}
|
|
732
|
-
|
|
733
|
-
if (cfgPos != null) position = cfgPos;
|
|
734
|
-
if (getConfigValue(cfg, 'statusline.state_format') === 'compact') stateFormat = 'compact';
|
|
735
|
-
if (getConfigValue(cfg, 'statusline.show_git') === true) {
|
|
917
|
+
if (options.showGit) {
|
|
736
918
|
gitSuffix = buildGitSegment(parseGitStatus(readGitStatus(dir)));
|
|
737
919
|
}
|
|
738
920
|
} catch (e) {
|
|
739
921
|
// Never break the statusline on config/transcript/git errors
|
|
740
922
|
}
|
|
741
923
|
|
|
924
|
+
// #2734: readGsdState is inside `if (!task)` deliberately — when a todo
|
|
925
|
+
// task is in flight the GSD-state segment is not rendered, so spending a
|
|
926
|
+
// freshness git spawn here would spend a subprocess on discarded output.
|
|
742
927
|
if (!task) {
|
|
743
|
-
const state = readGsdState(dir) || {};
|
|
744
|
-
gsdStateStr = stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
|
|
928
|
+
const state = readGsdState(dir, { stateFreshness: options.showStateFreshness }) || {};
|
|
929
|
+
gsdStateStr = options.stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
|
|
745
930
|
}
|
|
746
931
|
|
|
747
932
|
// Output
|
|
@@ -752,7 +937,7 @@ function runStatusline() {
|
|
|
752
937
|
? `\x1b[2m${gsdStateStr}\x1b[0m`
|
|
753
938
|
: null;
|
|
754
939
|
|
|
755
|
-
process.stdout.write(composeStatusline({ gsdUpdate, model, ctx, middle, dirname, lastCmdSuffix, gitSuffix, position }));
|
|
940
|
+
process.stdout.write(composeStatusline({ gsdUpdate, model, ctx, middle, dirname, lastCmdSuffix, gitSuffix, position: options.position }));
|
|
756
941
|
} catch (e) {
|
|
757
942
|
// Silent fail - don't break statusline on parse errors
|
|
758
943
|
}
|
|
@@ -846,6 +1031,9 @@ module.exports = {
|
|
|
846
1031
|
shortGsdStatus, formatGsdStateCompact,
|
|
847
1032
|
compactModelName,
|
|
848
1033
|
readGitStatus, parseGitStatus, buildGitSegment,
|
|
1034
|
+
STATE_HEAD_ADVISORY_COMMITS, isValidStateHeadStamp,
|
|
1035
|
+
readStateHeadCommits, parseRevListCounts, deriveStateFreshness,
|
|
1036
|
+
formatStateFreshness, resolveStatuslineOptions,
|
|
849
1037
|
};
|
|
850
1038
|
|
|
851
1039
|
/**
|
|
@@ -857,30 +1045,31 @@ function renderStatusline(data) {
|
|
|
857
1045
|
const dir = data.workspace?.current_dir || process.cwd();
|
|
858
1046
|
const dirname = path.basename(dir);
|
|
859
1047
|
|
|
1048
|
+
// #2734: config resolution moved to resolveStatuslineOptions() — the single
|
|
1049
|
+
// source of truth shared with runStatusline() above. The two entry points
|
|
1050
|
+
// duplicated this resolution byte-for-byte; one copy is what keeps a new
|
|
1051
|
+
// key from reaching only one of them.
|
|
860
1052
|
let lastCmdSuffix = '';
|
|
861
|
-
let position = 'end';
|
|
862
|
-
let stateFormat = 'full';
|
|
863
1053
|
let gitSuffix = '';
|
|
1054
|
+
let options = { showLastCommand: false, position: 'end', stateFormat: 'full', showGit: false, showStateFreshness: false };
|
|
864
1055
|
try {
|
|
865
1056
|
const cfg = readGsdConfig(dir);
|
|
866
|
-
|
|
1057
|
+
options = resolveStatuslineOptions(cfg);
|
|
1058
|
+
if (options.showLastCommand) {
|
|
867
1059
|
const lastCmd = readLastSlashCommand(data.transcript_path);
|
|
868
1060
|
if (lastCmd) {
|
|
869
1061
|
lastCmdSuffix = ` │ \x1b[2mlast: /${lastCmd}\x1b[0m`;
|
|
870
1062
|
}
|
|
871
1063
|
}
|
|
872
|
-
|
|
873
|
-
if (cfgPos != null) position = cfgPos;
|
|
874
|
-
if (getConfigValue(cfg, 'statusline.state_format') === 'compact') stateFormat = 'compact';
|
|
875
|
-
if (getConfigValue(cfg, 'statusline.show_git') === true) {
|
|
1064
|
+
if (options.showGit) {
|
|
876
1065
|
gitSuffix = buildGitSegment(parseGitStatus(readGitStatus(dir)));
|
|
877
1066
|
}
|
|
878
1067
|
} catch (e) { /* swallow */ }
|
|
879
1068
|
|
|
880
|
-
const state = readGsdState(dir) || {};
|
|
881
|
-
const gsdStateStr = stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
|
|
1069
|
+
const state = readGsdState(dir, { stateFreshness: options.showStateFreshness }) || {};
|
|
1070
|
+
const gsdStateStr = options.stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
|
|
882
1071
|
const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
|
|
883
|
-
return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, gitSuffix, position });
|
|
1072
|
+
return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, gitSuffix, position: options.position });
|
|
884
1073
|
}
|
|
885
1074
|
|
|
886
1075
|
module.exports.renderStatusline = renderStatusline;
|
|
@@ -7,10 +7,43 @@
|
|
|
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
|
|
10
26
|
|
|
11
27
|
# Check opt-in config — exit silently if not enabled
|
|
12
28
|
if [ -f .planning/config.json ]; then
|
|
13
|
-
|
|
29
|
+
ENABLED_ERR=$(mktemp)
|
|
30
|
+
ENABLED=$(node -e "
|
|
31
|
+
try{
|
|
32
|
+
const c=require('./.planning/config.json');
|
|
33
|
+
process.stdout.write(c.hooks?.community===true?'1':'0');
|
|
34
|
+
}catch(e){
|
|
35
|
+
process.stderr.write('CONFIG_READ_FAILED: '+(e&&e.message?e.message:String(e)));
|
|
36
|
+
process.exit(3);
|
|
37
|
+
}
|
|
38
|
+
" 2>"$ENABLED_ERR") || CONFIG_STATUS=$?
|
|
39
|
+
CONFIG_STATUS=${CONFIG_STATUS:-0}
|
|
40
|
+
if [ "$CONFIG_STATUS" != "0" ]; then
|
|
41
|
+
# Could not determine the opt-in flag at all (node missing, JSON parse
|
|
42
|
+
# error other than absence, etc.) — distinct from ".planning/config.json
|
|
43
|
+
# exists and legitimately disables the hook". Say so and pass, per #3838.
|
|
44
|
+
echo "gsd-validate-commit.sh: could not read .planning/config.json (opt-in check) — validator disabled for this call. $(cat "$ENABLED_ERR")" >&2
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
14
47
|
if [ "$ENABLED" != "1" ]; then exit 0; fi
|
|
15
48
|
else
|
|
16
49
|
exit 0
|
|
@@ -19,17 +52,58 @@ fi
|
|
|
19
52
|
INPUT=$(cat)
|
|
20
53
|
|
|
21
54
|
# Extract command from JSON using Node (handles escaping correctly, no jq needed)
|
|
22
|
-
|
|
55
|
+
CMD_ERR=$(mktemp)
|
|
56
|
+
CMD=$(echo "$INPUT" | node -e "
|
|
57
|
+
let d='';
|
|
58
|
+
process.stdin.on('data',c=>d+=c);
|
|
59
|
+
process.stdin.on('end',()=>{
|
|
60
|
+
try{
|
|
61
|
+
process.stdout.write(JSON.parse(d).tool_input?.command||'');
|
|
62
|
+
}catch(e){
|
|
63
|
+
process.stderr.write('COMMAND_EXTRACTION_FAILED: '+(e&&e.message?e.message:String(e)));
|
|
64
|
+
process.exit(3);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
" 2>"$CMD_ERR") || CMD_STATUS=$?
|
|
68
|
+
CMD_STATUS=${CMD_STATUS:-0}
|
|
69
|
+
if [ "$CMD_STATUS" != "0" ]; then
|
|
70
|
+
# Could not extract tool_input.command at all (node missing, malformed
|
|
71
|
+
# JSON, etc.) — distinct from "there is genuinely no command field". Say
|
|
72
|
+
# so and pass, per #3838.
|
|
73
|
+
echo "gsd-validate-commit.sh: could not extract tool_input.command from the hook payload — validator disabled for this call. $(cat "$CMD_ERR")" >&2
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
23
76
|
|
|
24
77
|
# Only check git commit commands.
|
|
25
78
|
# Delegates to hooks/lib/git-cmd.js isGitSubcommand() — the canonical token-walk
|
|
26
79
|
# classifier that handles env-prefix, -C path, and full-path git invocations.
|
|
27
80
|
# A naive `^git\s+commit` regex misses all three; this guard fixes that (#3129).
|
|
28
81
|
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
82
|
+
CLASSIFY_ERR=$(mktemp)
|
|
83
|
+
GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" node -e "
|
|
84
|
+
try {
|
|
85
|
+
const {isGitSubcommand}=require(process.env.GIT_CMD_LIB);
|
|
86
|
+
process.exit(isGitSubcommand(process.argv[1],'commit')?0:1);
|
|
87
|
+
} catch(e) {
|
|
88
|
+
process.stderr.write('CLASSIFIER_THREW: '+(e&&e.message?e.message:String(e)));
|
|
89
|
+
process.exit(3);
|
|
90
|
+
}
|
|
91
|
+
" "$CMD" 2>"$CLASSIFY_ERR" || CLASSIFY_STATUS=$?
|
|
92
|
+
CLASSIFY_STATUS=${CLASSIFY_STATUS:-0}
|
|
93
|
+
if [ "$CLASSIFY_STATUS" != "0" ] && [ "$CLASSIFY_STATUS" != "1" ]; then
|
|
94
|
+
# 0 = is a git commit (validate below); 1 = genuinely not a git commit
|
|
95
|
+
# (real negative, pass silently) — the ONLY intentional non-zero exit the
|
|
96
|
+
# script above ever produces on success. Any other status — 127 node
|
|
97
|
+
# missing, or 3 from the try/catch above when the git-cmd.js require chain
|
|
98
|
+
# throws (e.g. its built dependency, gsd-core/bin/lib/token-scanner.cjs, is
|
|
99
|
+
# a gitignored build artifact and absent on a fresh checkout — run
|
|
100
|
+
# `npm run build:lib`) — means the classifier could not run at all. Say so
|
|
101
|
+
# on stderr and pass (#3838): PreToolUse stderr does not disturb the JSON
|
|
102
|
+
# protocol.
|
|
103
|
+
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
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
if [ "$CLASSIFY_STATUS" = "0" ]; then
|
|
33
107
|
# Extract message from -m flag
|
|
34
108
|
MSG=""
|
|
35
109
|
if [[ "$CMD" =~ -m[[:space:]]+\"([^\"]+)\" ]]; 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
|
});
|
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
const fs = require('fs');
|
|
31
31
|
const path = require('path');
|
|
32
32
|
const { spawnSync } = require('child_process');
|
|
33
|
+
const { allow, deny } = require('./lib/hook-exit.js');
|
|
34
|
+
const { reportIfUndetermined } = require('./lib/git-probe.js');
|
|
35
|
+
|
|
36
|
+
// #3911 (ADR-3889 Phase 7): the exit(2) call site (block(), below) is
|
|
37
|
+
// migrated to hook-exit.js's deny(undefined, reason) — see
|
|
38
|
+
// gsd-windsurf-pre-command.js's identical note for the fixed defect
|
|
39
|
+
// (terminateNow's fd 1/fd 2 writes now run in independent try/catch blocks).
|
|
33
40
|
|
|
34
41
|
const SPAWNOPT = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000, windowsHide: true };
|
|
35
42
|
|
|
@@ -54,16 +61,11 @@ function nearestExistingDir(start) {
|
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
function block(reason) {
|
|
57
|
-
|
|
58
|
-
process.exit(2);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function allow() {
|
|
62
|
-
process.exit(0);
|
|
64
|
+
deny(undefined, `GSD windsurf pre_write_code guard: ${reason}\n`);
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
let input = '';
|
|
66
|
-
const stdinTimeout = setTimeout(() =>
|
|
68
|
+
const stdinTimeout = setTimeout(() => allow(undefined), 10000);
|
|
67
69
|
process.stdin.setEncoding('utf8');
|
|
68
70
|
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
69
71
|
process.stdin.on('end', () => {
|
|
@@ -72,14 +74,19 @@ process.stdin.on('end', () => {
|
|
|
72
74
|
const data = JSON.parse(input || '{}');
|
|
73
75
|
const toolInfo = (data && typeof data.tool_info === 'object' && data.tool_info) || {};
|
|
74
76
|
const rawFilePath = typeof toolInfo.file_path === 'string' ? toolInfo.file_path : '';
|
|
75
|
-
if (!rawFilePath) { allow(); return; }
|
|
77
|
+
if (!rawFilePath) { allow(undefined); return; }
|
|
76
78
|
|
|
77
79
|
const cwd = process.cwd();
|
|
78
80
|
|
|
79
81
|
// Determine the active project's git root. No git root at all -> nothing
|
|
80
82
|
// to enforce a boundary against -> fail open.
|
|
81
83
|
const cwdTopResult = git(['rev-parse', '--show-toplevel'], cwd);
|
|
82
|
-
|
|
84
|
+
// #3911: a timeout/spawn-failure result is indistinguishable from a clean
|
|
85
|
+
// "no git root here" answer by status/stdout alone — reportIfUndetermined
|
|
86
|
+
// is a no-op on a genuine negative and only fires when the probe itself
|
|
87
|
+
// could not run. The allow() below is UNCHANGED either way.
|
|
88
|
+
reportIfUndetermined('gsd-windsurf-pre-write', 'git rev-parse --show-toplevel (cwd)', cwdTopResult);
|
|
89
|
+
if (cwdTopResult.status !== 0 || !cwdTopResult.stdout) { allow(undefined); return; }
|
|
83
90
|
const cwdTopRaw = cwdTopResult.stdout.trim();
|
|
84
91
|
|
|
85
92
|
const filePath = path.isAbsolute(rawFilePath) ? path.resolve(rawFilePath) : path.resolve(cwd, rawFilePath);
|
|
@@ -95,14 +102,16 @@ process.stdin.on('end', () => {
|
|
|
95
102
|
}
|
|
96
103
|
})(),
|
|
97
104
|
);
|
|
98
|
-
if (!checkDir) { allow(); return; } // synthetic path with no existing ancestor — fail open
|
|
105
|
+
if (!checkDir) { allow(undefined); return; } // synthetic path with no existing ancestor — fail open
|
|
99
106
|
|
|
100
107
|
const fileTopResult = git(['rev-parse', '--show-toplevel'], checkDir);
|
|
108
|
+
reportIfUndetermined('gsd-windsurf-pre-write', 'git rev-parse --show-toplevel (file location)', fileTopResult);
|
|
101
109
|
if (fileTopResult.status !== 0 || !fileTopResult.stdout) {
|
|
102
110
|
// Not inside any git worktree. Distinguish "inside a .git/ internals
|
|
103
111
|
// directory" (dangerous — BLOCK) from "outside all git repos entirely"
|
|
104
112
|
// (not the escape vector this guard targets — fail open).
|
|
105
113
|
const insideGitDir = git(['rev-parse', '--is-inside-git-dir'], checkDir);
|
|
114
|
+
reportIfUndetermined('gsd-windsurf-pre-write', 'git rev-parse --is-inside-git-dir', insideGitDir);
|
|
106
115
|
if (insideGitDir.status === 0 && insideGitDir.stdout && insideGitDir.stdout.trim() === 'true') {
|
|
107
116
|
block(
|
|
108
117
|
`'${filePath}' is inside a git internal (.git) directory, not the active project at ` +
|
|
@@ -111,12 +120,12 @@ process.stdin.on('end', () => {
|
|
|
111
120
|
);
|
|
112
121
|
return;
|
|
113
122
|
}
|
|
114
|
-
allow();
|
|
123
|
+
allow(undefined);
|
|
115
124
|
return;
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
const fileTopRaw = fileTopResult.stdout.trim();
|
|
119
|
-
if (fileTopRaw === cwdTopRaw) { allow(); return; }
|
|
128
|
+
if (fileTopRaw === cwdTopRaw) { allow(undefined); return; }
|
|
120
129
|
|
|
121
130
|
// BLOCK: file resolves to a different git root than the active project.
|
|
122
131
|
block(
|
|
@@ -127,6 +136,6 @@ process.stdin.on('end', () => {
|
|
|
127
136
|
);
|
|
128
137
|
} catch {
|
|
129
138
|
// Silent fail-open — never block a valid tool call due to a hook bug.
|
|
130
|
-
allow();
|
|
139
|
+
allow(undefined);
|
|
131
140
|
}
|
|
132
141
|
});
|