@opengsd/gsd-core 1.9.1 → 1.11.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 +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +27 -3
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +12 -10
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +46 -0
- package/agents/gsd-planner.md +49 -54
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +26 -73
- package/bin/install.js +1272 -1238
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +2 -2
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/plan-phase.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1009 -115
- package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
- package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
- package/gsd-core/bin/lib/api-coverage.cjs +123 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +926 -202
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- 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-registry.cjs +608 -148
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +507 -24
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +114 -38
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +94 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +665 -99
- package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +76 -0
- package/gsd-core/bin/lib/config.cjs +22 -2
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +217 -40
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +239 -32
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
- 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 +145 -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 +265 -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 +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1325 -169
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +805 -264
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +160 -57
- 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-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
- package/gsd-core/bin/lib/io.cjs +38 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +821 -109
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +507 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +258 -58
- package/gsd-core/bin/lib/phase.cjs +891 -156
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +405 -84
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
- package/gsd-core/bin/lib/smart-entry.cjs +154 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +424 -105
- package/gsd-core/bin/lib/state.cjs +1927 -401
- package/gsd-core/bin/lib/surface.cjs +35 -10
- 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 +20 -4
- package/gsd-core/bin/lib/uat.cjs +706 -64
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +33 -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.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -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 +287 -20
- package/gsd-core/bin/lib/verify.cjs +368 -880
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
- package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
- package/gsd-core/bin/lib/workstream.cjs +8 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- 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 +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +8 -7
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +33 -70
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +74 -166
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +160 -95
- package/gsd-core/workflows/debug.md +16 -17
- package/gsd-core/workflows/diagnose-issues.md +56 -8
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- 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 +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +103 -187
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +7 -6
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +35 -47
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +27 -240
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +89 -209
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +45 -159
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +55 -405
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +41 -13
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +41 -20
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +106 -13
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +53 -31
- package/gsd-core/workflows/ui-phase.md +13 -12
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +19 -8
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +17 -65
- package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
- package/hooks/dist/gsd-check-update-worker.js +64 -12
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +45 -24
- package/hooks/dist/gsd-statusline.js +90 -6
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/git-cmd.js +92 -59
- 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 +277 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +517 -0
- package/hooks/gsd-check-update-worker.js +64 -12
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +607 -26
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +45 -24
- package/hooks/gsd-statusline.js +90 -6
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +277 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +31 -10
- package/pi/gsd.cjs +71 -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 +9 -0
- package/scripts/changeset/lint.cjs +68 -6
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +439 -0
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +150 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
- 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-compiled-artifact-sync.cjs +6 -1
- 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-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -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 +434 -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 +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +40 -3
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +52 -6
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +2 -2
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +2 -2
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +1 -2
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- 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/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 -577
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/gen-emitted-baseline.cjs +0 -145
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1050
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gsd-hook-version: {{GSD_VERSION}}
|
|
3
|
+
// GSD Write Guard — PreToolUse hook
|
|
4
|
+
// Blocks a whole-file Write that catastrophically shrinks a curated .planning/
|
|
5
|
+
// artifact (ROADMAP.md, milestone roadmaps, STATE.md).
|
|
6
|
+
//
|
|
7
|
+
// Problem (#973, fix 3 of 3): a planner read a ~16-line window of ROADMAP.md
|
|
8
|
+
// and Write-overwrote the whole 292-line file with it — three milestones of
|
|
9
|
+
// committed history destroyed. Fixes 1 and 2 (PR #989) are instructions to a
|
|
10
|
+
// model: they lower the probability of a clobber but cannot prevent one, and
|
|
11
|
+
// they protect only the agents that were audited. This hook is enforced by
|
|
12
|
+
// code rather than by instruction: it compares the pending Write payload
|
|
13
|
+
// against the file on disk and hard-blocks a catastrophic shrink BEFORE it
|
|
14
|
+
// happens. An advisory will not do — #973 records an agent reading the
|
|
15
|
+
// advisory, classifying it as non-binding, and reasoning past it while
|
|
16
|
+
// holding a false model of what Write does.
|
|
17
|
+
//
|
|
18
|
+
// The guarantee is bounded, and the bound is worth stating where the code
|
|
19
|
+
// lives: this stops accidental and single-shot collapse, not a determined
|
|
20
|
+
// agent. The sentinel hatch below is a plain file, so an agent that would
|
|
21
|
+
// reason past an advisory can arm one with a single Bash call it is already
|
|
22
|
+
// permitted to make. What ships is the conversion of "ignore a sentence" into
|
|
23
|
+
// "take one deliberate, path-bound, single-use, auditable action" — a real
|
|
24
|
+
// improvement against the confused-agent threat #973 records, not a defense
|
|
25
|
+
// against an evader.
|
|
26
|
+
//
|
|
27
|
+
// Deliberately narrow trigger:
|
|
28
|
+
// - Write only (Edit/MultiEdit are scoped by construction);
|
|
29
|
+
// - the target already exists on disk;
|
|
30
|
+
// - the target is a curated .planning/ artifact — the project ROADMAP.md,
|
|
31
|
+
// milestone roadmaps (.planning/milestones/*-ROADMAP.md), and STATE.md.
|
|
32
|
+
// NOT arbitrary markdown: free-prose docs get legitimately rewritten
|
|
33
|
+
// wholesale, and a guard that fires on those trains override-fatigue
|
|
34
|
+
// until nobody reads it.
|
|
35
|
+
//
|
|
36
|
+
// Threshold: block when the pending payload carries fewer than SHRINK_RATIO
|
|
37
|
+
// (40%) of the on-disk line count. The docs-update fix-loop's 90% bar is far
|
|
38
|
+
// too permissive for a curated artifact — the #973 incident was a ~94.5%
|
|
39
|
+
// collapse and clears a 90% bar only barely. The same ~40%/floor-40 tuning
|
|
40
|
+
// has run clean (no false positives) as a commit-time twin downstream.
|
|
41
|
+
//
|
|
42
|
+
// Floor: files under FLOOR_LINES are exempt, so a 10 → 2 line stub never
|
|
43
|
+
// trips the ratio check.
|
|
44
|
+
//
|
|
45
|
+
// Escape hatches — both named in the block message; a guard whose bypass is
|
|
46
|
+
// undocumented gets bypassed with the blunt instrument instead, with every
|
|
47
|
+
// other guard disabled at the same time:
|
|
48
|
+
// - GSD_ALLOW_PLANNING_SHRINK=1 (env) — for a human running interactively,
|
|
49
|
+
// where the variable can actually reach the hook's environment.
|
|
50
|
+
// - .planning/.gsd-allow-shrink (single-use sentinel file) — for workflow
|
|
51
|
+
// steps. A PreToolUse hook inherits the RUNTIME's environment, so a
|
|
52
|
+
// per-step env prefix can never reach it (#2255 round 5 M1); the sentinel
|
|
53
|
+
// is a transport that code consults, not prose an agent obeys. The step
|
|
54
|
+
// writes the target's path into the sentinel; at the block point the
|
|
55
|
+
// guard checks it is fresh (15 min) and names the pending target, then
|
|
56
|
+
// CONSUMES it and allows that one write. Path-bound + single-use +
|
|
57
|
+
// freshness is what keeps it from becoming a standing unlock left on disk.
|
|
58
|
+
//
|
|
59
|
+
// Known design limits (out of #2255's scope by review, disclosed here AND in
|
|
60
|
+
// the changeset + USER-GUIDE — round 9 required the user-facing docs to match):
|
|
61
|
+
// - Stateless per-write: sequential shrinks (292→120→50) each clear the 40%
|
|
62
|
+
// floor against CURRENT disk state, so cumulative erosion is invisible.
|
|
63
|
+
// - Unconditionally case-insensitive matching (required on the
|
|
64
|
+
// case-insensitive filesystems macOS/Windows default to): on
|
|
65
|
+
// case-sensitive Linux a genuinely distinct '.planning/roadmap.md' is
|
|
66
|
+
// also treated as curated. Narrow, accepted cost.
|
|
67
|
+
// (A third limit — a symlinked path into a curated file escaping the lexical
|
|
68
|
+
// match — was closed in round 9: the target is realpath-resolved before the
|
|
69
|
+
// curated match.)
|
|
70
|
+
//
|
|
71
|
+
// Triggers on: Write tool calls
|
|
72
|
+
// Action: BLOCK (decision: 'block', exit 2) on catastrophic shrink of a curated file
|
|
73
|
+
// No-op: other tools, new files, non-curated paths, sub-floor files, override set,
|
|
74
|
+
// hook errors (silent fail)
|
|
75
|
+
|
|
76
|
+
const fs = require('fs');
|
|
77
|
+
const path = require('path');
|
|
78
|
+
|
|
79
|
+
// Block when the pending payload has fewer than this fraction of the on-disk
|
|
80
|
+
// line count (0.4 → a Write shrinking a file below 40% of its current size).
|
|
81
|
+
const SHRINK_RATIO = 0.4;
|
|
82
|
+
|
|
83
|
+
// Files with fewer lines than this are exempt — small stubs get legitimately
|
|
84
|
+
// rewritten far below any ratio.
|
|
85
|
+
const FLOOR_LINES = 40;
|
|
86
|
+
|
|
87
|
+
// Curated .planning/ artifacts, matched against the resolved target path with
|
|
88
|
+
// separators normalized to '/'. Deliberately a closed set (see header).
|
|
89
|
+
// Case-insensitive: on the case-insensitive filesystems macOS and Windows
|
|
90
|
+
// default to, a differently-cased path is the SAME real file — a Write to
|
|
91
|
+
// '.planning/roadmap.md' clobbers ROADMAP.md while a case-sensitive match
|
|
92
|
+
// waves it through.
|
|
93
|
+
const CURATED_PATTERNS = [
|
|
94
|
+
/(?:^|\/)\.planning\/ROADMAP\.md$/i,
|
|
95
|
+
/(?:^|\/)\.planning\/STATE\.md$/i,
|
|
96
|
+
/(?:^|\/)\.planning\/milestones\/[^/]+-ROADMAP\.md$/i,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
// Count logical lines, ignoring a single trailing newline so that
|
|
100
|
+
// "a\nb\n" and "a\nb" both count as 2.
|
|
101
|
+
function countLines(text) {
|
|
102
|
+
if (!text) return 0;
|
|
103
|
+
const lines = text.split('\n');
|
|
104
|
+
if (lines[lines.length - 1] === '') lines.pop();
|
|
105
|
+
return lines.length;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isOverrideSet() {
|
|
109
|
+
const v = process.env.GSD_ALLOW_PLANNING_SHRINK;
|
|
110
|
+
return typeof v === 'string' && v !== '' && v !== '0' && v.toLowerCase() !== 'false';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Single-use sentinel (see header). Consulted ONLY at the shrink-block point —
|
|
114
|
+
// a write that would pass anyway never burns the token, so first-shrink-wins
|
|
115
|
+
// for the write the workflow armed it for.
|
|
116
|
+
const SENTINEL_NAME = '.gsd-allow-shrink';
|
|
117
|
+
const SENTINEL_REL = '.planning/' + SENTINEL_NAME;
|
|
118
|
+
const SENTINEL_TTL_MS = 15 * 60 * 1000;
|
|
119
|
+
|
|
120
|
+
function consumeSentinelFor(filePath, normalized) {
|
|
121
|
+
try {
|
|
122
|
+
// The curated match guarantees the target lives under a .planning/ dir;
|
|
123
|
+
// normalized is filePath with separators flipped, so offsets line up.
|
|
124
|
+
const m = normalized.match(/^(.*\/\.planning)\//i);
|
|
125
|
+
if (!m) return false;
|
|
126
|
+
const planningDir = filePath.slice(0, m[1].length);
|
|
127
|
+
const sentinelPath = path.join(planningDir, SENTINEL_NAME);
|
|
128
|
+
let st;
|
|
129
|
+
try {
|
|
130
|
+
st = fs.statSync(sentinelPath);
|
|
131
|
+
} catch {
|
|
132
|
+
return false; // not armed
|
|
133
|
+
}
|
|
134
|
+
if (Date.now() - st.mtimeMs > SENTINEL_TTL_MS) {
|
|
135
|
+
// A stale token is a leftover, not an authorization — housekeep it.
|
|
136
|
+
try { fs.unlinkSync(sentinelPath); } catch { /* best-effort */ }
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const token = fs.readFileSync(sentinelPath, 'utf8').split('\n')[0].trim();
|
|
140
|
+
if (!token) return false;
|
|
141
|
+
// Path-bound: the token names exactly one file, resolved against the
|
|
142
|
+
// .planning/ dir's parent (repo root) — same case-insensitive stance as
|
|
143
|
+
// the curated match itself.
|
|
144
|
+
const namedNorm = path.resolve(path.join(planningDir, '..'), token).replace(/\\/g, '/').toLowerCase();
|
|
145
|
+
if (namedNorm !== normalized.toLowerCase()) {
|
|
146
|
+
return false; // armed for a different file — leave it for that write
|
|
147
|
+
}
|
|
148
|
+
// Consume BEFORE allowing: even if the Write then fails, the safe
|
|
149
|
+
// direction is a spent token, never a lingering one.
|
|
150
|
+
fs.unlinkSync(sentinelPath);
|
|
151
|
+
return true;
|
|
152
|
+
} catch {
|
|
153
|
+
// Any sentinel-machinery error means "not exempt" — the guard's normal
|
|
154
|
+
// (blocking) flow proceeds; the hatch may never fail a guard open.
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// m2 (round 5): the block emission must itself be exception-safe. An EPIPE
|
|
160
|
+
// from writeSync inside the outer try would land in the fail-OPEN catch —
|
|
161
|
+
// the one outcome the fail-closed branches exist to prevent. The decision
|
|
162
|
+
// stands regardless of whether the payload could be delivered.
|
|
163
|
+
function emitBlock(output) {
|
|
164
|
+
try {
|
|
165
|
+
// writeSync: pipe writes via process.stdout/stderr are async on Windows
|
|
166
|
+
// and process.exit() does not flush them — a truncated block payload is
|
|
167
|
+
// a guard that silently half-fired.
|
|
168
|
+
fs.writeSync(1, JSON.stringify(output));
|
|
169
|
+
// Kimi feeds stderr (not stdout) back to the model on exit 2.
|
|
170
|
+
fs.writeSync(2, output.reason);
|
|
171
|
+
} catch {
|
|
172
|
+
// Emission failed; the block still stands.
|
|
173
|
+
}
|
|
174
|
+
process.exit(2);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// #2304: Kimi's native hook bus delivers Kimi's tool vocabulary in the payload
|
|
178
|
+
// (Write → WriteFile, Edit/MultiEdit → StrReplaceFile) while the [[hooks]]
|
|
179
|
+
// matcher is registered pre-translated (runtime-hooks-surface.cts
|
|
180
|
+
// buildKimiHooksTomlBlock) — so without normalizing the payload too, the
|
|
181
|
+
// matcher fires but the tool_name check below exits 0 and the guard is dormant
|
|
182
|
+
// on Kimi. The tool_input field names differ as well (kimi-cli
|
|
183
|
+
// src/kimi_cli/tools/file/write.py): WriteFile takes `path`/`content`, and
|
|
184
|
+
// kimi-cli's hooks/events.py forwards tool_input verbatim, so both layers need
|
|
185
|
+
// mapping. Only WriteFile is mapped: this guard exits 0 for any tool but
|
|
186
|
+
// Write, so an Edit-class mapping here would be dead code. Accepts bare and
|
|
187
|
+
// module-qualified ('kimi_cli.tools.file:WriteFile') names; unknown names fall
|
|
188
|
+
// through untouched. Inlined per guard (not hooks/lib/): hook scripts are
|
|
189
|
+
// staged as standalone files, and a sibling require is a staging dependency
|
|
190
|
+
// that can fail silently.
|
|
191
|
+
// A Map, not an object literal: bare bracket lookup resolves prototype keys
|
|
192
|
+
// ('constructor', '__proto__', 'toString') to truthy functions/objects, so the
|
|
193
|
+
// !mapped fall-through never fires for them; Map.get returns undefined (same
|
|
194
|
+
// shape as canonicalizeRuntimeName in src/runtime-name-policy.cts).
|
|
195
|
+
const KIMI_TOOL_NAMES = new Map([['WriteFile', 'Write']]);
|
|
196
|
+
function normalizeKimiPayload(data) {
|
|
197
|
+
// #2595: total over everything JSON can express — JSON.parse('null') is
|
|
198
|
+
// null, and reading .tool_name off a primitive would throw into the outer
|
|
199
|
+
// fail-open catch. A null payload has nothing to guard; pass it through
|
|
200
|
+
// deliberately rather than by crash.
|
|
201
|
+
if (data === null || typeof data !== 'object') return data;
|
|
202
|
+
const raw = data.tool_name;
|
|
203
|
+
if (typeof raw !== 'string') return data;
|
|
204
|
+
const mapped = KIMI_TOOL_NAMES.get(raw.slice(raw.lastIndexOf(':') + 1));
|
|
205
|
+
if (!mapped) return data;
|
|
206
|
+
data.tool_name = mapped;
|
|
207
|
+
const input = data.tool_input;
|
|
208
|
+
if (input && typeof input === 'object') {
|
|
209
|
+
// #2595 (review): Kimi's `path` is AUTHORITATIVE — it must win outright,
|
|
210
|
+
// not merely fill in when `file_path` happens to be absent. kimi-cli's
|
|
211
|
+
// WriteFile schema carries no `file_path` at all (src/kimi_cli/tools/
|
|
212
|
+
// file/write.py), so a `file_path` in a Kimi payload is ALWAYS
|
|
213
|
+
// model-supplied; under the old `=== undefined` condition a payload
|
|
214
|
+
// pairing a curated `path` with a spurious `file_path: ""` left this
|
|
215
|
+
// guard reading '' and exiting 0 while kimi-cli wrote to `path` — a
|
|
216
|
+
// one-key bypass needing no crash. Overwriting can only narrow what the
|
|
217
|
+
// guard inspects to the path that will actually be written.
|
|
218
|
+
if (typeof input.path === 'string') {
|
|
219
|
+
input.file_path = input.path;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return data;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let input = '';
|
|
226
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
|
227
|
+
process.stdin.setEncoding('utf8');
|
|
228
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
229
|
+
process.stdin.on('end', () => {
|
|
230
|
+
clearTimeout(stdinTimeout);
|
|
231
|
+
try {
|
|
232
|
+
const data = normalizeKimiPayload(JSON.parse(input));
|
|
233
|
+
|
|
234
|
+
// A null/primitive payload has nothing to guard — exit deliberately
|
|
235
|
+
// rather than throwing into the fail-open catch below (#2595 class).
|
|
236
|
+
if (data === null || typeof data !== 'object') {
|
|
237
|
+
process.exit(0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Only whole-file Write is catastrophic-by-construction; Edit/MultiEdit
|
|
241
|
+
// replace bounded spans and are out of scope by design (#2255).
|
|
242
|
+
if (data.tool_name !== 'Write') {
|
|
243
|
+
process.exit(0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (isOverrideSet()) {
|
|
247
|
+
process.exit(0); // documented escape hatch — legitimate reset in progress
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Typed read (#2547 class): `[]`/`{}` are truthy, pass a `!value`
|
|
251
|
+
// early-out, then throw inside path.resolve() — crash-to-allow via the
|
|
252
|
+
// outer catch. A non-string path field degrades to '' and exits here.
|
|
253
|
+
const rawInput = data.tool_input;
|
|
254
|
+
const rawFilePath = typeof rawInput?.file_path === 'string' ? rawInput.file_path : '';
|
|
255
|
+
const content = rawInput?.content;
|
|
256
|
+
if (!rawFilePath || typeof content !== 'string') {
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Resolve relative paths against the session cwd (the same base the
|
|
261
|
+
// runtime uses), then normalize separators for the curated match.
|
|
262
|
+
const cwd = data.cwd || process.cwd();
|
|
263
|
+
let filePath = path.resolve(cwd, rawFilePath);
|
|
264
|
+
// Resolve symlinks before the curated match (round 9, Minor 1): a Write
|
|
265
|
+
// to a non-curated path that symlinks into a curated file was not
|
|
266
|
+
// matched, while writeFileSync follows the link and clobbers the real
|
|
267
|
+
// target. ENOENT (new file) keeps the lexical resolution; any other
|
|
268
|
+
// realpath error also keeps it, and the read below then fails closed.
|
|
269
|
+
try {
|
|
270
|
+
filePath = fs.realpathSync(filePath);
|
|
271
|
+
} catch { /* keep the lexical path */ }
|
|
272
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
273
|
+
|
|
274
|
+
if (!CURATED_PATTERNS.some(re => re.test(normalized))) {
|
|
275
|
+
process.exit(0); // not a curated planning artifact
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Only guard overwrites — creating a curated file fresh is fine.
|
|
279
|
+
// ENOENT alone fails open (no baseline to protect); any OTHER read error
|
|
280
|
+
// (EACCES, EISDIR, ELOOP, EMFILE, a Windows lock) fails CLOSED — a guard
|
|
281
|
+
// that waves a curated Write through on a transient read error is not
|
|
282
|
+
// enforced by code at all, it is a race away from #973.
|
|
283
|
+
let onDisk;
|
|
284
|
+
try {
|
|
285
|
+
onDisk = fs.readFileSync(filePath, 'utf8');
|
|
286
|
+
} catch (err) {
|
|
287
|
+
if (err && err.code === 'ENOENT') {
|
|
288
|
+
process.exit(0); // does not exist — new-file Write, nothing to clobber
|
|
289
|
+
}
|
|
290
|
+
emitBlock({
|
|
291
|
+
decision: 'block',
|
|
292
|
+
readError: err && err.code ? String(err.code) : 'UNKNOWN',
|
|
293
|
+
overrideEnvVar: 'GSD_ALLOW_PLANNING_SHRINK',
|
|
294
|
+
overrideSentinel: SENTINEL_REL,
|
|
295
|
+
reason:
|
|
296
|
+
`Write guard: could not read '${filePath}' to compare against the pending ` +
|
|
297
|
+
`Write (${err && err.code ? err.code : 'unknown read error'}). ` +
|
|
298
|
+
`'${path.basename(filePath)}' is a curated planning artifact, so this guard ` +
|
|
299
|
+
`fails closed rather than risk a blind overwrite. Retry once the file is ` +
|
|
300
|
+
`readable, or — if this overwrite is intentional — re-run with the ` +
|
|
301
|
+
`environment variable GSD_ALLOW_PLANNING_SHRINK=1 to bypass this guard once.`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const oldLines = countLines(onDisk);
|
|
306
|
+
const newLines = countLines(content);
|
|
307
|
+
|
|
308
|
+
if (oldLines < FLOOR_LINES) {
|
|
309
|
+
process.exit(0); // sub-floor stub — ratio checks are meaningless here
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (newLines >= oldLines * SHRINK_RATIO) {
|
|
313
|
+
process.exit(0); // shrink (if any) is within tolerance
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// The mechanical hatch for workflow steps (see header): consulted only
|
|
317
|
+
// here, at the block point, so a within-tolerance write never burns it.
|
|
318
|
+
if (consumeSentinelFor(filePath, normalized)) {
|
|
319
|
+
process.exit(0); // armed for exactly this file, fresh, now consumed
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const pct = Math.round((newLines / oldLines) * 100);
|
|
323
|
+
// Typed fields (oldLines/newLines/overrideEnvVar/overrideSentinel) ride
|
|
324
|
+
// alongside the free-form reason so consumers — including this repo's
|
|
325
|
+
// tests — never have to regex the prose (CONTRIBUTING.md: no raw text
|
|
326
|
+
// matching).
|
|
327
|
+
emitBlock({
|
|
328
|
+
decision: 'block',
|
|
329
|
+
oldLines,
|
|
330
|
+
newLines,
|
|
331
|
+
overrideEnvVar: 'GSD_ALLOW_PLANNING_SHRINK',
|
|
332
|
+
overrideSentinel: SENTINEL_REL,
|
|
333
|
+
// Round 9 Major 2: the denial deliberately does NOT explain how to arm
|
|
334
|
+
// the sentinel — #973 was an agent reasoning past an advisory, and a
|
|
335
|
+
// block message that prints the bypass recipe hands that same agent a
|
|
336
|
+
// mechanical self-authorization at the moment it is blocked. The
|
|
337
|
+
// sentinel transport stays documented where humans and the workflow
|
|
338
|
+
// engine read (USER-GUIDE, complete-milestone.md); the typed
|
|
339
|
+
// overrideSentinel field above stays for the binding tests. The env
|
|
340
|
+
// var stays named per #2255's acceptance criterion ("the override must
|
|
341
|
+
// be real and its name must appear in the block message") — it cannot
|
|
342
|
+
// reach a hook from a per-step prefix, so naming it does not hand the
|
|
343
|
+
// blocked agent a same-tool bypass.
|
|
344
|
+
reason:
|
|
345
|
+
`Write guard: this Write would shrink '${filePath}' from ${oldLines} lines to ` +
|
|
346
|
+
`${newLines} (${pct}% of current). '${path.basename(filePath)}' is a curated planning ` +
|
|
347
|
+
`artifact; a whole-file Write this much smaller usually means the payload was built ` +
|
|
348
|
+
`from a partial read of the file and would destroy the sections outside that window ` +
|
|
349
|
+
`(#973: a planner collapsed ROADMAP.md 292 → 16 lines this way). To fix: use Edit for ` +
|
|
350
|
+
`a scoped change, or Read the full file and include every section in the Write. ` +
|
|
351
|
+
`Intentional milestone resets go through the workflow's documented escape hatch; ` +
|
|
352
|
+
`interactively, re-run with the environment variable GSD_ALLOW_PLANNING_SHRINK=1 ` +
|
|
353
|
+
`to bypass this guard once.`,
|
|
354
|
+
});
|
|
355
|
+
} catch {
|
|
356
|
+
// Silent fail — never block valid tool calls due to hook errors
|
|
357
|
+
process.exit(0);
|
|
358
|
+
}
|
|
359
|
+
});
|
package/hooks/hooks.json
CHANGED
|
@@ -21,6 +21,18 @@
|
|
|
21
21
|
"hooks": [
|
|
22
22
|
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-worktree-path-guard.js\"", "timeout": 5 }
|
|
23
23
|
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"matcher": "Write",
|
|
27
|
+
"hooks": [
|
|
28
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-write-guard.js\"", "timeout": 5 }
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"matcher": "Agent|Task",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-agent-isolation-guard.js\"", "timeout": 5 }
|
|
35
|
+
]
|
|
24
36
|
}
|
|
25
37
|
],
|
|
26
38
|
"PostToolUse": [
|
package/hooks/lib/git-cmd.js
CHANGED
|
@@ -19,9 +19,19 @@
|
|
|
19
19
|
* hook's own __dirname:
|
|
20
20
|
*
|
|
21
21
|
* const { isGitSubcommand } = require(path.join(__dirname, 'lib', 'git-cmd.js'));
|
|
22
|
+
*
|
|
23
|
+
* `tokenize()` delegates to the shared `src/token-scanner.cts` seam (ADR-3212
|
|
24
|
+
* §4, epic #3212 Phase 3, #3414) — the built `gsd-core/bin/lib/token-scanner.cjs`
|
|
25
|
+
* artifact, not a sibling hooks/-tree file, because hook scripts are staged as
|
|
26
|
+
* standalone files at install time and a sibling require is a staging
|
|
27
|
+
* dependency that can fail silently (see gsd-workflow-guard.js's own
|
|
28
|
+
* KIMI_TOOL_NAMES comment for the precedent this follows). Re-exported here
|
|
29
|
+
* unchanged — every existing caller's behavior is identical (parity-asserted
|
|
30
|
+
* in tests/token-scanner.test.cjs row 5).
|
|
22
31
|
*/
|
|
23
32
|
|
|
24
33
|
const path = require('path');
|
|
34
|
+
const { tokenizeShellLike } = require(path.join(__dirname, '..', '..', 'gsd-core', 'bin', 'lib', 'token-scanner.cjs'));
|
|
25
35
|
|
|
26
36
|
/**
|
|
27
37
|
* Git global options that take a following argument.
|
|
@@ -29,6 +39,7 @@ const path = require('path');
|
|
|
29
39
|
*/
|
|
30
40
|
const ARGUMENT_TAKING_FLAGS = new Set([
|
|
31
41
|
'-C', // working directory
|
|
42
|
+
'-c', // config override (separate-arg form: `git -c k=v …`; #3504)
|
|
32
43
|
'--git-dir', // path to git repository
|
|
33
44
|
'--work-tree', // path to working tree
|
|
34
45
|
'--namespace', // git namespace
|
|
@@ -57,94 +68,116 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
57
68
|
* Handles single-quoted strings, double-quoted strings, and unquoted tokens.
|
|
58
69
|
* Does NOT perform variable expansion or brace expansion.
|
|
59
70
|
*
|
|
71
|
+
* Delegates to the shared `src/token-scanner.cts` seam — see the module
|
|
72
|
+
* header comment for why the built artifact, not a sibling require, is used.
|
|
73
|
+
*
|
|
60
74
|
* @param {string} cmd
|
|
61
75
|
* @returns {string[]}
|
|
62
76
|
*/
|
|
63
77
|
function tokenize(cmd) {
|
|
64
|
-
|
|
65
|
-
let i = 0;
|
|
66
|
-
const len = cmd.length;
|
|
67
|
-
|
|
68
|
-
while (i < len) {
|
|
69
|
-
// Skip whitespace
|
|
70
|
-
while (i < len && /\s/.test(cmd[i])) i++;
|
|
71
|
-
if (i >= len) break;
|
|
72
|
-
|
|
73
|
-
let token = '';
|
|
74
|
-
while (i < len && !/\s/.test(cmd[i])) {
|
|
75
|
-
if (cmd[i] === "'") {
|
|
76
|
-
// Single-quoted string: take everything until closing '
|
|
77
|
-
i++;
|
|
78
|
-
while (i < len && cmd[i] !== "'") token += cmd[i++];
|
|
79
|
-
if (i < len) i++; // consume closing '
|
|
80
|
-
} else if (cmd[i] === '"') {
|
|
81
|
-
// Double-quoted string: take everything until closing " (no escape handling)
|
|
82
|
-
i++;
|
|
83
|
-
while (i < len && cmd[i] !== '"') token += cmd[i++];
|
|
84
|
-
if (i < len) i++; // consume closing "
|
|
85
|
-
} else {
|
|
86
|
-
token += cmd[i++];
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (token) tokens.push(token);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return tokens;
|
|
78
|
+
return tokenizeShellLike(cmd);
|
|
93
79
|
}
|
|
94
80
|
|
|
95
81
|
/**
|
|
96
|
-
*
|
|
82
|
+
* Walk past leading env-prefix assignments and global git options, same as
|
|
83
|
+
* `isGitSubcommand`'s phases 1-3. Returns the index of the subcommand token,
|
|
84
|
+
* or -1 if the command does not resolve to a git invocation at all.
|
|
97
85
|
*
|
|
98
|
-
* @param {string}
|
|
99
|
-
* @
|
|
100
|
-
* @returns {boolean}
|
|
86
|
+
* @param {string[]} tokens
|
|
87
|
+
* @returns {number}
|
|
101
88
|
*/
|
|
102
|
-
function
|
|
103
|
-
if (!cmd || !sub) return false;
|
|
104
|
-
|
|
105
|
-
const tokens = tokenize(cmd);
|
|
89
|
+
function skipToSubcommand(tokens) {
|
|
106
90
|
let i = 0;
|
|
107
|
-
|
|
108
|
-
// Phase 1: skip leading VAR=VALUE environment assignments
|
|
109
91
|
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) {
|
|
110
92
|
i++;
|
|
111
93
|
}
|
|
112
|
-
|
|
113
|
-
// Phase 2: the next token must be the git executable
|
|
114
|
-
if (i >= tokens.length) return false;
|
|
94
|
+
if (i >= tokens.length) return -1;
|
|
115
95
|
const gitToken = tokens[i++];
|
|
116
|
-
if (path.basename(gitToken) !== 'git') return
|
|
96
|
+
if (path.basename(gitToken) !== 'git') return -1;
|
|
117
97
|
|
|
118
|
-
// Phase 3: consume git global options
|
|
119
98
|
while (i < tokens.length) {
|
|
120
99
|
const t = tokens[i];
|
|
121
|
-
|
|
122
|
-
// --flag=value form for argument-taking flags
|
|
123
100
|
const eqIdx = t.indexOf('=');
|
|
124
101
|
const flagName = eqIdx !== -1 ? t.slice(0, eqIdx) : t;
|
|
125
102
|
if (ARGUMENT_TAKING_FLAGS.has(flagName)) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
103
|
+
i += eqIdx !== -1 ? 1 : 2;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// #3504: glued `-ckey=value` form — git accepts the config override with
|
|
107
|
+
// its argument attached (`git -cfoo.bar=1 …`). The eq-slice above yields
|
|
108
|
+
// flagName `-cfoo`, which no set contains, so without this arm the walk
|
|
109
|
+
// stops and the whole invocation is misclassified as not-git.
|
|
110
|
+
if (/^-c\S*=/.test(t)) {
|
|
111
|
+
i++;
|
|
133
112
|
continue;
|
|
134
113
|
}
|
|
135
|
-
|
|
136
114
|
if (BOOLEAN_FLAGS.has(t)) {
|
|
137
115
|
i++;
|
|
138
116
|
continue;
|
|
139
117
|
}
|
|
140
|
-
|
|
141
|
-
// Not a global option — this is the subcommand
|
|
142
118
|
break;
|
|
143
119
|
}
|
|
120
|
+
return i;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract the branch-name argument from a git command line that creates or
|
|
125
|
+
* references one — `git checkout -b <name>` or `git branch <name>`. Returns
|
|
126
|
+
* null for any other command, including plain `git checkout <ref>` (switches
|
|
127
|
+
* branches, does not create one) and commands where a checkout/branch-shaped
|
|
128
|
+
* substring appears only inside a quoted argument (e.g. a commit message).
|
|
129
|
+
*
|
|
130
|
+
* New capability (ADR-3212 §4, epic #3212 Phase 3, #3414) exercising the
|
|
131
|
+
* shared scanner on the domain the ADR names ("a branch name... [is] not
|
|
132
|
+
* regular") — not a migration of existing duplicated logic; no prior
|
|
133
|
+
* implementation of this existed in the repo (design doc §1.2).
|
|
134
|
+
*
|
|
135
|
+
* @param {string} cmd
|
|
136
|
+
* @returns {string | null}
|
|
137
|
+
*/
|
|
138
|
+
function extractBranchArgument(cmd) {
|
|
139
|
+
if (!cmd) return null;
|
|
140
|
+
const tokens = tokenizeShellLike(cmd);
|
|
141
|
+
const subIdx = skipToSubcommand(tokens);
|
|
142
|
+
if (subIdx === -1 || subIdx >= tokens.length) return null;
|
|
143
|
+
const sub = tokens[subIdx];
|
|
144
|
+
|
|
145
|
+
if (sub === 'checkout') {
|
|
146
|
+
for (let j = subIdx + 1; j < tokens.length; j++) {
|
|
147
|
+
if (tokens[j] === '-b' && j + 1 < tokens.length) return tokens[j + 1];
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (sub === 'branch') {
|
|
153
|
+
for (let j = subIdx + 1; j < tokens.length; j++) {
|
|
154
|
+
if (!tokens[j].startsWith('-')) return tokens[j];
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Return true if `cmd` invokes the git subcommand `sub`.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} cmd - Full shell command string (may include env vars, full paths)
|
|
166
|
+
* @param {string} sub - Subcommand to test for, e.g. 'commit'
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
function isGitSubcommand(cmd, sub) {
|
|
170
|
+
if (!cmd || !sub) return false;
|
|
171
|
+
|
|
172
|
+
// Phases 1-3 (env-prefix skip, git-executable check, global-option consume)
|
|
173
|
+
// extracted verbatim into skipToSubcommand — byte-identical logic, shared
|
|
174
|
+
// with extractBranchArgument rather than a second copy (#3212 Phase 3).
|
|
175
|
+
const tokens = tokenizeShellLike(cmd);
|
|
176
|
+
const subIdx = skipToSubcommand(tokens);
|
|
144
177
|
|
|
145
178
|
// Phase 4: check the subcommand
|
|
146
|
-
if (
|
|
147
|
-
return tokens[
|
|
179
|
+
if (subIdx === -1 || subIdx >= tokens.length) return false;
|
|
180
|
+
return tokens[subIdx] === sub;
|
|
148
181
|
}
|
|
149
182
|
|
|
150
|
-
module.exports = { isGitSubcommand, tokenize };
|
|
183
|
+
module.exports = { isGitSubcommand, tokenize, extractBranchArgument, skipToSubcommand };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* injection-patterns.js — the shared prompt-injection pattern list (#3504, epic #1900).
|
|
5
|
+
*
|
|
6
|
+
* Single source of truth for the standard injection signatures used by BOTH
|
|
7
|
+
* gsd-prompt-guard.js (PreToolUse scan of writes into .planning/) and
|
|
8
|
+
* gsd-read-injection-scanner.js (PostToolUse scan of Read/WebFetch/WebSearch
|
|
9
|
+
* content). Previously each hook carried a byte-identical copy ("inlined for
|
|
10
|
+
* hook independence") that could silently drift — a pattern tightened in one
|
|
11
|
+
* would stop protecting the other surface.
|
|
12
|
+
*
|
|
13
|
+
* Why a shared lib require is safe here (the old inlining rationale, retired):
|
|
14
|
+
* the installer stages hooks/lib/ from the GSD_HOOK_LIB_FILES allowlist for the
|
|
15
|
+
* shared-bundle surfaces (Claude-family settings.json runtimes and Kimi), the
|
|
16
|
+
* Cursor stager auto-discovers require('./lib/...') in staged scripts and fails
|
|
17
|
+
* the install loudly when a helper is missing (#2587), and the plugin path
|
|
18
|
+
* ships this directory wholesale.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT unified with src/security.cts's scanForInjection set: that
|
|
21
|
+
* set runs inside the compiled lib tree; hooks must stay loadable standalone
|
|
22
|
+
* without it. The two lists are different surfaces by design, not drift.
|
|
23
|
+
*
|
|
24
|
+
* Keep this file free of literal 'gsd:' text — the stager rewrites that marker
|
|
25
|
+
* in staged hook content.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const INJECTION_PATTERNS = Object.freeze([
|
|
29
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
30
|
+
/ignore\s+(all\s+)?above\s+instructions/i,
|
|
31
|
+
/disregard\s+(all\s+)?previous/i,
|
|
32
|
+
/forget\s+(all\s+)?(your\s+)?instructions/i,
|
|
33
|
+
/override\s+(system|previous)\s+(prompt|instructions)/i,
|
|
34
|
+
/you\s+are\s+now\s+(?:a|an|the)\s+/i,
|
|
35
|
+
/act\s+as\s+(?:a|an|the)\s+(?!plan|phase|wave)/i,
|
|
36
|
+
/pretend\s+(?:you(?:'re| are)\s+|to\s+be\s+)/i,
|
|
37
|
+
/from\s+now\s+on,?\s+you\s+(?:are|will|should|must)/i,
|
|
38
|
+
/(?:print|output|reveal|show|display|repeat)\s+(?:your\s+)?(?:system\s+)?(?:prompt|instructions)/i,
|
|
39
|
+
/<\/?(?:system|assistant|human)>/i,
|
|
40
|
+
/\[SYSTEM\]/i,
|
|
41
|
+
/\[INST\]/i,
|
|
42
|
+
/<<\s*SYS\s*>>/i,
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
module.exports = { INJECTION_PATTERNS };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// hooks/lib/isolation-deny-reason.js — shared, frozen reason-code enum for
|
|
3
|
+
// the #3045 dispatch-isolation guards' block/deny decisions
|
|
4
|
+
// (hooks/gsd-agent-isolation-guard.js, hooks/gsd-cursor-subagent-start.js).
|
|
5
|
+
//
|
|
6
|
+
// CONTRIBUTING.md ("Prohibited: Raw Text Matching on Test Outputs") bans
|
|
7
|
+
// asserting on a hook's free-form, human-readable reason/user_message prose
|
|
8
|
+
// — that text is for the operator/model reading the denial and may change
|
|
9
|
+
// wording without notice. Every block/deny decision therefore ALSO carries
|
|
10
|
+
// one of these STABLE codes (surfaced on the hook's stdout JSON as
|
|
11
|
+
// `reason_code`), so tests assert `out.reason_code === REASON_CODE.X`
|
|
12
|
+
// instead of regexing the message (mirrors the REASON enum convention in
|
|
13
|
+
// gsd-core/bin/verify-reapply-patches.cjs).
|
|
14
|
+
//
|
|
15
|
+
// Adding a new code requires updating this enum AND any test that locks the
|
|
16
|
+
// documented set.
|
|
17
|
+
const REASON_CODE = Object.freeze({
|
|
18
|
+
// The compiled runtime library (gsd-core/bin/lib/*.cjs) is missing and
|
|
19
|
+
// could not be self-built (ensure-runtime-build.cjs's RuntimeBuildError).
|
|
20
|
+
RUNTIME_BUILD_FAILED: 'runtime_build_failed',
|
|
21
|
+
// The project's dispatch-isolation configuration ('.planning/config.json')
|
|
22
|
+
// could not be read or resolved for a reason OTHER than a runtime-build
|
|
23
|
+
// failure (unreadable/malformed config, unexpected resolver error).
|
|
24
|
+
CONFIG_UNREADABLE: 'config_unreadable',
|
|
25
|
+
// Isolation resolves to "harness-worktree" but the Agent()/Task() dispatch
|
|
26
|
+
// is missing the harness's isolation flag/kwarg.
|
|
27
|
+
HARNESS_FLAG_MISSING: 'harness_flag_missing',
|
|
28
|
+
// Isolation resolves to "harness-worktree" but the dispatch payload carries
|
|
29
|
+
// no usable subagent_type, so the guard cannot confirm it is a GSD executor.
|
|
30
|
+
NO_SUBAGENT_TYPE: 'no_subagent_type',
|
|
31
|
+
// Isolation resolves to "harness-worktree" but whether the workspace root
|
|
32
|
+
// is an isolated worktree could not be determined (e.g. git unresponsive).
|
|
33
|
+
CANNOT_DETERMINE_ISOLATION: 'cannot_determine_isolation',
|
|
34
|
+
// Isolation resolves to "harness-worktree" and the workspace root is
|
|
35
|
+
// confirmed NOT an isolated worktree.
|
|
36
|
+
NOT_ISOLATED_WORKTREE: 'not_isolated_worktree',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
module.exports = { REASON_CODE };
|