@opengsd/gsd-core 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +12 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +78 -42
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +0 -1
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +3 -1
- package/agents/gsd-plan-checker.md +91 -112
- package/agents/gsd-planner.md +20 -4
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +82 -7
- package/agents/gsd-ui-researcher.md +70 -3
- package/agents/gsd-verifier.md +24 -2
- package/bin/install.js +847 -200
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/quick.md +8 -4
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +761 -100
- package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
- package/gsd-core/bin/lib/api-coverage.cjs +30 -9
- package/gsd-core/bin/lib/artifacts.cjs +2 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
- package/gsd-core/bin/lib/audit.cjs +163 -41
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +785 -144
- package/gsd-core/bin/lib/capability-state.cjs +25 -4
- package/gsd-core/bin/lib/capability-validator.cjs +321 -18
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +229 -6
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +877 -54
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +121 -29
- package/gsd-core/bin/lib/config.cjs +92 -2
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +118 -14
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +23 -2
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +975 -326
- package/gsd-core/bin/lib/gap-checker.cjs +41 -8
- package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
- package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
- package/gsd-core/bin/lib/host-integration.cjs +96 -11
- package/gsd-core/bin/lib/init-command-router.cjs +132 -21
- package/gsd-core/bin/lib/init.cjs +252 -56
- package/gsd-core/bin/lib/install-engine.cjs +252 -15
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
- package/gsd-core/bin/lib/install-profiles.cjs +100 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +195 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +175 -4
- package/gsd-core/bin/lib/milestone.cjs +112 -7
- package/gsd-core/bin/lib/model-catalog.cjs +177 -19
- package/gsd-core/bin/lib/model-resolver.cjs +10 -28
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +321 -13
- package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
- package/gsd-core/bin/lib/phase-locator.cjs +138 -17
- package/gsd-core/bin/lib/phase.cjs +1175 -115
- package/gsd-core/bin/lib/plan-document.cjs +273 -0
- package/gsd-core/bin/lib/plan-scan.cjs +13 -2
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +165 -34
- package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
- package/gsd-core/bin/lib/probe-core.cjs +4 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
- package/gsd-core/bin/lib/roadmap.cjs +248 -64
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -31
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +47 -18
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +216 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
- package/gsd-core/bin/lib/state-transition.cjs +850 -145
- package/gsd-core/bin/lib/state.cjs +1629 -287
- package/gsd-core/bin/lib/surface.cjs +33 -10
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +2542 -387
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
- package/gsd-core/bin/lib/unusable-input.cjs +13 -0
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/vendor/README.md +43 -5
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -13
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +441 -56
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +6 -5
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +37 -19
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/edge-probe.md +17 -5
- package/gsd-core/references/execute-mvp-tdd.md +18 -18
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +3 -3
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +22 -0
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/mvp-concepts.md +2 -2
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +76 -3
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +39 -9
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +15 -12
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +2 -2
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +11 -11
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/phase-prompt.md +7 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +3 -1
- package/gsd-core/workflows/add-phase.md +5 -3
- package/gsd-core/workflows/add-tests.md +4 -9
- package/gsd-core/workflows/add-todo.md +2 -2
- package/gsd-core/workflows/ai-integration-phase.md +5 -10
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +14 -3
- package/gsd-core/workflows/audit-milestone.md +11 -9
- package/gsd-core/workflows/audit-uat.md +19 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +12 -26
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +3 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
- package/gsd-core/workflows/code-review-fix.md +3 -1
- package/gsd-core/workflows/code-review.md +192 -69
- package/gsd-core/workflows/complete-milestone.md +28 -14
- package/gsd-core/workflows/debug.md +6 -4
- package/gsd-core/workflows/diagnose-issues.md +17 -7
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +46 -19
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +3 -1
- package/gsd-core/workflows/eval-review.md +5 -10
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +29 -5
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +4 -2
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +68 -66
- package/gsd-core/workflows/execute-plan.md +25 -20
- package/gsd-core/workflows/explore.md +3 -1
- package/gsd-core/workflows/extract-learnings.md +3 -1
- package/gsd-core/workflows/fast.md +8 -2
- package/gsd-core/workflows/forensics.md +3 -1
- package/gsd-core/workflows/graduation.md +6 -6
- package/gsd-core/workflows/health.md +4 -7
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +17 -14
- package/gsd-core/workflows/inbox.md +5 -6
- package/gsd-core/workflows/ingest-docs.md +45 -12
- package/gsd-core/workflows/insert-phase.md +7 -5
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +7 -3
- package/gsd-core/workflows/list-workspaces.md +3 -1
- package/gsd-core/workflows/manager.md +15 -26
- package/gsd-core/workflows/map-codebase.md +3 -1
- package/gsd-core/workflows/milestone-summary.md +3 -1
- package/gsd-core/workflows/mvp-phase.md +3 -3
- package/gsd-core/workflows/new-milestone.md +10 -22
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +17 -29
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +4 -2
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +20 -5
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
- package/gsd-core/workflows/plan-phase.md +251 -54
- package/gsd-core/workflows/plan-review-convergence.md +148 -19
- package/gsd-core/workflows/plant-seed.md +3 -3
- package/gsd-core/workflows/pr-branch.md +195 -51
- package/gsd-core/workflows/profile-user.md +17 -15
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +52 -15
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +33 -32
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +6 -4
- package/gsd-core/workflows/remove-workspace.md +3 -3
- package/gsd-core/workflows/resume-project.md +14 -14
- package/gsd-core/workflows/review.md +404 -21
- package/gsd-core/workflows/scan.md +3 -1
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +9 -9
- package/gsd-core/workflows/settings-integrations.md +66 -32
- package/gsd-core/workflows/settings.md +4 -6
- package/gsd-core/workflows/ship.md +22 -16
- package/gsd-core/workflows/sketch-wrap-up.md +13 -17
- package/gsd-core/workflows/sketch.md +13 -19
- package/gsd-core/workflows/smart-entry.md +4 -6
- package/gsd-core/workflows/spec-phase.md +31 -4
- package/gsd-core/workflows/spike-wrap-up.md +9 -11
- package/gsd-core/workflows/spike.md +21 -32
- package/gsd-core/workflows/stats.md +4 -2
- package/gsd-core/workflows/sync-skills.md +13 -5
- package/gsd-core/workflows/thread.md +13 -7
- package/gsd-core/workflows/transition.md +7 -5
- package/gsd-core/workflows/ui-phase.md +36 -21
- package/gsd-core/workflows/ui-review.md +7 -11
- package/gsd-core/workflows/ultraplan-phase.md +7 -13
- package/gsd-core/workflows/undo.md +9 -17
- package/gsd-core/workflows/update.md +47 -48
- package/gsd-core/workflows/validate-phase.md +3 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +106 -21
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +302 -22
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +77 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +46 -12
- package/hooks/dist/gsd-read-guard.js +18 -7
- package/hooks/dist/gsd-read-injection-scanner.js +22 -13
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +222 -29
- package/hooks/dist/gsd-validate-commit.sh +523 -12
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +36 -17
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +4 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +302 -22
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +3 -1
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +28 -23
- package/hooks/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +77 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +46 -12
- package/hooks/gsd-read-guard.js +18 -7
- package/hooks/gsd-read-injection-scanner.js +22 -13
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +222 -29
- package/hooks/gsd-validate-commit.sh +523 -12
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +36 -17
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/hooks.json +6 -0
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +4 -0
- package/package.json +14 -9
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +12 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +52 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +406 -0
- package/scripts/gen-capability-registry.cjs +8 -6
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-loop-host-contract.cjs +189 -4
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +32 -2
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +45 -14
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-response-language-coverage.cjs +524 -0
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +124 -4
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +51 -64
- package/scripts/lint-vendored-deps.cjs +208 -35
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +83 -14
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +22 -3
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -109
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/state-write-path-drift-baseline.json +0 -19
|
@@ -180,4 +180,213 @@ function isGitSubcommand(cmd, sub) {
|
|
|
180
180
|
return tokens[subIdx] === sub;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Resolve a `-m` message argument to its SUBJECT — the first line of the commit
|
|
185
|
+
* message — resolving the command-substituted heredoc form to the heredoc
|
|
186
|
+
* BODY's first line.
|
|
187
|
+
*
|
|
188
|
+
* PURE STRING FUNCTION. It deliberately does NOT tokenize or walk the command
|
|
189
|
+
* line: selecting *which* argument is the message stays with the caller, exactly
|
|
190
|
+
* as before, so this cannot change which commands are validated. An earlier
|
|
191
|
+
* revision of this fix did walk tokens and regressed four separate cases —
|
|
192
|
+
* `git commit -- -m WIP` (a pathspec), `git commit --amend && echo -m WIP` (a
|
|
193
|
+
* later command's flag), `-m "" --allow-empty-message` (the scanner drops empty
|
|
194
|
+
* tokens, so the following flag became the subject), and unquoted
|
|
195
|
+
* `git commit -m WIP`. All were allowed upstream and would have started being
|
|
196
|
+
* blocked. Reported in review of #3802.
|
|
197
|
+
*
|
|
198
|
+
* The defect this DOES fix: `gsd-validate-commit.sh` captured the message with
|
|
199
|
+
* `-m[[:space:]]+"([^"]+)"`, and bash `[^"]` matches newlines, so the widely
|
|
200
|
+
* used agent-authored commit idiom
|
|
201
|
+
*
|
|
202
|
+
* git commit -m "$(cat <<'EOF'
|
|
203
|
+
* feat(auth): add login flow
|
|
204
|
+
* EOF
|
|
205
|
+
* )"
|
|
206
|
+
*
|
|
207
|
+
* captured the whole span up to the final quote at `)"`. Taking its first line
|
|
208
|
+
* yielded the literal `$(cat <<'EOF'`, which can never satisfy Conventional
|
|
209
|
+
* Commits, so every heredoc-form commit was blocked regardless of its message.
|
|
210
|
+
*
|
|
211
|
+
* Recognition is anchored at BOTH ends and requires a command substitution, so
|
|
212
|
+
* an ordinary message merely CONTAINING — or ending in — `<<WORD` is not
|
|
213
|
+
* mistaken for an opener. Without the `^\$\(` anchor,
|
|
214
|
+
* `-m "WIP notes <<EOF\nfix: smuggled subject"` resolved to the second line and
|
|
215
|
+
* ALLOWED a non-conforming commit: an enforcement bypass, not just a
|
|
216
|
+
* misclassification (review of #3802).
|
|
217
|
+
*
|
|
218
|
+
* NOT resolved, by design: an UNQUOTED delimiter (`<<EOF`). bash expands `$var`
|
|
219
|
+
* and `$(...)` in that body, so the literal text here is not what git receives.
|
|
220
|
+
* An earlier revision resolved it anyway and called the gap "the same
|
|
221
|
+
* pre-existing limit as expansions in a plain `-m` argument" — that framing was
|
|
222
|
+
* wrong on both halves: on base the whole heredoc form was blocked, so this fix
|
|
223
|
+
* CREATED the path, and it dodged the length gate as well as the format gate.
|
|
224
|
+
* See the expansion guard in the body (review of #3816, round 4).
|
|
225
|
+
*
|
|
226
|
+
* KNOWN LIMIT: the DOUBLE-QUOTED delimiter spelling (`<<"EOF"`) is resolvable
|
|
227
|
+
* here but unreachable through the caller — `gsd-validate-commit.sh`'s
|
|
228
|
+
* double-quoted `-m` capture stops at the first `"`, which in that spelling is
|
|
229
|
+
* the delimiter's own quote, so the resolver only ever sees a truncated opener
|
|
230
|
+
* and the commit stays blocked. Its single-quoted `-m` capture DOES deliver the
|
|
231
|
+
* spelling intact, which is why an earlier revision's claim of unreachability
|
|
232
|
+
* was false; the caller now gates the resolver on the double-quoted arm alone
|
|
233
|
+
* (review of #3816, round 4), so the claim holds again — for that reason, not
|
|
234
|
+
* by luck. That is the pre-fix behaviour for the whole form (fail closed, a
|
|
235
|
+
* false positive on one rare spelling), and widening the bash capture to span
|
|
236
|
+
* inner quotes would change what is captured for EVERY message containing one —
|
|
237
|
+
* a regression class this fix deliberately does not touch (Codex review of
|
|
238
|
+
* #3816). The same capture truncation blocks a `"` in the SUBJECT LINE itself,
|
|
239
|
+
* where it lands inside the line being measured. It does NOT block a `"` on a
|
|
240
|
+
* later body line: the subject is already complete before the truncation point,
|
|
241
|
+
* so that message resolves and is allowed (measured; an earlier revision of this
|
|
242
|
+
* comment and of the changeset claimed a `"` ANYWHERE blocked, which is false —
|
|
243
|
+
* Codex review of #3816, round 4). The truncation guard below is what keeps the
|
|
244
|
+
* unmeasurable half fail-closed. Two more legal-but-unrecognized spellings
|
|
245
|
+
* stay blocked the same fail-closed way: an env-prefixed cat
|
|
246
|
+
* (`$(A=1 cat <<'EOF'`) and an option-terminated cat (`$(cat -- <<'EOF'`) —
|
|
247
|
+
* recognizing either would mean modelling bash prefix words here, cost with
|
|
248
|
+
* no reported user (review of #3816, round 3).
|
|
249
|
+
*
|
|
250
|
+
* @param {string} messageArg - the raw `-m` argument, already selected by the caller
|
|
251
|
+
* @returns {string} the subject to validate
|
|
252
|
+
*/
|
|
253
|
+
function resolveCommitSubject(messageArg) {
|
|
254
|
+
// CRLF-tolerant split. With a bare split('\n') every body line kept its \r,
|
|
255
|
+
// so `body.indexOf(delimiter)` never matched on CRLF input: the truncation
|
|
256
|
+
// guard was inert, an empty CRLF message resolved to 'EOF\r' instead of '',
|
|
257
|
+
// and a real 72-char subject measured 73 (review of #3816). Splitting on
|
|
258
|
+
// /\r?\n/ is the repo-wide remedy for this recurring defect class.
|
|
259
|
+
const lines = String(messageArg == null ? '' : messageArg).split(/\r?\n/);
|
|
260
|
+
// The path prefix is a PATH-CHARACTER class, not \S*: `\S*` accepted
|
|
261
|
+
// `id;/bin/cat`, so `$(id;/bin/cat <<'EOF' ...` was resolved to its heredoc
|
|
262
|
+
// body while bash actually runs `id` first and git's real subject is `id`'s
|
|
263
|
+
// OUTPUT — an enforcement bypass (Codex review of #3816). A prefix carrying
|
|
264
|
+
// any shell metacharacter now fails recognition, which falls back to the
|
|
265
|
+
// opener line and the format gate: fail closed, exactly the pre-fix
|
|
266
|
+
// behaviour for the whole form.
|
|
267
|
+
//
|
|
268
|
+
// Whitespace inside the recognition is ASCII space/tab — [ \t], never \s —
|
|
269
|
+
// because JavaScript \s includes Unicode whitespace bash does NOT split on:
|
|
270
|
+
// `$(<NBSP>/bin/cat <<'EOF'` was recognized here while bash reads
|
|
271
|
+
// `<NBSP>/bin/cat` as the executable NAME, so recognition claimed a
|
|
272
|
+
// substitution that does not run cat (Codex review of #3816, round 2). The
|
|
273
|
+
// same ASCII rule as the blank-line skip below, for the same reason.
|
|
274
|
+
// `cat[ \t]*<<`, not `+`: bash accepts `cat<<'EOF'` with no space (review of
|
|
275
|
+
// #3816, round 3), and recognizing it costs nothing — the token before `<<`
|
|
276
|
+
// is still literally `cat`.
|
|
277
|
+
//
|
|
278
|
+
// The path prefix must be ABSOLUTE (Codex review of #3816, round 4). The old
|
|
279
|
+
// `[\w./-]*\/` also accepted `./cat` and `../evil/cat`, so a relative
|
|
280
|
+
// executable that merely ENDS in `cat` was trusted to echo its stdin: with a
|
|
281
|
+
// planted `../evil/cat` printing `WIP injected`, the resolver validated the
|
|
282
|
+
// heredoc body while git's real subject was `WIP injected` (measured
|
|
283
|
+
// base=2 -> head=0 against a real commit). Requiring `/` up front costs
|
|
284
|
+
// nothing real — `/bin/cat` and a bare `cat` both still resolve.
|
|
285
|
+
//
|
|
286
|
+
// AN ABSOLUTE PATH IS NOT AN IDENTITY EITHER (independent review of #3816,
|
|
287
|
+
// round 8). Round 4 stopped at "must be absolute", so any absolute path
|
|
288
|
+
// ENDING in `/cat` was still trusted to echo its stdin — the very thing the
|
|
289
|
+
// round-4 reasoning rejected one spelling earlier. With an executable at
|
|
290
|
+
// `/some/scratch/dir/cat` printing `WIP injected`, the resolver validated
|
|
291
|
+
// the conforming heredoc body while git's real subject was `WIP injected`
|
|
292
|
+
// (measured on bash 3.2.57 and 5.3.15 against a real commit: hook exit 0,
|
|
293
|
+
// `git cat-file -p` subject `WIP injected`, while the same command through
|
|
294
|
+
// `./cat` was already refused). The prefix is now the canonical system
|
|
295
|
+
// locations, which is the only claim a string can support. `/usr/local/bin`
|
|
296
|
+
// is deliberately excluded: it is user-writable on ordinary machines, which
|
|
297
|
+
// is the plantable case this guard exists for.
|
|
298
|
+
//
|
|
299
|
+
// RESIDUAL, not fixable from a string: a bare `cat` shadowed earlier on PATH
|
|
300
|
+
// has the same effect and is indistinguishable here. It is also not a
|
|
301
|
+
// meaningful boundary — anyone who can plant an executable on PATH can run
|
|
302
|
+
// `git commit` directly — so this hook stays an authoring guard, not a
|
|
303
|
+
// security control.
|
|
304
|
+
//
|
|
305
|
+
// The delimiter alternatives are split so the BARE spelling is its own group:
|
|
306
|
+
// `\\(...)` (backslash-quoted) and `(...)` (bare) were one `\\?(...)` branch,
|
|
307
|
+
// which conflated the only two spellings that differ in bash. See the
|
|
308
|
+
// expansion guard below.
|
|
309
|
+
const opener = /^\$\([ \t]*(?:\/(?:usr\/)?bin\/)?cat[ \t]*<<(-?)[ \t]*(?:'([^']+)'|"([^"]+)"|\\([^\s'"();|&<>\\]+)|([^\s'"();|&<>\\]+))[ \t]*$/
|
|
310
|
+
.exec(lines[0]);
|
|
311
|
+
if (!opener) return lines[0];
|
|
312
|
+
|
|
313
|
+
// EXPANSION GUARD (review of #3816, round 4 — BLOCKER). Only `<<'D'`, `<<"D"`
|
|
314
|
+
// and `<<\D` suppress expansion. A BARE `<<D` is expanded by bash, so the body
|
|
315
|
+
// captured here is NOT the text git receives, and measuring it is an
|
|
316
|
+
// enforcement bypass in both gates at once:
|
|
317
|
+
//
|
|
318
|
+
// -m "$(cat <<EOF\nfeat: $UNSET_VAR\nEOF\n)" git gets `feat:` -> format gate dodged
|
|
319
|
+
// -m "$(cat <<EOF\nfeat: ${LONG}\nEOF\n)" git gets any length -> length gate dodged
|
|
320
|
+
//
|
|
321
|
+
// Both measured base=2 -> head=0 against the real hook. This is NOT the
|
|
322
|
+
// pre-existing plain-`-m` expansion limit an earlier revision claimed it was:
|
|
323
|
+
// on base the whole heredoc form was blocked, so no expansion inside a body
|
|
324
|
+
// ever reached an allow — this fix created the path, and closes it here.
|
|
325
|
+
// Same rule every other guard in this function follows: when the real subject
|
|
326
|
+
// cannot be known, fall back to the opener line, which fails the format gate.
|
|
327
|
+
if (opener[5]) return lines[0];
|
|
328
|
+
|
|
329
|
+
// `<<-` strips leading TABS from every body line, including the terminator.
|
|
330
|
+
const stripTabs = opener[1] === '-';
|
|
331
|
+
const delimiter = opener[2] || opener[3] || opener[4];
|
|
332
|
+
const body = lines.slice(1).map((l) => (stripTabs ? l.replace(/^\t+/, '') : l));
|
|
333
|
+
|
|
334
|
+
// TRUNCATION GUARD. The capture that produced this argument stops at the first
|
|
335
|
+
// `"`, so a message containing one arrives here missing its tail — and its
|
|
336
|
+
// terminator. Resolving anyway would hand the length gate a PREFIX of the real
|
|
337
|
+
// subject and let an over-long message through, an enforcement hole that did
|
|
338
|
+
// not exist before this fix (review of #3802). A body with no terminator is
|
|
339
|
+
// therefore not resolved at all: returning the opener line fails the format
|
|
340
|
+
// gate, which is exactly what this whole form did before the fix. The fix
|
|
341
|
+
// applies where the capture is complete and changes nothing where it is not.
|
|
342
|
+
const end = body.indexOf(delimiter);
|
|
343
|
+
|
|
344
|
+
// POST-TERMINATOR GUARD (review of #3816, round 3 — BLOCKER). Everything
|
|
345
|
+
// after the terminator is still part of the real message once bash
|
|
346
|
+
// substitutes: `-m "$(cat <<'EOF'\nfeat: ok\nEOF\n) <200 a's>"` expands to a
|
|
347
|
+
// single 200+ char subject, but discarding the tail measured 8 and DODGED
|
|
348
|
+
// COMMIT_SUBJECT_TOO_LONG — the same prefix-measurement class the truncation
|
|
349
|
+
// guard below exists for, missed on the other side of the terminator. The
|
|
350
|
+
// canonical idiom's tail is exactly one closing-paren line; anything else
|
|
351
|
+
// means the substitution is composed with more text, so fall back to the
|
|
352
|
+
// opener line — blocked, the pre-fix behaviour for the whole form.
|
|
353
|
+
if (end !== -1) {
|
|
354
|
+
const tail = body.slice(end + 1);
|
|
355
|
+
if (tail.length !== 1 || !/^[ \t]*\)[ \t]*$/.test(tail[0])) return lines[0];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// git's default `cleanup=whitespace` strips leading blank lines, so the subject
|
|
359
|
+
// is the first NON-EMPTY body line, not blindly the first one. Taking lines[1]
|
|
360
|
+
// returned '' for a body that starts blank and falsely blocked a conforming
|
|
361
|
+
// commit — the very defect class #3802 reports (review of #3802).
|
|
362
|
+
const scan = end === -1 ? body : body.slice(0, end);
|
|
363
|
+
// "Blank" is git's ASCII definition — space and tab — never JavaScript's
|
|
364
|
+
// trim(), whose Unicode whitespace class skips lines git KEEPS: a body whose
|
|
365
|
+
// first line is a NBSP resolved to the SECOND line while git's real subject
|
|
366
|
+
// is the NBSP line — an enforcement bypass (Codex review of #3816, verified
|
|
367
|
+
// against `git stripspace`, which preserves the c2a0 bytes). A Unicode-blank
|
|
368
|
+
// first line is now returned as the subject and fails the format gate: the
|
|
369
|
+
// same fail-closed direction git itself takes.
|
|
370
|
+
const idx = scan.findIndex((l) => !/^[ \t]*$/.test(l));
|
|
371
|
+
if (idx === -1) return end === -1 ? lines[0] : '';
|
|
372
|
+
|
|
373
|
+
// Truncation is only fatal to the line it lands IN. A captured line is complete
|
|
374
|
+
// exactly when another line follows it, because the capture kept its newline.
|
|
375
|
+
// So an unterminated body whose subject line is followed by more text is still
|
|
376
|
+
// measurable; only a subject line that runs to the end of a truncated capture
|
|
377
|
+
// is not, and that one falls back to the opener — which fails the format gate,
|
|
378
|
+
// exactly as this whole form did before the fix. Without this, the length gate
|
|
379
|
+
// measured a PREFIX of the real subject and let an over-long message through
|
|
380
|
+
// (review of #3802).
|
|
381
|
+
if (end === -1 && idx >= body.length - 1) return lines[0];
|
|
382
|
+
|
|
383
|
+
// git's `cleanup=whitespace` strips whitespace at BOTH ends of the line, not
|
|
384
|
+
// just leading blanks. Measuring the raw line rejected `feat: <66 x's>` plus
|
|
385
|
+
// three trailing spaces as 75 chars when git's actual subject is a conforming
|
|
386
|
+
// 72 — a still-blocked conforming commit, the very defect #3802 reports
|
|
387
|
+
// (review of #3816). Trailing only, here: leading blank-LINE handling is the
|
|
388
|
+
// findIndex above, and `<<-` leading-tab stripping already happened.
|
|
389
|
+
return scan[idx].replace(/[ \t]+$/, '');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
module.exports = { isGitSubcommand, tokenize, extractBranchArgument, skipToSubcommand, resolveCommitSubject };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// hooks/lib/git-probe.js — classify a bounded spawnSync(git, ...) result,
|
|
3
|
+
// distinguishing a GENUINE negative answer (git ran and said "no") from
|
|
4
|
+
// "could not determine" (timeout / spawn failure / signal kill) — #3911.
|
|
5
|
+
//
|
|
6
|
+
// Proven defect: hooks/gsd-worktree-path-guard.js, hooks/gsd-workflow-guard.js,
|
|
7
|
+
// and hooks/gsd-windsurf-pre-write.js each treat spawnSync(git, ..., {timeout})
|
|
8
|
+
// returning a non-zero/empty result as "not applicable" and allow silently.
|
|
9
|
+
// A macOS CI run showed three deny cases land at 2084ms/2112ms/2177ms — just
|
|
10
|
+
// past the 2000ms budget — each returning exit 0 with EMPTY stdout AND EMPTY
|
|
11
|
+
// stderr: a timeout is INDISTINGUISHABLE, at the call site, from git cleanly
|
|
12
|
+
// answering "not a repo" / "not this worktree" unless the spawnSync() result
|
|
13
|
+
// itself is inspected for `error`/`signal`, not just `status`/`stdout`.
|
|
14
|
+
//
|
|
15
|
+
// This module does not change any hook's decision or exit code — it only
|
|
16
|
+
// answers "was this probe's answer real?" so a caller can emit a diagnostic
|
|
17
|
+
// on the undetermined branch while keeping the existing fail-open behavior.
|
|
18
|
+
//
|
|
19
|
+
// Classification mirrors tests/helpers/process-seam.cjs's OUTCOME vocabulary
|
|
20
|
+
// (EXITED vs TIMED_OUT/SPAWN_FAILED/KILLED) — same evidence-over-classification
|
|
21
|
+
// ordering (a populated `status` outranks an attached `error`, since Node can
|
|
22
|
+
// report `error.code === 'ETIMEDOUT'` on a result that also carried a real
|
|
23
|
+
// exit that beat the timer) — so a probe here and a subprocess outcome in the
|
|
24
|
+
// test suite never disagree on the same raw spawnSync() shape.
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {{error?: (Error & {code?: string})|null, status: number|null, signal: string|null}} result
|
|
28
|
+
* the raw return value of `spawnSync('git', ...)`.
|
|
29
|
+
* @returns {{determined: true} | {determined: false, reason: string}}
|
|
30
|
+
*/
|
|
31
|
+
function classifyGitProbe(result) {
|
|
32
|
+
const error = result && result.error;
|
|
33
|
+
const status = result ? result.status : null;
|
|
34
|
+
const signal = result ? result.signal : null;
|
|
35
|
+
|
|
36
|
+
if (!error && signal === null) {
|
|
37
|
+
// A real exit — status 0 or non-zero is a genuine, trustworthy answer.
|
|
38
|
+
return { determined: true };
|
|
39
|
+
}
|
|
40
|
+
if (status !== null) {
|
|
41
|
+
// `status` is only ever populated by a real exit; it outranks an attached
|
|
42
|
+
// `error` even at the exact timeout boundary (process-seam.cjs's own
|
|
43
|
+
// "evidence over classification" rule).
|
|
44
|
+
return { determined: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const code = error ? (error.code ?? null) : null;
|
|
48
|
+
let reason;
|
|
49
|
+
if (code === 'ETIMEDOUT' || signal !== null) {
|
|
50
|
+
reason = signal ? `timed out (signal ${signal})` : 'timed out';
|
|
51
|
+
} else if (code) {
|
|
52
|
+
reason = `spawn failed (${code})`;
|
|
53
|
+
} else {
|
|
54
|
+
reason = 'could not run (unknown reason)';
|
|
55
|
+
}
|
|
56
|
+
return { determined: false, reason };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* If `result` could not be determined (timeout / spawn failure / signal
|
|
61
|
+
* kill), write a stderr diagnostic naming the probe and why — never changes
|
|
62
|
+
* any exit code, never throws. A no-op when the probe genuinely ran and
|
|
63
|
+
* answered (status 0 or non-zero with no error/signal).
|
|
64
|
+
*
|
|
65
|
+
* @param {string} hookName - e.g. 'gsd-worktree-path-guard'
|
|
66
|
+
* @param {string} probeLabel - e.g. "git rev-parse --show-toplevel"
|
|
67
|
+
* @param {*} result - the raw spawnSync() return value
|
|
68
|
+
*/
|
|
69
|
+
function reportIfUndetermined(hookName, probeLabel, result) {
|
|
70
|
+
const classification = classifyGitProbe(result);
|
|
71
|
+
if (classification.determined) return;
|
|
72
|
+
try {
|
|
73
|
+
process.stderr.write(
|
|
74
|
+
`${hookName}: git probe '${probeLabel}' ${classification.reason} — ` +
|
|
75
|
+
`allowing this call because the probe's answer is unknown, not because ` +
|
|
76
|
+
`git answered "no" (#3911).\n`
|
|
77
|
+
);
|
|
78
|
+
} catch {
|
|
79
|
+
// A stderr write failing (EPIPE, closed fd) must never itself change the
|
|
80
|
+
// hook's fail-open outcome.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { classifyGitProbe, reportIfUndetermined };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// hooks/lib/hook-exit.js — hand-written, NOT generated. A tiny hook-facing
|
|
3
|
+
// vocabulary layered over hooks/lib/cli-exit.js's terminateNow (ADR-3889 §3),
|
|
4
|
+
// for the 19 enforcement hooks P7/#3911 is migrating off raw process.exit().
|
|
5
|
+
//
|
|
6
|
+
// WHY onCrash is a REQUIRED argument, with no default (this is the entire
|
|
7
|
+
// point of this module): every hook audited for #3911 ended its outer
|
|
8
|
+
// try/catch in ONE of two ways — some `process.exit(0)` (fail open: a hook
|
|
9
|
+
// error must never block a legitimate tool call), others `process.exit(2)`
|
|
10
|
+
// (fail closed: a hook whose whole job is a security/safety denial must not
|
|
11
|
+
// let its own bug wave the denial through). Both are legitimate policies for
|
|
12
|
+
// DIFFERENT hooks — but neither is the "obviously correct" one, and a shared
|
|
13
|
+
// helper that silently defaulted to either would let a future hook inherit
|
|
14
|
+
// the wrong policy by omission, which is exactly the "nothing fails with
|
|
15
|
+
// success" defect ADR-3889 exists to close. Requiring the caller to name its
|
|
16
|
+
// policy at every call site — not just once at module load — makes "I forgot
|
|
17
|
+
// to decide" a load-time/call-time crash instead of a silent behavior. A
|
|
18
|
+
// caller that truly wants "allow on crash" must still write ALLOW; there is
|
|
19
|
+
// no path that reaches terminateNow without a declared choice.
|
|
20
|
+
//
|
|
21
|
+
// crash() itself is total: it does not throw on a bad `onCrash`, because
|
|
22
|
+
// throwing would unwind into the CALLER's own outer catch — the exact
|
|
23
|
+
// fail-open-by-accident hazard this module removes. Instead an unrecognized
|
|
24
|
+
// policy terminates via terminateNow('INTERNAL', ...) with a diagnostic
|
|
25
|
+
// payload naming the offending value, so a typo'd/misspelled policy is
|
|
26
|
+
// debuggable on stderr rather than silently reinterpreted as ALLOW or DENY.
|
|
27
|
+
const { terminateNow } = require('./cli-exit.js');
|
|
28
|
+
|
|
29
|
+
/** Frozen enum of the two declarable crash policies. No third option. */
|
|
30
|
+
const HOOK_ON_CRASH = Object.freeze({
|
|
31
|
+
ALLOW: 'allow',
|
|
32
|
+
DENY: 'deny',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** Terminate with the hook-protocol PASS outcome (exit 0). */
|
|
36
|
+
function allow(payload) {
|
|
37
|
+
terminateNow('PASS', payload);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Terminate with the hook-protocol deny outcome (exit 2, stdout+stderr).
|
|
42
|
+
*
|
|
43
|
+
* @param {*} payload - JSON-serializable value written to fd 1 (and, when
|
|
44
|
+
* `stderrPayload` is omitted, fd 2 too).
|
|
45
|
+
* @param {*} [stderrPayload] - optional distinct value for fd 2, forwarded
|
|
46
|
+
* verbatim to terminateNow's own `stderrPayload` param — a string is
|
|
47
|
+
* written raw, anything else is JSON-stringified.
|
|
48
|
+
*/
|
|
49
|
+
function deny(payload, stderrPayload) {
|
|
50
|
+
terminateNow('HOOK_DENY', payload, stderrPayload);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Dispatch to allow()/deny() per a DECLARED crash policy. `onCrash` is
|
|
55
|
+
* required — see the module header for why there is no default.
|
|
56
|
+
*
|
|
57
|
+
* @param {'allow'|'deny'} onCrash - one of HOOK_ON_CRASH's two values.
|
|
58
|
+
* @param {*} payload - JSON-serializable value forwarded to terminateNow.
|
|
59
|
+
*/
|
|
60
|
+
function crash(onCrash, payload) {
|
|
61
|
+
if (onCrash === HOOK_ON_CRASH.ALLOW) {
|
|
62
|
+
allow(payload);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (onCrash === HOOK_ON_CRASH.DENY) {
|
|
66
|
+
deny(payload);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Never guess a policy and never throw (a throw would unwind into the
|
|
70
|
+
// caller's own outer catch — the fail-open-by-accident defect this module
|
|
71
|
+
// exists to remove). Terminate deterministically with INTERNAL, naming the
|
|
72
|
+
// offending value so a misspelled/omitted policy is diagnosable.
|
|
73
|
+
terminateNow('INTERNAL', {
|
|
74
|
+
error: 'hook-exit: crash() called with an unrecognized onCrash policy',
|
|
75
|
+
receivedOnCrash: onCrash,
|
|
76
|
+
expectedOneOf: [HOOK_ON_CRASH.ALLOW, HOOK_ON_CRASH.DENY],
|
|
77
|
+
originalPayload: payload,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { HOOK_ON_CRASH, allow, deny, crash };
|
|
@@ -26,11 +26,29 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
const INJECTION_PATTERNS = Object.freeze([
|
|
29
|
-
|
|
30
|
-
/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// #4016: ONE filler-tolerant imperative-override pattern. It REPLACES the
|
|
30
|
+
// five narrow verb patterns (ignore x2 / disregard / forget / override) that
|
|
31
|
+
// used to sit here: they tolerated no filler between verb and noun, so a
|
|
32
|
+
// planted "forget all of your ..." phrasing (measured in the wild) matched
|
|
33
|
+
// nothing. A single pattern rather than narrow-plus-combined because both
|
|
34
|
+
// consumers count one finding PER PATTERN toward the severity threshold
|
|
35
|
+
// (3 = HIGH, blockable under security.injection_blocking): overlapping
|
|
36
|
+
// patterns made one sentence count twice and pushed a two-phrasing LOW
|
|
37
|
+
// payload to HIGH (PR #4061 review).
|
|
38
|
+
// verbs ignore|disregard|forget|discard|override. `override` carries
|
|
39
|
+
// the old `override system|previous prompt|instructions` family;
|
|
40
|
+
// `discard` is the synonym seen in the same planted phrasings.
|
|
41
|
+
// fillers at least ONE of all|of|the|your|my|<qualifier> must sit between
|
|
42
|
+
// verb and noun (a lookahead, no repetition). Bare "override
|
|
43
|
+
// rules" / "ignore instructions" are ordinary repo prose
|
|
44
|
+
// (measured: 6 hits across docs and source) and stay unmatched.
|
|
45
|
+
// tails `disregard (all) previous` with no noun, and bare `forget` +
|
|
46
|
+
// `instructions`, are kept verbatim because the old narrow
|
|
47
|
+
// patterns accepted them; the superset is pinned by
|
|
48
|
+
// tests/injection-patterns-parity.security.test.cjs.
|
|
49
|
+
// known FP determined linter-doc prose such as "ignore (the) rules" trips
|
|
50
|
+
// a single-pattern LOW advisory; one pattern can never block.
|
|
51
|
+
/(?:ignore|disregard|forget|discard|override)\s+(?=(?:all|of|the|your|my|system|previous|prior|above|earlier)\s)(?:all\s+)?(?:of\s+)?(?:the\s+|your\s+|my\s+)?(?:(?:system|previous|prior|above|earlier)\s+)?(?:instructions|directives|prompts?|rules)|disregard\s+(?:all\s+)?previous|forget\s+instructions/i,
|
|
34
52
|
/you\s+are\s+now\s+(?:a|an|the)\s+/i,
|
|
35
53
|
/act\s+as\s+(?:a|an|the)\s+(?!plan|phase|wave)/i,
|
|
36
54
|
/pretend\s+(?:you(?:'re| are)\s+|to\s+be\s+)/i,
|
|
@@ -42,4 +60,16 @@ const INJECTION_PATTERNS = Object.freeze([
|
|
|
42
60
|
/<<\s*SYS\s*>>/i,
|
|
43
61
|
]);
|
|
44
62
|
|
|
45
|
-
|
|
63
|
+
// Short single-line label for a matched pattern, used for both the rendered
|
|
64
|
+
// advisory prose and the typed `findings[].match` field in BOTH consumer
|
|
65
|
+
// hooks. The raw regex source is not user-facing: the #4016 superset pattern
|
|
66
|
+
// is ~280 characters, and gsd-prompt-guard.js echoed it verbatim into its
|
|
67
|
+
// advisory (PR #4061 review nit). Byte-identical to the transform
|
|
68
|
+
// gsd-read-injection-scanner.js carried inline since #3523, hoisted here so
|
|
69
|
+
// one finding renders the same way in both hooks. Both hooks already require
|
|
70
|
+
// this module, so this adds no new staging dependency.
|
|
71
|
+
function describePattern(pattern) {
|
|
72
|
+
return pattern.source.replace(/\\s\+/g, '-').replace(/[()\\]/g, '').substring(0, 50);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { INJECTION_PATTERNS, describePattern };
|
|
@@ -29,10 +29,14 @@ const MANAGED_HOOKS = [
|
|
|
29
29
|
'gsd-cursor-subagent-stop.js',
|
|
30
30
|
'gsd-ensure-canonical-path.js',
|
|
31
31
|
'gsd-graphify-update.sh',
|
|
32
|
+
// #3662: portable node resolver (helper staged in hooks/; managed JS hook
|
|
33
|
+
// commands route through it under --portable-hooks).
|
|
34
|
+
'gsd-node-runner.sh',
|
|
32
35
|
'gsd-phase-boundary.sh',
|
|
33
36
|
'gsd-prompt-guard.js',
|
|
34
37
|
'gsd-read-guard.js',
|
|
35
38
|
'gsd-read-injection-scanner.js',
|
|
39
|
+
'gsd-secret-read-guard.js',
|
|
36
40
|
'gsd-session-state.sh',
|
|
37
41
|
'gsd-statusline.js',
|
|
38
42
|
'gsd-update-banner.js',
|
|
@@ -65,6 +65,23 @@ const path = require('path');
|
|
|
65
65
|
const os = require('os');
|
|
66
66
|
const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch } = require('./lib/isolation-sentinel.js');
|
|
67
67
|
const { REASON_CODE } = require('./lib/isolation-deny-reason.js');
|
|
68
|
+
const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
|
|
69
|
+
|
|
70
|
+
// Required at module top, alongside the other ./lib requires — NOT behind
|
|
71
|
+
// ensureRuntimeBuild() below. Terminating on a parse/timeout failure must
|
|
72
|
+
// never depend on the gitignored build artifacts this hook self-heals for
|
|
73
|
+
// its own registry lookups (#3911).
|
|
74
|
+
//
|
|
75
|
+
// This guard's outer catch (main(), below) has always exited 0 (fail open):
|
|
76
|
+
// that outer catch only covers payload PARSING failing before applicability
|
|
77
|
+
// could even be determined (malformed stdin JSON, etc.) — the guard's real
|
|
78
|
+
// fail-closed logic (a GSD project whose dispatch-isolation configuration
|
|
79
|
+
// cannot be verified) is handled separately, inside evaluateDispatch/
|
|
80
|
+
// resolveIsolationState, and already returns a 'block' decision through the
|
|
81
|
+
// normal exit-2 path rather than through this catch. So an unparseable
|
|
82
|
+
// payload has nothing to enforce; allowing it preserves today's behavior
|
|
83
|
+
// exactly.
|
|
84
|
+
const ON_CRASH = HOOK_ON_CRASH.ALLOW;
|
|
68
85
|
// #3582: gsd-core/bin/lib/*.cjs (runtime-name-policy.cjs, capability-registry.cjs
|
|
69
86
|
// below) are tsc build artifacts (ADR-457), gitignored and absent on a raw
|
|
70
87
|
// plugin-marketplace / git-clone install that never ran `npm run build:lib`.
|
|
@@ -95,40 +112,40 @@ function parseHarnessFlag(flag) {
|
|
|
95
112
|
return { param: m[1], value: m[2] };
|
|
96
113
|
}
|
|
97
114
|
|
|
98
|
-
// ─── #
|
|
115
|
+
// ─── #3897 rung 2: per-install runtime marker, single canonical owner ────────
|
|
99
116
|
// bin/install.js writes `<install>/gsd-core/.gsd-runtime` for EVERY runtime
|
|
100
117
|
// install (#2297), co-located with VERSION. Unlike `~/.gsd/defaults.json` —
|
|
101
118
|
// which is host-wide and names whichever runtime's install ran LAST, the exact
|
|
102
119
|
// leakage #2840's config.cjs change exists to prevent — the marker describes
|
|
103
120
|
// THIS install, which is the property runtime identity needs on a machine
|
|
104
|
-
// with 2+ runtimes.
|
|
105
|
-
// (
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// consolidating every marker reader into one shared seam.
|
|
110
|
-
let _installMarkerCache; // undefined = unread; null = known absent; string = value
|
|
111
|
-
|
|
121
|
+
// with 2+ runtimes. Previously this hook held its own private reader/cache
|
|
122
|
+
// (one of four #3897 found); it now delegates to the single canonical owner,
|
|
123
|
+
// `src/runtime-slash.cts` (compiled to gsd-core/bin/lib/runtime-slash.cjs),
|
|
124
|
+
// reached through `ensureRuntimeBuild()` like every other compiled-lib require
|
|
125
|
+
// in this file (`scripts/lint-hooks-runtime-build-seam.cjs`).
|
|
112
126
|
function readInstallRuntimeMarker() {
|
|
113
|
-
if (_installMarkerCache !== undefined) return _installMarkerCache;
|
|
114
127
|
try {
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
128
|
+
ensureRuntimeBuild();
|
|
129
|
+
const runtimeSlash = require('../gsd-core/bin/lib/runtime-slash.cjs');
|
|
130
|
+
return runtimeSlash.readInstallRuntimeMarker();
|
|
118
131
|
} catch {
|
|
119
|
-
//
|
|
120
|
-
// from this rung", never a resolution failure.
|
|
121
|
-
|
|
122
|
-
_installMarkerCache = null;
|
|
132
|
+
// Unbuilt runtime library, or any other failure reaching the canonical
|
|
133
|
+
// owner — "no signal from this rung" (N4), never a resolution failure.
|
|
134
|
+
return null;
|
|
123
135
|
}
|
|
124
|
-
return _installMarkerCache;
|
|
125
136
|
}
|
|
126
137
|
|
|
127
|
-
// Test seam for the marker rung
|
|
128
|
-
//
|
|
129
|
-
// model-resolver.cts's _setInstallRuntimeMarkerForTests, #2297).
|
|
138
|
+
// Test seam for the marker rung — forwards to the canonical owner's seam so
|
|
139
|
+
// this hook and runtime-slash.cjs always share one cache (#3897 rung 2).
|
|
130
140
|
function _setInstallRuntimeMarkerForTests(value) {
|
|
131
|
-
|
|
141
|
+
try {
|
|
142
|
+
ensureRuntimeBuild();
|
|
143
|
+
const runtimeSlash = require('../gsd-core/bin/lib/runtime-slash.cjs');
|
|
144
|
+
runtimeSlash._setInstallRuntimeMarkerForTests(value);
|
|
145
|
+
} catch {
|
|
146
|
+
// Test-only seam; an unbuilt library here means the test itself will fail
|
|
147
|
+
// downstream, which is a louder and more actionable signal than throwing here.
|
|
148
|
+
}
|
|
132
149
|
}
|
|
133
150
|
|
|
134
151
|
/**
|
|
@@ -282,18 +299,40 @@ function resolveRegistryIsolation(cwd, configPath) {
|
|
|
282
299
|
|
|
283
300
|
if (isolation === 'harness-worktree') {
|
|
284
301
|
let useWorktrees = true;
|
|
302
|
+
// #3972: the opt-out read shares the ONE owner every other
|
|
303
|
+
// isolation-deciding surface uses — planning-workspace's
|
|
304
|
+
// worktreesOptedOut ladder (scoped own-key wins; root inherited under
|
|
305
|
+
// the GSD_WORKSTREAM gate; strict === false). A flat single-file read
|
|
306
|
+
// here made a workstream-LOCAL opt-out invisible, so the sentinel-absent
|
|
307
|
+
// fallback denied a sequential dispatch the config explicitly allowed.
|
|
308
|
+
// Reached through ensureRuntimeBuild() like every other compiled-lib
|
|
309
|
+
// require in this file (scripts/lint-hooks-runtime-build-seam.cjs); if
|
|
310
|
+
// the library is unreachable the flat-root read below remains as the
|
|
311
|
+
// degraded fallback — today's behavior, never worse.
|
|
312
|
+
let ladderAnswered = false;
|
|
285
313
|
try {
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
useWorktrees = false;
|
|
291
|
-
}
|
|
314
|
+
ensureRuntimeBuild();
|
|
315
|
+
const { worktreesOptedOut } = require('../gsd-core/bin/lib/planning-workspace.cjs');
|
|
316
|
+
useWorktrees = !worktreesOptedOut(cwd);
|
|
317
|
+
ladderAnswered = true;
|
|
292
318
|
} catch {
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
319
|
+
// Unbuilt runtime library or a ladder failure — fall through to the
|
|
320
|
+
// legacy flat-root read (conservative: keep enforcing).
|
|
321
|
+
}
|
|
322
|
+
if (!ladderAnswered) {
|
|
323
|
+
try {
|
|
324
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
325
|
+
const parsedCfg = JSON.parse(raw);
|
|
326
|
+
if (parsedCfg && typeof parsedCfg === 'object' && parsedCfg.workflow &&
|
|
327
|
+
typeof parsedCfg.workflow === 'object' && parsedCfg.workflow.use_worktrees === false) {
|
|
328
|
+
useWorktrees = false;
|
|
329
|
+
}
|
|
330
|
+
} catch {
|
|
331
|
+
// Unreadable config already propagated to the outer caller's catch
|
|
332
|
+
// before this point in practice (resolveRuntimeIdentity reads it
|
|
333
|
+
// first); tolerate defensively and keep the conservative (enforce)
|
|
334
|
+
// default rather than silently disabling the guard.
|
|
335
|
+
}
|
|
297
336
|
}
|
|
298
337
|
if (!useWorktrees) isolation = 'none';
|
|
299
338
|
}
|
|
@@ -476,7 +515,7 @@ function evaluateDispatch(data, { clock = Date } = {}) {
|
|
|
476
515
|
/* istanbul ignore next -- stdin adapter, exercised via spawnSync in tests */
|
|
477
516
|
function main() {
|
|
478
517
|
let input = '';
|
|
479
|
-
const stdinTimeout = setTimeout(() =>
|
|
518
|
+
const stdinTimeout = setTimeout(() => allow(undefined), 3000);
|
|
480
519
|
process.stdin.setEncoding('utf8');
|
|
481
520
|
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
482
521
|
process.stdin.on('end', () => {
|
|
@@ -486,18 +525,18 @@ function main() {
|
|
|
486
525
|
const decision = evaluateDispatch(data);
|
|
487
526
|
if (decision.action === 'block') {
|
|
488
527
|
const out = { decision: 'block', reason: decision.reason, reason_code: decision.reasonCode };
|
|
489
|
-
process.stdout.write(JSON.stringify(out));
|
|
490
528
|
// Kimi feeds stderr (not stdout) back to the model on exit 2.
|
|
491
|
-
|
|
492
|
-
process.exit(2);
|
|
529
|
+
deny(out, decision.reason);
|
|
493
530
|
}
|
|
494
|
-
|
|
531
|
+
allow(undefined);
|
|
495
532
|
} catch {
|
|
496
533
|
// Silent fail — never block valid tool calls due to hook errors
|
|
497
534
|
// (malformed payload, etc.). This is distinct from resolveIsolationState's
|
|
498
535
|
// internal error handling, which DOES deny — this outer catch only
|
|
499
536
|
// covers payload parsing before applicability could even be determined.
|
|
500
|
-
|
|
537
|
+
// ON_CRASH is declared ALLOW at module top: this preserves today's
|
|
538
|
+
// exit(0) fail-open behavior exactly (#3911).
|
|
539
|
+
crash(ON_CRASH, undefined);
|
|
501
540
|
}
|
|
502
541
|
});
|
|
503
542
|
}
|
|
@@ -156,5 +156,22 @@ const result = {
|
|
|
156
156
|
};
|
|
157
157
|
|
|
158
158
|
if (cacheFile) {
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
// #4091: the cache file is shared per-PACKAGE across every runtime's worker
|
|
160
|
+
// (#607/#1421), so concurrent statusline/banner readers parse it while this
|
|
161
|
+
// worker writes it. A direct writeFileSync truncates before writing — a
|
|
162
|
+
// reader landing mid-write sees a torn/empty record (its JSON.parse catch
|
|
163
|
+
// swallows it, so the symptom is an intermittently blank update segment).
|
|
164
|
+
// Publish atomically instead: stage under a unique same-directory temp
|
|
165
|
+
// (same filesystem, so rename(2) is atomic — readers see the old or the new
|
|
166
|
+
// record, never a partial one), then renameSync into place. Failure policy
|
|
167
|
+
// is unchanged (#3582 degrade): any error is swallowed and the temp, if
|
|
168
|
+
// left behind, is best-effort removed.
|
|
169
|
+
const tmp = cacheFile + '.tmp-' + process.pid;
|
|
170
|
+
try {
|
|
171
|
+
fs.writeFileSync(tmp, JSON.stringify(result));
|
|
172
|
+
fs.renameSync(tmp, cacheFile);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
try {
|
|
175
|
+
fs.rmSync(tmp, { force: true });
|
|
176
|
+
} catch (e2) {}
|
|
177
|
+
}}
|