@opengsd/gsd-core 1.10.0 → 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 +1 -1
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +5 -5
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +37 -0
- package/agents/gsd-planner.md +44 -46
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +12 -3
- package/bin/install.js +841 -971
- 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 +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.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 +469 -88
- package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
- package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
- package/gsd-core/bin/lib/api-coverage.cjs +3 -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 +876 -240
- package/gsd-core/bin/lib/broken-windows.cjs +1 -1
- 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 +575 -101
- 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 +495 -22
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +71 -37
- 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 +22 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +651 -86
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +75 -0
- package/gsd-core/bin/lib/config.cjs +10 -1
- package/gsd-core/bin/lib/core-utils.cjs +127 -29
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +155 -20
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
- 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 +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-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init.cjs +321 -129
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +745 -258
- 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 +134 -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-migrations.cjs +138 -31
- package/gsd-core/bin/lib/io.cjs +10 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +754 -70
- 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 +444 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +125 -18
- package/gsd-core/bin/lib/phase.cjs +646 -143
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- 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 +56 -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/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
- 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 +34 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +385 -94
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -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 +115 -3
- 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/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
- package/gsd-core/bin/lib/smart-entry.cjs +142 -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 +371 -117
- package/gsd-core/bin/lib/state.cjs +1794 -357
- package/gsd-core/bin/lib/surface.cjs +23 -9
- 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 +9 -3
- package/gsd-core/bin/lib/uat.cjs +399 -56
- 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 +24 -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 +258 -8
- package/gsd-core/bin/lib/verify.cjs +368 -888
- 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.cjs +2 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- 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/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- 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/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/autonomous.md +1 -1
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +38 -12
- package/gsd-core/workflows/complete-milestone.md +141 -18
- package/gsd-core/workflows/debug.md +7 -5
- package/gsd-core/workflows/diagnose-issues.md +35 -9
- 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.md +2 -1
- 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 +31 -6
- 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 +2 -0
- package/gsd-core/workflows/execute-phase.md +38 -50
- 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/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +5 -5
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone.md +14 -9
- package/gsd-core/workflows/new-project.md +14 -14
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase.md +41 -17
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress.md +34 -6
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
- package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
- package/gsd-core/workflows/quick.md +35 -15
- package/gsd-core/workflows/review.md +26 -5
- 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/spec-phase.md +30 -12
- package/gsd-core/workflows/sync-skills.md +63 -8
- package/gsd-core/workflows/transition.md +46 -11
- package/gsd-core/workflows/ui-phase.md +5 -5
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update.md +1 -1
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +9 -7
- package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- 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 +77 -2
- 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 +38 -24
- package/hooks/dist/gsd-statusline.js +18 -0
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- 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 +9 -0
- package/hooks/gsd-agent-isolation-guard.js +103 -14
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +77 -2
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +38 -24
- package/hooks/gsd-statusline.js +18 -0
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- 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 +9 -0
- package/package.json +21 -9
- package/pi/gsd.cjs +19 -5
- 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/changeset/lint.cjs +60 -5
- 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-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-registry.cjs +3 -14
- 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 +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-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -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 +21 -10
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +25 -0
- 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 +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 +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 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -72,23 +72,10 @@ const MARKDOWN_LINK_PATTERNS = [
|
|
|
72
72
|
},
|
|
73
73
|
];
|
|
74
74
|
|
|
75
|
-
// Standard injection patterns —
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
/disregard\s+(all\s+)?previous/i,
|
|
80
|
-
/forget\s+(all\s+)?(your\s+)?instructions/i,
|
|
81
|
-
/override\s+(system|previous)\s+(prompt|instructions)/i,
|
|
82
|
-
/you\s+are\s+now\s+(?:a|an|the)\s+/i,
|
|
83
|
-
/act\s+as\s+(?:a|an|the)\s+(?!plan|phase|wave)/i,
|
|
84
|
-
/pretend\s+(?:you(?:'re| are)\s+|to\s+be\s+)/i,
|
|
85
|
-
/from\s+now\s+on,?\s+you\s+(?:are|will|should|must)/i,
|
|
86
|
-
/(?:print|output|reveal|show|display|repeat)\s+(?:your\s+)?(?:system\s+)?(?:prompt|instructions)/i,
|
|
87
|
-
/<\/?(?:system|assistant|human)>/i,
|
|
88
|
-
/\[SYSTEM\]/i,
|
|
89
|
-
/\[INST\]/i,
|
|
90
|
-
/<<\s*SYS\s*>>/i,
|
|
91
|
-
];
|
|
75
|
+
// Standard injection patterns — shared with gsd-prompt-guard.js via
|
|
76
|
+
// hooks/lib/injection-patterns.js so the two surfaces cannot drift (#3504).
|
|
77
|
+
// Staging of the lib helper is allowlisted in GSD_HOOK_LIB_FILES (bin/install.js).
|
|
78
|
+
const { INJECTION_PATTERNS } = require('./lib/injection-patterns.js');
|
|
92
79
|
|
|
93
80
|
const ALL_PATTERNS = [...INJECTION_PATTERNS, ...SUMMARISATION_PATTERNS];
|
|
94
81
|
|
|
@@ -98,6 +85,16 @@ const ALL_PATTERNS = [...INJECTION_PATTERNS, ...SUMMARISATION_PATTERNS];
|
|
|
98
85
|
// construction. Normalized to forward slashes to match `p` below.
|
|
99
86
|
const OWN_BUNDLE_PREFIX = __dirname.replace(/\\/g, '/').replace(/\/+$/, '') + '/';
|
|
100
87
|
|
|
88
|
+
// Synthetic rule ids for the finding classes that have no entry in
|
|
89
|
+
// MARKDOWN_LINK_PATTERNS. Frozen and referenced from BOTH the push sites and
|
|
90
|
+
// renderFinding so the two can never drift — a bare literal repeated at each
|
|
91
|
+
// site is how a rename silently falls through to the generic render branch.
|
|
92
|
+
const RULE_IDS = Object.freeze({
|
|
93
|
+
INJECTION_PATTERN: 'INJECTION-PATTERN',
|
|
94
|
+
INVISIBLE_UNICODE: 'INVISIBLE-UNICODE',
|
|
95
|
+
UNICODE_TAG_BLOCK: 'UNICODE-TAG-BLOCK',
|
|
96
|
+
});
|
|
97
|
+
|
|
101
98
|
function isExcludedPath(filePath) {
|
|
102
99
|
const p = filePath.replace(/\\/g, '/');
|
|
103
100
|
return (
|
|
@@ -267,12 +264,19 @@ process.stdin.on('end', () => {
|
|
|
267
264
|
process.exit(0);
|
|
268
265
|
}
|
|
269
266
|
|
|
267
|
+
// Typed findings IR — single source of truth for both the machine-readable
|
|
268
|
+
// `findings` array and the rendered advisory prose. Never build these as two
|
|
269
|
+
// parallel arrays: that invites the generative-fix-divergence defect class
|
|
270
|
+
// where the rendered text and the structured data silently drift apart.
|
|
270
271
|
const findings = [];
|
|
271
272
|
|
|
272
273
|
for (const pattern of ALL_PATTERNS) {
|
|
273
274
|
if (pattern.test(content)) {
|
|
274
275
|
// Trim pattern source for readable output
|
|
275
|
-
findings.push(
|
|
276
|
+
findings.push({
|
|
277
|
+
ruleId: RULE_IDS.INJECTION_PATTERN,
|
|
278
|
+
match: pattern.source.replace(/\\s\+/g, '-').replace(/[()\\]/g, '').substring(0, 50),
|
|
279
|
+
});
|
|
276
280
|
}
|
|
277
281
|
}
|
|
278
282
|
|
|
@@ -284,19 +288,19 @@ process.stdin.on('end', () => {
|
|
|
284
288
|
const m = line.match(entry.pattern);
|
|
285
289
|
if (!m) continue;
|
|
286
290
|
if (entry.safePredicate && entry.safePredicate(line)) continue;
|
|
287
|
-
findings.push(
|
|
291
|
+
findings.push({ ruleId: entry.ruleId, match: m[0].substring(0, 40) });
|
|
288
292
|
}
|
|
289
293
|
}
|
|
290
294
|
|
|
291
295
|
// Invisible Unicode (zero-width, RTL override, soft hyphen, BOM)
|
|
292
296
|
if (/[\u200B-\u200F\u2028-\u202F\uFEFF\u00AD\u2060-\u2069]/.test(content)) {
|
|
293
|
-
findings.push(
|
|
297
|
+
findings.push({ ruleId: RULE_IDS.INVISIBLE_UNICODE, match: null });
|
|
294
298
|
}
|
|
295
299
|
|
|
296
300
|
// Unicode tag block U+E0000–E007F (invisible instruction injection vector)
|
|
297
301
|
try {
|
|
298
302
|
if (/[\u{E0000}-\u{E007F}]/u.test(content)) {
|
|
299
|
-
findings.push(
|
|
303
|
+
findings.push({ ruleId: RULE_IDS.UNICODE_TAG_BLOCK, match: null });
|
|
300
304
|
}
|
|
301
305
|
} catch {
|
|
302
306
|
// Engine does not support Unicode property escapes — skip this check
|
|
@@ -306,6 +310,16 @@ process.stdin.on('end', () => {
|
|
|
306
310
|
process.exit(0);
|
|
307
311
|
}
|
|
308
312
|
|
|
313
|
+
// Renders one finding back into the exact prose fragment the advisory has
|
|
314
|
+
// always embedded. Kept as the ONLY place that maps IR -> text, so the
|
|
315
|
+
// `additionalContext` string and the `findings` array can never diverge.
|
|
316
|
+
function renderFinding(f) {
|
|
317
|
+
if (f.ruleId === RULE_IDS.INVISIBLE_UNICODE) return 'invisible-unicode';
|
|
318
|
+
if (f.ruleId === RULE_IDS.UNICODE_TAG_BLOCK) return 'unicode-tag-block';
|
|
319
|
+
if (f.ruleId === RULE_IDS.INJECTION_PATTERN) return f.match;
|
|
320
|
+
return `${f.ruleId}:${f.match}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
309
323
|
const severity = findings.length >= 3 ? 'HIGH' : 'LOW';
|
|
310
324
|
const label = toolName === 'Read' ? path.basename(source) : source;
|
|
311
325
|
const detail = severity === 'HIGH'
|
|
@@ -313,7 +327,7 @@ process.stdin.on('end', () => {
|
|
|
313
327
|
: 'Single pattern match may be a false positive (e.g., documentation). Proceed with awareness.';
|
|
314
328
|
const advisory =
|
|
315
329
|
`\u26a0\ufe0f INJECTION SCAN [${severity}] (${toolName}): "${label}" triggered ` +
|
|
316
|
-
`${findings.length} pattern(s): ${findings.join(', ')}. ` +
|
|
330
|
+
`${findings.length} pattern(s): ${findings.map(renderFinding).join(', ')}. ` +
|
|
317
331
|
`This content is now in your conversation context. ${detail} Source: ${source}`;
|
|
318
332
|
|
|
319
333
|
// Opt-in blocking: only when configured AND high-confidence
|
|
@@ -330,8 +344,8 @@ process.stdin.on('end', () => {
|
|
|
330
344
|
const output = blocking
|
|
331
345
|
? { decision: 'block',
|
|
332
346
|
reason: `Prompt-injection blocked (${toolName}). ${advisory}`,
|
|
333
|
-
hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: advisory } }
|
|
334
|
-
: { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: advisory } };
|
|
347
|
+
hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: advisory, findings } }
|
|
348
|
+
: { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: advisory, findings } };
|
|
335
349
|
|
|
336
350
|
process.stdout.write(JSON.stringify(output));
|
|
337
351
|
} catch {
|
package/hooks/gsd-statusline.js
CHANGED
|
@@ -9,6 +9,24 @@ const os = require('os');
|
|
|
9
9
|
// Namespace (not destructured) so tests can inject spawn failures by
|
|
10
10
|
// monkeypatching childProcess.execFileSync.
|
|
11
11
|
const childProcess = require('child_process');
|
|
12
|
+
// #3582: gsd-core/bin/lib/*.cjs (semver-compare.cjs, state-document.cjs,
|
|
13
|
+
// active-workstream-store.cjs, planning-workspace.cjs — required below) and
|
|
14
|
+
// package-identity.cjs are tsc build artifacts (ADR-457), gitignored and
|
|
15
|
+
// absent on a raw plugin-marketplace / git-clone install that never ran
|
|
16
|
+
// `npm run build:lib`. The statusline renders on EVERY prompt, so a build
|
|
17
|
+
// failure here must DEGRADE (print nothing, exit 0) rather than crash
|
|
18
|
+
// Claude Code's per-render statusline hook. Scoped to the spawned-as-a-script
|
|
19
|
+
// path (`require.main === module`) — a test `require()` of this module for
|
|
20
|
+
// its pure helpers assumes a built tree, same as every other hook test.
|
|
21
|
+
if (require.main === module) {
|
|
22
|
+
try {
|
|
23
|
+
const { ensureRuntimeBuild } = require('../gsd-core/bin/ensure-runtime-build.cjs');
|
|
24
|
+
ensureRuntimeBuild();
|
|
25
|
+
} catch (e) {
|
|
26
|
+
process.stdout.write('');
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
12
30
|
const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
|
|
13
31
|
const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
|
|
14
32
|
const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
|
|
@@ -15,7 +15,28 @@
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const os = require('os');
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
// #3582: gsd-core/bin/lib/package-identity.cjs is a tsc build artifact
|
|
20
|
+
// (ADR-457), gitignored and absent on a raw plugin-marketplace / git-clone
|
|
21
|
+
// install that never ran `npm run build:lib`. This is an opt-in SessionStart
|
|
22
|
+
// hook — a build failure here must DEGRADE, not crash session start. With
|
|
23
|
+
// PACKAGE_NAME left null, buildBannerOutput's own lineage guard
|
|
24
|
+
// (`!cache.package_name || cache.package_name !== PACKAGE_NAME`) always
|
|
25
|
+
// treats the cache as untrusted, so main() falls through to its existing
|
|
26
|
+
// silent "print nothing" path below — no separate degrade branch needed.
|
|
27
|
+
// This try/require/ensureRuntimeBuild/require/catch shape is deliberately
|
|
28
|
+
// duplicated (not extracted to hooks/lib/) — see
|
|
29
|
+
// gsd-check-update-worker.js's identical #3582 comment for why.
|
|
30
|
+
let PACKAGE_NAME = null;
|
|
31
|
+
let updateCacheFileName = 'gsd-update-check.json';
|
|
32
|
+
try {
|
|
33
|
+
const { ensureRuntimeBuild } = require('../gsd-core/bin/ensure-runtime-build.cjs');
|
|
34
|
+
ensureRuntimeBuild();
|
|
35
|
+
({ PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs'));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// Runtime library missing/broken and could not self-build — degrade to the
|
|
38
|
+
// fallbacks above rather than crash the SessionStart hook.
|
|
39
|
+
}
|
|
19
40
|
|
|
20
41
|
// Suppress repeat parse-error banners for 24 hours so a genuinely broken
|
|
21
42
|
// cache file doesn't nag the user every session.
|
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
// Detects when Claude attempts file edits outside a GSD workflow context
|
|
5
5
|
// (no active /gsd- skill or Task subagent) and injects an advisory warning.
|
|
6
6
|
//
|
|
7
|
-
// This is a SOFT guard — it advises, not blocks. The edit still
|
|
8
|
-
// The warning nudges Claude to use /gsd:quick or /gsd:fast instead
|
|
9
|
-
// making direct edits that bypass state tracking.
|
|
7
|
+
// This is a SOFT guard for edits — it advises, not blocks. The edit still
|
|
8
|
+
// proceeds. The warning nudges Claude to use /gsd:quick or /gsd:fast instead
|
|
9
|
+
// of making direct edits that bypass state tracking.
|
|
10
|
+
//
|
|
11
|
+
// ONE hard block lives here: `git add -f` on an agent/worktree-agent branch
|
|
12
|
+
// (WORKTREE_AGENT_FORCE_ADD_FORBIDDEN) — and that block leg fails CLOSED on
|
|
13
|
+
// internal error (#3504): when the guard is enabled and the blocking context
|
|
14
|
+
// holds, a thrown error exits 2 (block), not 0. The advisory legs keep the
|
|
15
|
+
// fail-open posture — a broken advisory must never wedge every tool call.
|
|
10
16
|
//
|
|
11
17
|
// Enable via config: hooks.workflow_guard: true (default: false)
|
|
12
18
|
// Only triggers on Write/Edit tool calls to non-.planning/ files.
|
|
@@ -14,40 +20,46 @@
|
|
|
14
20
|
const fs = require('fs');
|
|
15
21
|
const path = require('path');
|
|
16
22
|
const { spawnSync } = require('child_process');
|
|
17
|
-
const { tokenize } = require('./lib/git-cmd.js');
|
|
23
|
+
const { tokenize, skipToSubcommand } = require('./lib/git-cmd.js');
|
|
18
24
|
|
|
19
25
|
function forceGitAddCwds(command, defaultCwd) {
|
|
20
26
|
const tokens = tokenize(command || '');
|
|
21
27
|
const separators = new Set(['&&', '||', ';', '|']);
|
|
22
28
|
const cwdList = [];
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
// #3504: per-segment walk driven by git-cmd.js's canonical skipToSubcommand.
|
|
30
|
+
// The previous inline walk knew six global flags and silently missed the
|
|
31
|
+
// rest — `git -c core.hooksPath=/tmp/x add -f x`, `git --no-optional-locks
|
|
32
|
+
// add -f x`, `--literal-pathspecs`, `--namespace=…` and friends all fell
|
|
33
|
+
// through to a silent exit 0, a no-crash bypass the fail-closed catch is
|
|
34
|
+
// structurally blind to (it only fires on throws). Sharing the classifier
|
|
35
|
+
// makes the block's flag knowledge exactly the classifier's.
|
|
36
|
+
const segments = [];
|
|
37
|
+
let start = 0;
|
|
38
|
+
for (let i = 0; i <= tokens.length; i++) {
|
|
39
|
+
if (i === tokens.length || separators.has(tokens[i])) {
|
|
40
|
+
if (i > start) segments.push(tokens.slice(start, i));
|
|
41
|
+
start = i + 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const seg of segments) {
|
|
45
|
+
const subIdx = skipToSubcommand(seg);
|
|
46
|
+
if (subIdx === -1 || subIdx >= seg.length || seg[subIdx] !== 'add') continue;
|
|
25
47
|
|
|
26
|
-
|
|
48
|
+
// Resolve a `-C <dir>` (separate-arg form) preceding the subcommand so
|
|
49
|
+
// the branch is probed at the repository the add targets.
|
|
27
50
|
let gitCwd = defaultCwd;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
gitCwd = path.resolve(gitCwd, tokens[j + 1]);
|
|
33
|
-
j += 2;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (['-C', '--git-dir', '--work-tree'].includes(flagName) && !token.includes('=')) {
|
|
37
|
-
j += 2;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
if (['--git-dir', '--work-tree', '--no-pager', '-p', '-P'].includes(flagName)) {
|
|
41
|
-
j++;
|
|
51
|
+
for (let k = 0; k < subIdx; ) {
|
|
52
|
+
if (seg[k] === '-C' && k + 1 < subIdx) {
|
|
53
|
+
gitCwd = path.resolve(gitCwd, seg[k + 1]);
|
|
54
|
+
k += 2;
|
|
42
55
|
continue;
|
|
43
56
|
}
|
|
44
|
-
|
|
57
|
+
k++;
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
if (tokens[k] === '--force' || tokens[k] === '-f' || /^-[A-Za-z]*f[A-Za-z]*$/.test(tokens[k])) {
|
|
60
|
+
for (let k = subIdx + 1; k < seg.length; k++) {
|
|
61
|
+
if (seg[k] === '--') break;
|
|
62
|
+
if (seg[k] === '--force' || seg[k] === '-f' || /^-[A-Za-z]*f[A-Za-z]*$/.test(seg[k])) {
|
|
51
63
|
cwdList.push(gitCwd);
|
|
52
64
|
break;
|
|
53
65
|
}
|
|
@@ -62,11 +74,86 @@ function currentBranch(cwd) {
|
|
|
62
74
|
encoding: 'utf8',
|
|
63
75
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
64
76
|
windowsHide: true,
|
|
77
|
+
// #3504: bounded — this ran unbounded before, an indefinite hang under a
|
|
78
|
+
// wedged git would hang every PreToolUse call. Host wiring allows a 5s
|
|
79
|
+
// budget for the whole hook, so the probe gets 2s of it; a timeout
|
|
80
|
+
// returns '' (branch unknown), which both the block decision and the
|
|
81
|
+
// fail-closed re-check treat as "cannot establish agent branch".
|
|
82
|
+
timeout: 2000,
|
|
83
|
+
killSignal: 'SIGTERM',
|
|
65
84
|
});
|
|
66
85
|
if (result.status !== 0) return '';
|
|
67
86
|
return result.stdout.trim();
|
|
68
87
|
}
|
|
69
88
|
|
|
89
|
+
// Agent-branch predicate, shared by the happy-path detector and the fail-closed
|
|
90
|
+
// re-check so the block's scope cannot drift between the two paths (#3504).
|
|
91
|
+
function isAgentBranch(branch) {
|
|
92
|
+
return /^(worktree-)?agent-/.test(branch);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Typed cwd read, shared by both paths for the same reason: a non-string cwd
|
|
96
|
+
// degrades to process.cwd() instead of throwing inside path.join().
|
|
97
|
+
function payloadCwd(data) {
|
|
98
|
+
return typeof data?.cwd === 'string' && data.cwd ? data.cwd : process.cwd();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The force-add block payload, emitted by the happy-path detector and the
|
|
102
|
+
// fail-closed catch — one source so the two exits cannot drift. `origin`
|
|
103
|
+
// distinguishes them structurally ('force-add-detected' vs 'fail-closed') so a
|
|
104
|
+
// consumer — or the model reading Kimi's stderr feedback — is never told a
|
|
105
|
+
// force-add was detected when the call was in fact blocked unanalyzed.
|
|
106
|
+
function emitForceAddBlock(origin) {
|
|
107
|
+
const failClosed = origin === 'fail-closed';
|
|
108
|
+
const reason = failClosed
|
|
109
|
+
? 'workflow guard internal error on an agent branch - failing closed. The command was NOT analyzed and was NOT confirmed to be a force-add. Retry the call or inspect the guard.'
|
|
110
|
+
: 'agent/worktree-agent branches must not run git add -f or git add --force. Respect the SDK skipped_gitignored/skipped_commit_docs_false contract and leave gitignored files untracked.';
|
|
111
|
+
const output = {
|
|
112
|
+
decision: 'block',
|
|
113
|
+
code: 'WORKTREE_AGENT_FORCE_ADD_FORBIDDEN',
|
|
114
|
+
origin: failClosed ? 'fail-closed' : 'force-add-detected',
|
|
115
|
+
reason,
|
|
116
|
+
};
|
|
117
|
+
process.stdout.write(JSON.stringify(output));
|
|
118
|
+
// Kimi CLI's exit-2 protocol feeds stderr back to the model (#2304)
|
|
119
|
+
process.stderr.write(output.reason);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// #3504 fail-closed context re-derivation. Runs INSIDE the outer catch, after
|
|
123
|
+
// an internal error, and answers exactly one question: does the blocking
|
|
124
|
+
// context of the force-add guard hold for this payload? Each stage is guarded —
|
|
125
|
+
// a re-derivation that itself throws must degrade to "cannot establish" (false),
|
|
126
|
+
// never take down the catch. Deliberately does NOT re-detect the force-add: the
|
|
127
|
+
// error may live in the detector itself, and on an agent branch with the guard
|
|
128
|
+
// enabled, a Bash call under an internal error is conservative-correct to block.
|
|
129
|
+
// Anything it cannot establish (unparseable payload, non-Bash tool, guard
|
|
130
|
+
// disabled, branch not determinably agent-*) fails open, preserving the
|
|
131
|
+
// advisory legs' fail-open posture.
|
|
132
|
+
function failClosedBlockContext(rawInput) {
|
|
133
|
+
let data;
|
|
134
|
+
try {
|
|
135
|
+
data = normalizeKimiPayload(JSON.parse(rawInput));
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (data === null || typeof data !== 'object' || data.tool_name !== 'Bash') return false;
|
|
140
|
+
const cwd = payloadCwd(data);
|
|
141
|
+
let enabled;
|
|
142
|
+
try {
|
|
143
|
+
enabled = workflowGuardEnabled(cwd);
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (!enabled) return false;
|
|
148
|
+
let branch;
|
|
149
|
+
try {
|
|
150
|
+
branch = currentBranch(cwd);
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return isAgentBranch(branch);
|
|
155
|
+
}
|
|
156
|
+
|
|
70
157
|
function workflowGuardEnabled(cwd) {
|
|
71
158
|
const configPath = path.join(cwd, '.planning', 'config.json');
|
|
72
159
|
if (!fs.existsSync(configPath)) return false;
|
|
@@ -180,6 +267,15 @@ process.stdin.on('end', () => {
|
|
|
180
267
|
clearTimeout(stdinTimeout);
|
|
181
268
|
try {
|
|
182
269
|
const data = normalizeKimiPayload(JSON.parse(input));
|
|
270
|
+
// #3504 test-only fault seam: throws right after parse so the fail-closed
|
|
271
|
+
// posture of the outer catch is exercisable — no JSON-expressible input
|
|
272
|
+
// throws in this handler today (#2547/#2595 hardened every read). Gated on
|
|
273
|
+
// GSD_TEST_MODE as well so a leaked GSD_TEST_WORKFLOW_GUARD_FAULT in a real
|
|
274
|
+
// shell cannot wedge a production session. The fault's failure direction
|
|
275
|
+
// is CLOSED: the catch below may block, never bypass a block.
|
|
276
|
+
if (process.env.GSD_TEST_MODE === '1' && process.env.GSD_TEST_WORKFLOW_GUARD_FAULT === '1') {
|
|
277
|
+
throw new Error('GSD_TEST_WORKFLOW_GUARD_FAULT: injected fault');
|
|
278
|
+
}
|
|
183
279
|
const toolName = data.tool_name;
|
|
184
280
|
const cwd = data.cwd || process.cwd();
|
|
185
281
|
const isWorkflowGuardEnabled = workflowGuardEnabled(cwd);
|
|
@@ -192,14 +288,7 @@ process.stdin.on('end', () => {
|
|
|
192
288
|
for (const gitCwd of forceGitAddCwds(command, cwd)) {
|
|
193
289
|
const branch = currentBranch(gitCwd);
|
|
194
290
|
if (/^(worktree-)?agent-/.test(branch)) {
|
|
195
|
-
|
|
196
|
-
decision: 'block',
|
|
197
|
-
code: 'WORKTREE_AGENT_FORCE_ADD_FORBIDDEN',
|
|
198
|
-
reason: 'agent/worktree-agent branches must not run git add -f or git add --force. Respect the SDK skipped_gitignored/skipped_commit_docs_false contract and leave gitignored files untracked.',
|
|
199
|
-
};
|
|
200
|
-
process.stdout.write(JSON.stringify(output));
|
|
201
|
-
// Kimi CLI's exit-2 protocol feeds stderr back to the model (#2304)
|
|
202
|
-
process.stderr.write(output.reason);
|
|
291
|
+
emitForceAddBlock();
|
|
203
292
|
process.exit(2);
|
|
204
293
|
}
|
|
205
294
|
}
|
|
@@ -264,8 +353,17 @@ process.stdin.on('end', () => {
|
|
|
264
353
|
};
|
|
265
354
|
|
|
266
355
|
process.stdout.write(JSON.stringify(output));
|
|
267
|
-
} catch
|
|
268
|
-
//
|
|
356
|
+
} catch {
|
|
357
|
+
// #3504: split posture on internal error. The ONE hard block in this hook
|
|
358
|
+
// (force-add on agent branches) fails CLOSED — if the blocking context can
|
|
359
|
+
// be re-derived from the payload (Bash tool + guard enabled + determinably
|
|
360
|
+
// an agent branch), exit 2 rather than silently allowing. Everything else
|
|
361
|
+
// keeps the historical fail-open posture: a broken advisory guard must
|
|
362
|
+
// never wedge the session's tool calls.
|
|
363
|
+
if (failClosedBlockContext(input)) {
|
|
364
|
+
emitForceAddBlock('fail-closed');
|
|
365
|
+
process.exit(2);
|
|
366
|
+
}
|
|
269
367
|
process.exit(0);
|
|
270
368
|
}
|
|
271
369
|
});
|
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 };
|