@opengsd/gsd-core 1.10.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 +12 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +22 -14
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +5 -3
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +23 -2
- package/agents/gsd-plan-checker.md +50 -53
- package/agents/gsd-planner.md +50 -50
- 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-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +13 -4
- package/bin/install.js +1448 -1103
- package/commands/gsd/code-review.md +1 -1
- 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/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +9 -5
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1035 -138
- package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
- package/gsd-core/bin/lib/api-coverage.cjs +33 -14
- package/gsd-core/bin/lib/artifacts.cjs +5 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +1026 -268
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +845 -130
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +700 -40
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +216 -42
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- 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 +735 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +1172 -108
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
- package/gsd-core/bin/lib/config-loader.cjs +187 -23
- package/gsd-core/bin/lib/config.cjs +102 -3
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +208 -33
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- 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/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +899 -229
- package/gsd-core/bin/lib/gap-checker.cjs +95 -10
- package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +268 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +187 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +439 -168
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +811 -259
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
- package/gsd-core/bin/lib/install-profiles.cjs +212 -61
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- 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 +148 -38
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +170 -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-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +183 -22
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +842 -73
- package/gsd-core/bin/lib/model-catalog.cjs +232 -16
- package/gsd-core/bin/lib/model-resolver.cjs +193 -68
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
- package/gsd-core/bin/lib/phase-id.cjs +514 -40
- package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
- package/gsd-core/bin/lib/phase-locator.cjs +262 -34
- package/gsd-core/bin/lib/phase.cjs +1038 -214
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +98 -3
- 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-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
- package/gsd-core/bin/lib/probe-core.cjs +5 -2
- package/gsd-core/bin/lib/profile-output.cjs +1 -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 +766 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
- package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +442 -96
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
- package/gsd-core/bin/lib/smart-entry.cjs +133 -23
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +52 -19
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +338 -8
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +846 -176
- package/gsd-core/bin/lib/state.cjs +2589 -369
- package/gsd-core/bin/lib/surface.cjs +33 -11
- 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/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +67 -23
- package/gsd-core/bin/lib/uat.cjs +1761 -167
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
- package/gsd-core/bin/lib/unusable-input.cjs +37 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +75 -0
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +272 -9
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +453 -918
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
- 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 +44 -26
- 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 +39 -21
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +4 -6
- package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +17 -11
- 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 +5 -4
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +61 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- 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-guidance.md +3 -9
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -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 +44 -13
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/specless-probe-fallback.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 +5 -5
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -17
- 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/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +5 -4
- package/gsd-core/templates/state.md +11 -4
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +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 +13 -20
- 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 +11 -27
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +64 -5
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
- package/gsd-core/workflows/code-review-fix.md +38 -11
- package/gsd-core/workflows/code-review.md +159 -52
- package/gsd-core/workflows/complete-milestone.md +151 -23
- package/gsd-core/workflows/debug.md +12 -8
- package/gsd-core/workflows/diagnose-issues.md +47 -15
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- 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 +4 -3
- 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 +27 -2
- package/gsd-core/workflows/eval-review.md +7 -14
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
- 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 +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +24 -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 +72 -100
- package/gsd-core/workflows/execute-plan.md +52 -15
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +76 -10
- package/gsd-core/workflows/import.md +18 -15
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +49 -16
- 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 +8 -5
- package/gsd-core/workflows/new-milestone.md +22 -29
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +26 -40
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +14 -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 +162 -59
- package/gsd-core/workflows/plan-review-convergence.md +96 -11
- 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 +61 -18
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
- package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
- package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +55 -44
- 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 +219 -20
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +69 -7
- package/gsd-core/workflows/ship.md +116 -50
- 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 +53 -13
- 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 +64 -9
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +49 -14
- package/gsd-core/workflows/ui-phase.md +15 -21
- package/gsd-core/workflows/ui-review.md +8 -12
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +7 -11
- 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 +66 -25
- package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- 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 +2 -3
- 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 +83 -3
- package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
- 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 +37 -27
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +55 -32
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +231 -24
- package/hooks/dist/gsd-update-banner.js +22 -1
- 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 +162 -46
- 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 +92 -59
- 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 +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +158 -30
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- 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 +2 -3
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +83 -3
- package/hooks/gsd-cursor-subagent-stop.js +6 -3
- 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 +37 -27
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +55 -32
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +231 -24
- package/hooks/gsd-update-banner.js +22 -1
- 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 +162 -46
- 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-cmd.js +92 -59
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +28 -11
- package/pi/gsd.cjs +19 -5
- package/scripts/base64-scan.sh +74 -12
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/build-hooks.js +5 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -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 +64 -14
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +11 -21
- package/scripts/gen-context-index.cjs +2 -11
- 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-health-docs.cjs +390 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +138 -25
- package/scripts/gen-registry.cjs +3 -14
- 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/alias-drift-families.cjs +46 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +308 -0
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- 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 +38 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +492 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +471 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +488 -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-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +950 -0
- package/scripts/lint-test-file-count.allowlist.json +137 -8
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +830 -0
- package/scripts/lint-vendored-deps.cjs +297 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +100 -14
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +24 -7
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +9 -5
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -107
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared truncation-detection helper for `gh pr view --json files` (#3211).
|
|
5
|
+
*
|
|
6
|
+
* GitHub's GraphQL `files` connection on a pull request is a paginated
|
|
7
|
+
* connection. `gh pr view --json files` requests it with a single page at
|
|
8
|
+
* `first: 100` and does NOT paginate through the rest — so any PR touching
|
|
9
|
+
* more than 100 files silently returns only the first 100, with no error and
|
|
10
|
+
* no indication in the `files` array itself that entries are missing.
|
|
11
|
+
* `changedFiles` (a separate, non-paginated scalar field) reports the true
|
|
12
|
+
* total. A policy that relaxes a gate based on "every changed file matches
|
|
13
|
+
* pattern X" must treat a `files` list it cannot confirm is complete as
|
|
14
|
+
* incomplete — i.e. fail closed — rather than silently approving a PR whose
|
|
15
|
+
* 101st+ file might violate the policy.
|
|
16
|
+
*
|
|
17
|
+
* This file lives at the top level of scripts/ rather than in scripts/lib/
|
|
18
|
+
* because scripts/lib/** is enumerated in bin/install.js and ships to users
|
|
19
|
+
* on install; this is CI-only tooling with no reason to be installed.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// GitHub's GraphQL `files` connection is requested at `first: 100` by
|
|
23
|
+
// `gh pr view --json files`; `gh` does not paginate this field.
|
|
24
|
+
const FILE_LIST_PAGE_LIMIT = 100;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns true iff `changedFiles` can be trusted as the COMPLETE list of
|
|
28
|
+
* files changed in the PR (i.e. it was not silently truncated).
|
|
29
|
+
*
|
|
30
|
+
* - An empty/non-array list cannot confirm anything about the PR — mirror
|
|
31
|
+
* the fail-closed stance `allPathsAreTooling` already takes on an empty
|
|
32
|
+
* list in scripts/pr-template-policy.cjs.
|
|
33
|
+
* - When the true total is known it is the AUTHORITY, at every list size —
|
|
34
|
+
* not just when the list has hit the page cap. The 100-entry page cap is
|
|
35
|
+
* only ONE way a list can be short of the truth; a `$GITHUB_OUTPUT`
|
|
36
|
+
* heredoc terminated early by an attacker-named file, or a path
|
|
37
|
+
* containing a newline, truncates or inflates the list just as
|
|
38
|
+
* effectively, and at any length. Comparing against the total catches all
|
|
39
|
+
* of them without enumerating the mechanisms.
|
|
40
|
+
* - Only when no total is supplied do we fall back to the page-cap
|
|
41
|
+
* heuristic: a list below the cap cannot have been truncated BY THE CAP,
|
|
42
|
+
* which is the only mechanism a caller without a total can rule out.
|
|
43
|
+
*/
|
|
44
|
+
function fileListIsComplete(changedFiles, changedFilesTotal) {
|
|
45
|
+
if (!Array.isArray(changedFiles) || changedFiles.length === 0) return false;
|
|
46
|
+
if (Number.isInteger(changedFilesTotal)) return changedFilesTotal === changedFiles.length;
|
|
47
|
+
return changedFiles.length < FILE_LIST_PAGE_LIMIT;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parses a newline-delimited env var (as produced by `git diff --name-only`
|
|
52
|
+
* or `gh pr view --json files -q '.files[].path'`) into a trimmed,
|
|
53
|
+
* empty-line-free array. Returns [] for null/undefined/empty input.
|
|
54
|
+
*/
|
|
55
|
+
function parseChangedFilesEnv(value) {
|
|
56
|
+
if (!value) return [];
|
|
57
|
+
return String(value)
|
|
58
|
+
.split(/\r?\n/)
|
|
59
|
+
.map((line) => line.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { fileListIsComplete, FILE_LIST_PAGE_LIMIT, parseChangedFilesEnv };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { matchesGlob } = require('path');
|
|
4
|
+
const { fileListIsComplete, parseChangedFilesEnv } = require('./pr-changed-files.cjs');
|
|
4
5
|
|
|
5
6
|
const TRUSTED_AUTHOR_ASSOCIATIONS = new Set([
|
|
6
7
|
'CONTRIBUTOR',
|
|
@@ -142,8 +143,12 @@ function matchingTemplate(body) {
|
|
|
142
143
|
* pattern in the allowlist. Returns false for an empty file list (no
|
|
143
144
|
* files means we cannot confirm it is a tooling-only PR).
|
|
144
145
|
*/
|
|
145
|
-
function allPathsAreTooling(changedFiles, allowlist) {
|
|
146
|
+
function allPathsAreTooling(changedFiles, allowlist, changedFilesTotal) {
|
|
146
147
|
if (!Array.isArray(changedFiles) || changedFiles.length === 0) return false;
|
|
148
|
+
// #3211: `gh pr view --json files` truncates at 100 entries with no
|
|
149
|
+
// in-band signal. A truncated list must not relax enforcement — an unseen
|
|
150
|
+
// 101st+ file could fall outside the tooling allowlist entirely.
|
|
151
|
+
if (!fileListIsComplete(changedFiles, changedFilesTotal)) return false;
|
|
147
152
|
return changedFiles.every((file) =>
|
|
148
153
|
allowlist.some((pattern) => matchesGlob(file, pattern)),
|
|
149
154
|
);
|
|
@@ -159,13 +164,13 @@ function hasExemptMarker(body, regex) {
|
|
|
159
164
|
return match[1].trim().length > 0;
|
|
160
165
|
}
|
|
161
166
|
|
|
162
|
-
function evaluatePrTemplate(body, authorAssociation, changedFiles) {
|
|
167
|
+
function evaluatePrTemplate(body, authorAssociation, changedFiles, changedFilesTotal) {
|
|
163
168
|
const association = String(authorAssociation || '').toUpperCase();
|
|
164
169
|
const trusted = TRUSTED_AUTHOR_ASSOCIATIONS.has(association);
|
|
165
170
|
const normalizedBody = String(body || '').trim();
|
|
166
171
|
|
|
167
172
|
// --- Carve-out 1: all changed files are in the tooling allowlist ---
|
|
168
|
-
if (allPathsAreTooling(changedFiles, TOOLING_PATH_ALLOWLIST)) {
|
|
173
|
+
if (allPathsAreTooling(changedFiles, TOOLING_PATH_ALLOWLIST, changedFilesTotal)) {
|
|
169
174
|
return {
|
|
170
175
|
valid: true,
|
|
171
176
|
action: 'pass',
|
|
@@ -235,13 +240,18 @@ function evaluatePrTemplate(body, authorAssociation, changedFiles) {
|
|
|
235
240
|
}
|
|
236
241
|
|
|
237
242
|
function main() {
|
|
243
|
+
// Preserve the existing distinction between "unset" (undefined) and
|
|
244
|
+
// "set but empty" ([]) — allPathsAreTooling treats them differently.
|
|
238
245
|
const changedFiles = process.env.CHANGED_FILES
|
|
239
|
-
? process.env.CHANGED_FILES
|
|
246
|
+
? parseChangedFilesEnv(process.env.CHANGED_FILES)
|
|
240
247
|
: undefined;
|
|
248
|
+
const parsedTotal = Number.parseInt(process.env.CHANGED_FILES_TOTAL, 10);
|
|
249
|
+
const changedFilesTotal = Number.isNaN(parsedTotal) ? undefined : parsedTotal;
|
|
241
250
|
const result = evaluatePrTemplate(
|
|
242
251
|
process.env.PR_BODY || '',
|
|
243
252
|
process.env.AUTHOR_ASSOCIATION || '',
|
|
244
253
|
changedFiles,
|
|
254
|
+
changedFilesTotal,
|
|
245
255
|
);
|
|
246
256
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
247
257
|
if (process.env.GITHUB_OUTPUT) {
|
|
@@ -6,12 +6,29 @@
|
|
|
6
6
|
# scripts/prompt-injection-scan.sh --file path/to/file # Scan a single file
|
|
7
7
|
# scripts/prompt-injection-scan.sh --dir agents/ # Scan all files in a directory
|
|
8
8
|
#
|
|
9
|
-
# Exit codes:
|
|
10
|
-
# 0
|
|
11
|
-
# 1
|
|
12
|
-
#
|
|
9
|
+
# Exit codes (ADR-3889, #3908 — registered in gsd-core/bin/shared/exit-codes.json):
|
|
10
|
+
# 0 = clean
|
|
11
|
+
# 1 = findings detected
|
|
12
|
+
# $EXIT_USAGE (64) = usage error (bad argv, missing --file/--dir target)
|
|
13
|
+
# $EXIT_NO_INPUT (66) = ran; scope established; zero files in scope (genuinely empty)
|
|
14
|
+
# $EXIT_UNAVAILABLE (69) = could not establish scope (bad ref, not a repo, unreadable dir)
|
|
13
15
|
set -euo pipefail
|
|
14
16
|
|
|
17
|
+
# ─── Exit-code registry (ADR-3889, #3908) ────────────────────────────────────
|
|
18
|
+
# Resolved relative to THIS script's location, not the caller's cwd. Loud,
|
|
19
|
+
# non-zero failure if the fragment is missing — never fall back to a guessed
|
|
20
|
+
# literal integer, and never let a missing registry silently degrade to
|
|
21
|
+
# exit 0.
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
EXIT_CODES_SH="$SCRIPT_DIR/../gsd-core/bin/shared/exit-codes.sh"
|
|
24
|
+
if [[ ! -f "$EXIT_CODES_SH" ]]; then
|
|
25
|
+
echo "prompt-injection-scan: FATAL: exit-code registry not found at $EXIT_CODES_SH" >&2
|
|
26
|
+
echo " Regenerate with: node scripts/gen-exit-code-registry.cjs --write" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
# shellcheck disable=SC1090
|
|
30
|
+
. "$EXIT_CODES_SH"
|
|
31
|
+
|
|
15
32
|
# ─── Patterns ────────────────────────────────────────────────────────────────
|
|
16
33
|
# Each pattern is a POSIX extended regex. Keep alphabetized by category.
|
|
17
34
|
#
|
|
@@ -72,6 +89,15 @@ PATTERNS=(
|
|
|
72
89
|
# `eval('...')` (single-quoted) silently went undetected on macOS while
|
|
73
90
|
# passing on GNU-grep CI runners. Found auditing #3175; fixed here since it
|
|
74
91
|
# is the same unanchored/portability defect class as the boundary fix.
|
|
92
|
+
#
|
|
93
|
+
# `exec` stays receiver-blind on purpose. A left boundary that excludes a
|
|
94
|
+
# preceding `.` would drop every member-position `.exec('…')` — including
|
|
95
|
+
# `require('child_process').exec('…')`, the single most common Node spelling
|
|
96
|
+
# of the vector this pattern exists to catch — and a receiver allowlist
|
|
97
|
+
# cannot restore it, because the literal `child_process` is not adjacent to
|
|
98
|
+
# `.exec`. The cost is that `RegExp.prototype.exec`, which takes a subject
|
|
99
|
+
# string rather than code, also matches; files that legitimately call it are
|
|
100
|
+
# handled by ALLOWLIST below, never by narrowing the pattern.
|
|
75
101
|
'(^|[^[:alnum:]])eval[[:space:]]*\([[:space:]]*["'"'"']'
|
|
76
102
|
'exec[[:space:]]*\([[:space:]]*["'"'"']'
|
|
77
103
|
'(^|[^[:alnum:]])Function[[:space:]]*\([[:space:]]*["'"'"'].*return'
|
|
@@ -129,6 +155,22 @@ ALLOWLIST=(
|
|
|
129
155
|
# asserts nothing: it is the payload the guard is required to catch, carried
|
|
130
156
|
# as test DATA. Same class as the read-injection-scanner suites above.
|
|
131
157
|
'tests/kimi-payload-field-shadowing.security.test.cjs'
|
|
158
|
+
# Phase-ID grammar regression tests exercise `RegExp.prototype.exec` via
|
|
159
|
+
# `re.exec('<phase-id>')` against fixtures like 'MANIFOLD-64-auth' / 'CK-64-auth'.
|
|
160
|
+
# The scanner's `exec('` code-execution pattern matches that benign method call,
|
|
161
|
+
# not an attack vector — same DEFECT.PROMPT-INJECTION-SCAN-COLLISION class as the
|
|
162
|
+
# test fixtures above. Pre-existing content (16 such calls on `next`); it surfaces
|
|
163
|
+
# here only because #2573's W024 `state_head` assertions make the file appear in
|
|
164
|
+
# the changed-file set the diff-mode scan walks.
|
|
165
|
+
'tests/health-validation.test.cjs'
|
|
166
|
+
# #2528 — same collision, same disposition: the continuation-grammar suite
|
|
167
|
+
# drives the tokenizer regexes directly via `re.exec('05-80-20')`, so the
|
|
168
|
+
# argument is the subject string, not a command. Exempted per file rather
|
|
169
|
+
# than by narrowing the `exec(` pattern: a left boundary excluding a preceding
|
|
170
|
+
# `.` would drop `require('child_process').exec('…')`, and a receiver
|
|
171
|
+
# allowlist cannot reach it either, because the literal `child_process` is
|
|
172
|
+
# not adjacent to `.exec`. See the note at the pattern itself.
|
|
173
|
+
'tests/continuation-grammar-parity.test.cjs'
|
|
132
174
|
)
|
|
133
175
|
|
|
134
176
|
is_allowlisted() {
|
|
@@ -150,33 +192,73 @@ collect_files() {
|
|
|
150
192
|
case "$mode" in
|
|
151
193
|
--diff)
|
|
152
194
|
local base="${1:-origin/main}"
|
|
153
|
-
#
|
|
154
|
-
|
|
155
|
-
|
|
195
|
+
# Run git separately from the filter pipe so its OWN exit status (not
|
|
196
|
+
# grep's) decides whether the diff could be established. stdout and
|
|
197
|
+
# stderr are captured SEPARATELY (never merged with `2>&1`) so that a
|
|
198
|
+
# warning git writes to stderr on an otherwise successful diff can
|
|
199
|
+
# never be mistaken for a filename in the file list. On failure the
|
|
200
|
+
# captured stderr is emitted as the diagnostic; on success it is
|
|
201
|
+
# forwarded as a warning, never folded into the file list.
|
|
202
|
+
# `|| true` on the filter below is CORRECT (not gratuitous): `grep -E`
|
|
203
|
+
# exits 1 when nothing matches the scannable extensions (e.g. a diff
|
|
204
|
+
# touching only non-scannable file types), which is a legitimate empty
|
|
205
|
+
# result, not a failure to run.
|
|
206
|
+
local raw status err_file
|
|
207
|
+
err_file=$(mktemp)
|
|
208
|
+
raw=$(git diff --name-only --diff-filter=ACMR "$base"...HEAD 2>"$err_file")
|
|
209
|
+
status=$?
|
|
210
|
+
if (( status != 0 )); then
|
|
211
|
+
cat "$err_file" >&2
|
|
212
|
+
rm -f "$err_file"
|
|
213
|
+
exit "$EXIT_UNAVAILABLE"
|
|
214
|
+
fi
|
|
215
|
+
if [[ -s "$err_file" ]]; then
|
|
216
|
+
echo "Warning: git diff emitted stderr output:" >&2
|
|
217
|
+
cat "$err_file" >&2
|
|
218
|
+
fi
|
|
219
|
+
rm -f "$err_file"
|
|
220
|
+
printf '%s\n' "$raw" | grep -E '\.(md|cjs|js|json|yml|yaml|sh)$' || true
|
|
156
221
|
;;
|
|
157
222
|
--file)
|
|
158
223
|
if [[ -f "$1" ]]; then
|
|
159
224
|
echo "$1"
|
|
160
225
|
else
|
|
161
226
|
echo "Error: file not found: $1" >&2
|
|
162
|
-
exit
|
|
227
|
+
exit "$EXIT_USAGE"
|
|
163
228
|
fi
|
|
164
229
|
;;
|
|
165
230
|
--dir)
|
|
166
231
|
local dir="$1"
|
|
167
232
|
if [[ ! -d "$dir" ]]; then
|
|
168
233
|
echo "Error: directory not found: $dir" >&2
|
|
169
|
-
exit
|
|
234
|
+
exit "$EXIT_USAGE"
|
|
235
|
+
fi
|
|
236
|
+
# Same treatment as --diff: a `find` that fails (e.g. permission
|
|
237
|
+
# denied) must not be reported as an empty directory, and stdout/stderr
|
|
238
|
+
# are captured separately so a stderr warning never enters the file list.
|
|
239
|
+
local raw status err_file
|
|
240
|
+
err_file=$(mktemp)
|
|
241
|
+
raw=$(find "$dir" -type f \( -name '*.md' -o -name '*.cjs' -o -name '*.js' -o -name '*.json' -o -name '*.yml' -o -name '*.yaml' -o -name '*.sh' \) \
|
|
242
|
+
! -path '*/node_modules/*' ! -path '*/.git/*' ! -path '*/dist/*' 2>"$err_file")
|
|
243
|
+
status=$?
|
|
244
|
+
if (( status != 0 )); then
|
|
245
|
+
cat "$err_file" >&2
|
|
246
|
+
rm -f "$err_file"
|
|
247
|
+
exit "$EXIT_UNAVAILABLE"
|
|
248
|
+
fi
|
|
249
|
+
if [[ -s "$err_file" ]]; then
|
|
250
|
+
echo "Warning: find emitted stderr output:" >&2
|
|
251
|
+
cat "$err_file" >&2
|
|
170
252
|
fi
|
|
171
|
-
|
|
172
|
-
|
|
253
|
+
rm -f "$err_file"
|
|
254
|
+
printf '%s\n' "$raw"
|
|
173
255
|
;;
|
|
174
256
|
--stdin)
|
|
175
257
|
cat
|
|
176
258
|
;;
|
|
177
259
|
*)
|
|
178
260
|
echo "Usage: $0 --diff [base] | --file <path> | --dir <path> | --stdin" >&2
|
|
179
|
-
exit
|
|
261
|
+
exit "$EXIT_USAGE"
|
|
180
262
|
;;
|
|
181
263
|
esac
|
|
182
264
|
}
|
|
@@ -215,7 +297,7 @@ scan_file() {
|
|
|
215
297
|
main() {
|
|
216
298
|
if [[ $# -eq 0 ]]; then
|
|
217
299
|
echo "Usage: $0 --diff [base] | --file <path> | --dir <path>" >&2
|
|
218
|
-
exit
|
|
300
|
+
exit "$EXIT_USAGE"
|
|
219
301
|
fi
|
|
220
302
|
|
|
221
303
|
local mode="$1"
|
|
@@ -225,8 +307,12 @@ main() {
|
|
|
225
307
|
files=$(collect_files "$mode" "$@")
|
|
226
308
|
|
|
227
309
|
if [[ -z "$files" ]]; then
|
|
310
|
+
# collect_files already exited (UNAVAILABLE/USAGE) for anything that
|
|
311
|
+
# could not establish scope. Reaching here with an empty result means
|
|
312
|
+
# scope WAS established and is genuinely empty — that is NO_INPUT, not a
|
|
313
|
+
# silent clean pass.
|
|
228
314
|
echo "prompt-injection-scan: no files to scan"
|
|
229
|
-
exit
|
|
315
|
+
exit "$EXIT_NO_INPUT"
|
|
230
316
|
fi
|
|
231
317
|
|
|
232
318
|
local total=0
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Require-issue-link policy (#3211, preserving #1389).
|
|
6
|
+
*
|
|
7
|
+
* Replaces the shell-only `grep -qiE '(closes|fixes|resolves)\s+#[0-9]+'`
|
|
8
|
+
* step with a pure, testable `evaluateIssueLink` verdict function so a PR
|
|
9
|
+
* that only REFERENCES an issue (rather than closing it — e.g. a follow-up
|
|
10
|
+
* regression-coverage PR) is not forced to fabricate a closing keyword, while
|
|
11
|
+
* two pre-existing constraints are preserved exactly:
|
|
12
|
+
*
|
|
13
|
+
* 1. (#1389, anti-forgery) The backmerge exemption only fires when the
|
|
14
|
+
* head ref carries the backmerge prefix AND the PR is same-repo (not a
|
|
15
|
+
* fork) — dropping the `sameRepo` conjunct would let a fork forge a
|
|
16
|
+
* branch name to bypass the issue-link requirement entirely.
|
|
17
|
+
* 2. (#3211, truncation) `gh pr view --json files` truncates the file list
|
|
18
|
+
* at 100 entries with no in-band signal that it did so (see
|
|
19
|
+
* scripts/pr-changed-files.cjs). The reference-only carve-out below
|
|
20
|
+
* only applies when every changed file is a test/doc file, so a PR
|
|
21
|
+
* whose file list may be truncated must fail closed rather than let an
|
|
22
|
+
* unseen 101st+ file (which could touch `src/`) slip through.
|
|
23
|
+
*
|
|
24
|
+
* Tests assert on the typed ISSUE_LINK_REASON enum, never on free text.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { fileListIsComplete, parseChangedFilesEnv } = require('./pr-changed-files.cjs');
|
|
28
|
+
const { runMain } = require('./lib/cli-exit.cjs');
|
|
29
|
+
|
|
30
|
+
const ISSUE_LINK_REASON = Object.freeze({
|
|
31
|
+
OK_CLOSING_KEYWORD: 'ok_closing_keyword',
|
|
32
|
+
OK_BACKMERGE_EXEMPT: 'ok_backmerge_exempt',
|
|
33
|
+
OK_FOLLOWUP_REFERENCE: 'ok_followup_reference',
|
|
34
|
+
FAIL_NO_ISSUE_REFERENCE: 'fail_no_issue_reference',
|
|
35
|
+
FAIL_REFERENCE_NEEDS_CLOSING: 'fail_reference_needs_closing',
|
|
36
|
+
FAIL_FILE_LIST_INCOMPLETE: 'fail_file_list_incomplete',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// #1389: backmerge PRs are opened by CI against `next`, never by a human or a
|
|
40
|
+
// fork, so they are exempt from the issue-link requirement outright — but
|
|
41
|
+
// ONLY when combined with `sameRepo === true` below (see header comment).
|
|
42
|
+
const BACKMERGE_BRANCH_PREFIX = 'chore/backmerge-main-to-next-';
|
|
43
|
+
|
|
44
|
+
// A follow-up-only PR (references an issue without closing it) is only
|
|
45
|
+
// allowed to skip the closing keyword when every changed file is a test or
|
|
46
|
+
// doc file — i.e. it cannot be the PR that actually implements the fix.
|
|
47
|
+
const EXEMPT_PATH_PREFIXES = ['tests/', 'docs/'];
|
|
48
|
+
|
|
49
|
+
// Root-level markdown is documentation — this mirrors the repo's own doc-only
|
|
50
|
+
// classifier (.claude/hooks/pre-pr-gate.sh:111), whose `[^/]+\.md` anchor is
|
|
51
|
+
// deliberately root-only so runtime-loaded text under a subdirectory
|
|
52
|
+
// (gsd-core/workflows/*.md, agents/*.md, commands/**/*.md) stays gated.
|
|
53
|
+
// CHANGELOG.md is excluded: scripts/changeset/lint.cjs classes a direct edit to
|
|
54
|
+
// it as user-facing precisely to close a bypass, so it must not ride in on the
|
|
55
|
+
// docs carve-out either.
|
|
56
|
+
const EXCLUDED_ROOT_DOCS = new Set(['CHANGELOG.md']);
|
|
57
|
+
|
|
58
|
+
// Case-insensitive lookup set derived from EXCLUDED_ROOT_DOCS. The exclusion
|
|
59
|
+
// check below is case-insensitive because the `.md` extension test above it
|
|
60
|
+
// already is (`/\.md$/i`) — `changelog.md` or `CHANGELOG.MD` would otherwise
|
|
61
|
+
// slip past the exclusion while still passing the extension test. In
|
|
62
|
+
// practice this is defense in depth rather than a live bypass: the GitHub
|
|
63
|
+
// API always reports the real path with its actual, fixed casing (the
|
|
64
|
+
// filesystem is case-sensitive on the runners this executes on), so a PR
|
|
65
|
+
// cannot rename CHANGELOG.md to bypass the check by casing alone.
|
|
66
|
+
const EXCLUDED_ROOT_DOCS_UPPER = new Set(
|
|
67
|
+
Array.from(EXCLUDED_ROOT_DOCS, (name) => name.toUpperCase()),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
function isRootLevelDoc(normalizedPath) {
|
|
71
|
+
if (!normalizedPath) return false;
|
|
72
|
+
if (normalizedPath.includes('/')) return false;
|
|
73
|
+
if (!/\.md$/i.test(normalizedPath)) return false;
|
|
74
|
+
if (EXCLUDED_ROOT_DOCS_UPPER.has(normalizedPath.toUpperCase())) return false;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Mirrors the shipped `grep -qiE '(closes|fixes|resolves)\s+#[0-9]+'` exactly:
|
|
79
|
+
// no additional keywords, no `\b` anchors (the shell grep it replaces has
|
|
80
|
+
// none either) — see the corpus-parity test in
|
|
81
|
+
// tests/require-issue-link-policy.test.cjs.
|
|
82
|
+
const CLOSING_KEYWORD_REGEX = /(?:closes|fixes|resolves)\s+#[0-9]+/i;
|
|
83
|
+
|
|
84
|
+
// Accepts a soft "this PR relates to #N" reference without claiming to close
|
|
85
|
+
// it. The leading `\b` prevents matching inside a longer word (e.g. `xref#1`,
|
|
86
|
+
// `prefs #1`) because there is no word-boundary transition between the
|
|
87
|
+
// preceding word character and the start of the alternative — verified
|
|
88
|
+
// explicitly for `preferences #1` / `unreferenced #1` in the test suite.
|
|
89
|
+
const FOLLOWUP_REFERENCE_REGEX = /\b(?:refs?|references?|relates\s+to|related\s+to|follow[-\s]?up\s+to)\s+#[0-9]+/i;
|
|
90
|
+
|
|
91
|
+
function hasClosingKeyword(body) {
|
|
92
|
+
return CLOSING_KEYWORD_REGEX.test(String(body || ''));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hasFollowUpReference(body) {
|
|
96
|
+
return FOLLOWUP_REFERENCE_REGEX.test(String(body || ''));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Path separator normalization — unconditional, per repo convention (see
|
|
100
|
+
// CLAUDE.md "Path-sep normalization"), so a Windows-style diff entry like
|
|
101
|
+
// `tests\windows\a.test.cjs` is still recognized as tests/-prefixed.
|
|
102
|
+
function normalizePath(p) {
|
|
103
|
+
return String(p).replace(/\\/g, '/');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns true iff every changed file qualifies as a test/doc file, i.e. each
|
|
108
|
+
* one is one of:
|
|
109
|
+
* 1. under one of EXEMPT_PATH_PREFIXES (`tests/`, `docs/`) — the trailing
|
|
110
|
+
* slash on each prefix makes this directory-boundary aware, so
|
|
111
|
+
* lookalikes like `tests-e2e/`, `testsuite/`, or `docsite/` are
|
|
112
|
+
* correctly rejected (they are not `tests/` or `docs/`); or
|
|
113
|
+
* 2. a root-level markdown file (no `/`, `.md` extension, case-insensitive)
|
|
114
|
+
* that is not in EXCLUDED_ROOT_DOCS — see isRootLevelDoc.
|
|
115
|
+
*/
|
|
116
|
+
function allPathsAreTestsOrDocs(changedFiles) {
|
|
117
|
+
if (!Array.isArray(changedFiles) || changedFiles.length === 0) return false;
|
|
118
|
+
return changedFiles.every((file) => {
|
|
119
|
+
const normalized = normalizePath(file);
|
|
120
|
+
if (EXEMPT_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return true;
|
|
121
|
+
return isRootLevelDoc(normalized);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function evaluateIssueLink({ prBody, headRef, sameRepo, changedFiles, changedFilesTotal }) {
|
|
126
|
+
if (hasClosingKeyword(prBody)) {
|
|
127
|
+
return { ok: true, reason: ISSUE_LINK_REASON.OK_CLOSING_KEYWORD };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// #1389: `sameRepo === true` is required, not just the branch-name prefix —
|
|
131
|
+
// a fork PR could otherwise name its branch to forge this exemption.
|
|
132
|
+
if (String(headRef || '').startsWith(BACKMERGE_BRANCH_PREFIX) && sameRepo === true) {
|
|
133
|
+
return { ok: true, reason: ISSUE_LINK_REASON.OK_BACKMERGE_EXEMPT };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!hasFollowUpReference(prBody)) {
|
|
137
|
+
return { ok: false, reason: ISSUE_LINK_REASON.FAIL_NO_ISSUE_REFERENCE };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// #3211: a truncated file list cannot be trusted to prove "tests/docs
|
|
141
|
+
// only" — fail closed rather than risk approving on an unseen file.
|
|
142
|
+
if (!fileListIsComplete(changedFiles, changedFilesTotal)) {
|
|
143
|
+
return { ok: false, reason: ISSUE_LINK_REASON.FAIL_FILE_LIST_INCOMPLETE };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (allPathsAreTestsOrDocs(changedFiles)) {
|
|
147
|
+
return { ok: true, reason: ISSUE_LINK_REASON.OK_FOLLOWUP_REFERENCE };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { ok: false, reason: ISSUE_LINK_REASON.FAIL_REFERENCE_NEEDS_CLOSING };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function main() {
|
|
154
|
+
const changedFiles = parseChangedFilesEnv(process.env.CHANGED_FILES);
|
|
155
|
+
const parsedTotal = Number.parseInt(process.env.CHANGED_FILES_TOTAL, 10);
|
|
156
|
+
const changedFilesTotal = Number.isNaN(parsedTotal) ? undefined : parsedTotal;
|
|
157
|
+
|
|
158
|
+
const result = evaluateIssueLink({
|
|
159
|
+
prBody: process.env.PR_BODY || '',
|
|
160
|
+
headRef: process.env.HEAD_REF || '',
|
|
161
|
+
sameRepo: process.env.SAME_REPO === 'true',
|
|
162
|
+
changedFiles,
|
|
163
|
+
changedFilesTotal,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
167
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
168
|
+
const fs = require('node:fs');
|
|
169
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `ok=${result.ok ? 'true' : 'false'}\n`);
|
|
170
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `reason=${result.reason}\n`);
|
|
171
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `result=${JSON.stringify(result)}\n`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result.ok ? 0 : 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (require.main === module) runMain(main);
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
ISSUE_LINK_REASON,
|
|
181
|
+
BACKMERGE_BRANCH_PREFIX,
|
|
182
|
+
EXEMPT_PATH_PREFIXES,
|
|
183
|
+
EXCLUDED_ROOT_DOCS,
|
|
184
|
+
CLOSING_KEYWORD_REGEX,
|
|
185
|
+
FOLLOWUP_REFERENCE_REGEX,
|
|
186
|
+
hasClosingKeyword,
|
|
187
|
+
hasFollowUpReference,
|
|
188
|
+
normalizePath,
|
|
189
|
+
isRootLevelDoc,
|
|
190
|
+
allPathsAreTestsOrDocs,
|
|
191
|
+
evaluateIssueLink,
|
|
192
|
+
};
|
package/scripts/secret-scan.sh
CHANGED
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
# This flag does not change secret-detection logic — only which
|
|
18
18
|
# exclusions are applied.
|
|
19
19
|
#
|
|
20
|
-
# Exit codes:
|
|
21
|
-
# 0
|
|
22
|
-
# 1
|
|
23
|
-
#
|
|
20
|
+
# Exit codes (ADR-3889, #3908 — registered in gsd-core/bin/shared/exit-codes.json):
|
|
21
|
+
# 0 = clean
|
|
22
|
+
# 1 = findings detected
|
|
23
|
+
# $EXIT_USAGE (64) = usage error (bad argv, missing --file/--dir target)
|
|
24
|
+
# $EXIT_NO_INPUT (66) = ran; scope established; zero files in scope (genuinely empty)
|
|
25
|
+
# $EXIT_UNAVAILABLE (69) = could not establish scope (bad ref, not a repo, unreadable dir)
|
|
24
26
|
#
|
|
25
27
|
# Annotation format for .secretscanignore (required for --strict compliance):
|
|
26
28
|
# # allow: <pattern> reason="..." owner="..." expires="YYYY-MM-DD" [rule-id="..."]
|
|
@@ -39,6 +41,21 @@
|
|
|
39
41
|
# cannot permanently hide secrets. See SECURITY.md for the audit runbook.
|
|
40
42
|
set -euo pipefail
|
|
41
43
|
|
|
44
|
+
# ─── Exit-code registry (ADR-3889, #3908) ────────────────────────────────────
|
|
45
|
+
# Resolved relative to THIS script's location, not the caller's cwd, so this
|
|
46
|
+
# works regardless of where the scanner is invoked from. Loud, non-zero
|
|
47
|
+
# failure if the fragment is missing — never fall back to a guessed literal
|
|
48
|
+
# integer, and never let a missing registry silently degrade to exit 0.
|
|
49
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
50
|
+
EXIT_CODES_SH="$SCRIPT_DIR/../gsd-core/bin/shared/exit-codes.sh"
|
|
51
|
+
if [[ ! -f "$EXIT_CODES_SH" ]]; then
|
|
52
|
+
echo "secret-scan: FATAL: exit-code registry not found at $EXIT_CODES_SH" >&2
|
|
53
|
+
echo " Regenerate with: node scripts/gen-exit-code-registry.cjs --write" >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
# shellcheck disable=SC1090
|
|
57
|
+
. "$EXIT_CODES_SH"
|
|
58
|
+
|
|
42
59
|
# ─── Global mode flag ─────────────────────────────────────────────────────────
|
|
43
60
|
STRICT_MODE=false
|
|
44
61
|
|
|
@@ -235,7 +252,31 @@ collect_files() {
|
|
|
235
252
|
case "$mode" in
|
|
236
253
|
--diff)
|
|
237
254
|
local base="${1:-origin/main}"
|
|
238
|
-
git
|
|
255
|
+
# Run git separately from the filter pipe so its OWN exit status (not
|
|
256
|
+
# grep's) decides whether the diff could be established. stdout and
|
|
257
|
+
# stderr are captured SEPARATELY (never merged with `2>&1`) so that a
|
|
258
|
+
# warning git writes to stderr on an otherwise successful diff can
|
|
259
|
+
# never be mistaken for a filename in the file list. On failure the
|
|
260
|
+
# captured stderr is emitted as the diagnostic; on success it is
|
|
261
|
+
# forwarded as a warning, never folded into the file list.
|
|
262
|
+
# `|| true` on the filter below is CORRECT (not gratuitous): `grep -v`
|
|
263
|
+
# exits 1 when every line is filtered out (e.g. an all-images diff),
|
|
264
|
+
# which is a legitimate empty result, not a failure to run.
|
|
265
|
+
local raw status err_file
|
|
266
|
+
err_file=$(mktemp)
|
|
267
|
+
raw=$(git diff --name-only --diff-filter=ACMR "$base"...HEAD 2>"$err_file")
|
|
268
|
+
status=$?
|
|
269
|
+
if (( status != 0 )); then
|
|
270
|
+
cat "$err_file" >&2
|
|
271
|
+
rm -f "$err_file"
|
|
272
|
+
exit "$EXIT_UNAVAILABLE"
|
|
273
|
+
fi
|
|
274
|
+
if [[ -s "$err_file" ]]; then
|
|
275
|
+
echo "Warning: git diff emitted stderr output:" >&2
|
|
276
|
+
cat "$err_file" >&2
|
|
277
|
+
fi
|
|
278
|
+
rm -f "$err_file"
|
|
279
|
+
printf '%s\n' "$raw" \
|
|
239
280
|
| grep -vE '\.(png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot|otf|zip|tar|gz|pdf)$' || true
|
|
240
281
|
;;
|
|
241
282
|
--file)
|
|
@@ -243,24 +284,41 @@ collect_files() {
|
|
|
243
284
|
echo "$1"
|
|
244
285
|
else
|
|
245
286
|
echo "Error: file not found: $1" >&2
|
|
246
|
-
exit
|
|
287
|
+
exit "$EXIT_USAGE"
|
|
247
288
|
fi
|
|
248
289
|
;;
|
|
249
290
|
--dir)
|
|
250
291
|
local dir="$1"
|
|
251
292
|
if [[ ! -d "$dir" ]]; then
|
|
252
293
|
echo "Error: directory not found: $dir" >&2
|
|
253
|
-
exit
|
|
294
|
+
exit "$EXIT_USAGE"
|
|
295
|
+
fi
|
|
296
|
+
# Same treatment as --diff: a `find` that fails (e.g. permission
|
|
297
|
+
# denied) must not be reported as an empty directory, and stdout/stderr
|
|
298
|
+
# are captured separately so a stderr warning never enters the file list.
|
|
299
|
+
local raw status err_file
|
|
300
|
+
err_file=$(mktemp)
|
|
301
|
+
raw=$(find "$dir" -type f ! -path '*/node_modules/*' ! -path '*/.git/*' ! -path '*/dist/*' \
|
|
302
|
+
! -name '*.png' ! -name '*.jpg' ! -name '*.gif' ! -name '*.woff*' 2>"$err_file")
|
|
303
|
+
status=$?
|
|
304
|
+
if (( status != 0 )); then
|
|
305
|
+
cat "$err_file" >&2
|
|
306
|
+
rm -f "$err_file"
|
|
307
|
+
exit "$EXIT_UNAVAILABLE"
|
|
308
|
+
fi
|
|
309
|
+
if [[ -s "$err_file" ]]; then
|
|
310
|
+
echo "Warning: find emitted stderr output:" >&2
|
|
311
|
+
cat "$err_file" >&2
|
|
254
312
|
fi
|
|
255
|
-
|
|
256
|
-
|
|
313
|
+
rm -f "$err_file"
|
|
314
|
+
printf '%s\n' "$raw"
|
|
257
315
|
;;
|
|
258
316
|
--stdin)
|
|
259
317
|
cat
|
|
260
318
|
;;
|
|
261
319
|
*)
|
|
262
320
|
echo "Usage: $0 --diff [base] | --file <path> | --dir <path> | --stdin" >&2
|
|
263
|
-
exit
|
|
321
|
+
exit "$EXIT_USAGE"
|
|
264
322
|
;;
|
|
265
323
|
esac
|
|
266
324
|
}
|
|
@@ -300,7 +358,7 @@ scan_file() {
|
|
|
300
358
|
main() {
|
|
301
359
|
if [[ $# -eq 0 ]]; then
|
|
302
360
|
echo "Usage: $0 --diff [base] | --file <path> | --dir <path> [--strict]" >&2
|
|
303
|
-
exit
|
|
361
|
+
exit "$EXIT_USAGE"
|
|
304
362
|
fi
|
|
305
363
|
|
|
306
364
|
# Parse --strict flag first (may appear anywhere in argv)
|
|
@@ -316,7 +374,7 @@ main() {
|
|
|
316
374
|
|
|
317
375
|
if [[ $# -eq 0 ]]; then
|
|
318
376
|
echo "Usage: $0 --diff [base] | --file <path> | --dir <path> [--strict]" >&2
|
|
319
|
-
exit
|
|
377
|
+
exit "$EXIT_USAGE"
|
|
320
378
|
fi
|
|
321
379
|
|
|
322
380
|
load_ignorelist
|
|
@@ -328,8 +386,12 @@ main() {
|
|
|
328
386
|
files=$(collect_files "$mode" "$@")
|
|
329
387
|
|
|
330
388
|
if [[ -z "$files" ]]; then
|
|
389
|
+
# collect_files already exited (UNAVAILABLE/USAGE) for anything that
|
|
390
|
+
# could not establish scope. Reaching here with an empty result means
|
|
391
|
+
# scope WAS established and is genuinely empty (e.g. an all-images
|
|
392
|
+
# diff) — that is NO_INPUT, not a silent clean pass.
|
|
331
393
|
echo "secret-scan: no files to scan"
|
|
332
|
-
exit
|
|
394
|
+
exit "$EXIT_NO_INPUT"
|
|
333
395
|
fi
|
|
334
396
|
|
|
335
397
|
local total=0
|