@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
|
@@ -22,18 +22,153 @@ const fs = require('fs');
|
|
|
22
22
|
const os = require('os');
|
|
23
23
|
const path = require('path');
|
|
24
24
|
const { spawn } = require('child_process');
|
|
25
|
+
const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
|
|
26
|
+
|
|
27
|
+
// This hook only injects an advisory context-usage warning; it never blocks
|
|
28
|
+
// the tool call it rides in on. A crash here (e.g. a malformed bridge file)
|
|
29
|
+
// must not turn a PostToolUse advisory into a blocked tool call — losing a
|
|
30
|
+
// context warning is far cheaper than stalling the agent's work (#3911).
|
|
31
|
+
const ON_CRASH = HOOK_ON_CRASH.ALLOW;
|
|
25
32
|
|
|
26
33
|
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
|
|
27
34
|
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
|
|
28
35
|
const STALE_SECONDS = 60; // ignore metrics older than 60s
|
|
29
36
|
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
|
|
37
|
+
// How long after a PreCompact readings stay suspect. The watermark records the
|
|
38
|
+
// compaction's START; the compaction keeps running after it, and a statusline
|
|
39
|
+
// render during it stamps the PRE-compaction reading with a CURRENT timestamp
|
|
40
|
+
// (Codex review of #3808, round 3) — so "newer than the watermark" alone still
|
|
41
|
+
// admits it. Everything inside this window is dropped instead. The cost is
|
|
42
|
+
// bounded: a healthy reading dropped here behaves identically to an accepted
|
|
43
|
+
// one (it would exit above-threshold anyway). A genuine exhaustion reading
|
|
44
|
+
// inside the window is SKIPPED, not queued — its warning and its #1974
|
|
45
|
+
// breadcrumb both fire on the next reading after the window, so they are
|
|
46
|
+
// delayed by at most this window plus the accepted skew below when a later
|
|
47
|
+
// reading comes, and lost when
|
|
48
|
+
// none does, i.e. when the session ends inside the window (review of #3808,
|
|
49
|
+
// round 9). That loss is accepted over the alternative, which is trusting a
|
|
50
|
+
// reading that may be the pre-compaction value under a fresh timestamp.
|
|
51
|
+
const COMPACT_GRACE_SECONDS = 60;
|
|
52
|
+
// How far AHEAD of this process's clock a watermark may be and still be
|
|
53
|
+
// honored. PreCompact stamps it from the same clock as the reader, so the
|
|
54
|
+
// legitimate skew is 0; this tolerance only absorbs a clock step. It is a
|
|
55
|
+
// THRESHOLD, so it is named rather than inlined and carries its own boundary
|
|
56
|
+
// trio (Codex review of #3808, round 4). Note it also extends the mute: a
|
|
57
|
+
// watermark this far ahead pushes first recovery from +61 to +66 (measured).
|
|
58
|
+
const WATERMARK_SKEW_SECONDS = 5;
|
|
59
|
+
|
|
60
|
+
// One DEFINITION of what counts as a lifecycle event name, shared by the #3709
|
|
61
|
+
// PreCompact reset and the #2289 output-envelope allowlist. Two call sites, one
|
|
62
|
+
// rule — so the two cannot drift into disagreeing about what "no event name" is.
|
|
63
|
+
// TOTAL, and STRICT about type: only an actual string is an event name. The old
|
|
64
|
+
// inline expression threw on a truthy non-string, and hoisting it ahead of the
|
|
65
|
+
// pipeline would have moved that throw ahead of the side effects #2289
|
|
66
|
+
// documents as always running; a String() coercion is no better — it renders
|
|
67
|
+
// ['PreCompact'] as 'PreCompact' and would run the reset off a malformed
|
|
68
|
+
// payload, and a hostile toString still throws (Codex review of #3808,
|
|
69
|
+
// round 3). typeof does neither: any non-string reads as "no event" — silent,
|
|
70
|
+
// side effects intact — on both call sites.
|
|
71
|
+
function readEventName(data) {
|
|
72
|
+
const name = data && data.hook_event_name;
|
|
73
|
+
if (typeof name === 'string') return name.trim();
|
|
74
|
+
// ABSENT vs MALFORMED are not the same event (Codex review of #3808, round 7,
|
|
75
|
+
// measured base-vs-head). A MISSING name is the documented pre-#2289 Gemini
|
|
76
|
+
// fallback: under GEMINI_API_KEY it means AfterTool and still emits. A name
|
|
77
|
+
// that is PRESENT but not a string is a malformed payload and must not
|
|
78
|
+
// inherit that fallback — at the merge-base it threw on `.trim()` after the
|
|
79
|
+
// side effects, so no envelope was ever produced, and collapsing both onto ''
|
|
80
|
+
// silently turned `42`, `{}` and `['PreCompact']` into emitting AfterTool
|
|
81
|
+
// events. Measured: base silent, head emitted, for both `42` and
|
|
82
|
+
// `['PreCompact']`. null keeps them distinguishable while staying unequal to
|
|
83
|
+
// every event name, so the PreCompact reset and the allowlist below are
|
|
84
|
+
// byte-for-byte unchanged for every well-formed payload.
|
|
85
|
+
return (name === undefined || name === null) ? '' : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// SENTINEL WRITE HARDENING (review of #3808, round 7). `warnPath` lives in
|
|
89
|
+
// os.tmpdir(), which may resolve to a shared sticky directory — not guaranteed
|
|
90
|
+
// per-user, and the file persists across invocations — so an object already
|
|
91
|
+
// sitting there may be a planted symlink. The three routine debounce-accounting writes were bare
|
|
92
|
+
// writeFileSync, which follows one and writes through to its target, while the
|
|
93
|
+
// PreCompact clear and the compaction watermark in this same file already
|
|
94
|
+
// refuse to. Unlink-then-O_EXCL is the watermark's own shape (the watermark
|
|
95
|
+
// write itself now calls this helper — review round 10): the unlink
|
|
96
|
+
// removes any existing object (regular file or link) and O_EXCL then refuses
|
|
97
|
+
// to create through one, so the write can only ever land on a fresh regular
|
|
98
|
+
// file this process made. Best effort by design — a lost sentinel write costs
|
|
99
|
+
// only debounce accounting, which is never worth breaking the hook over, so
|
|
100
|
+
// every failure is swallowed exactly as the watermark write's is.
|
|
101
|
+
// NOT an atomic read-modify-write, and not claimed to be (Codex review of
|
|
102
|
+
// #3808, round 7): two concurrent invocations can read the same state and race
|
|
103
|
+
// through unlink/create, so one invocation's accounting can be lost — the same
|
|
104
|
+
// lost-update race the bare writeFileSync already had, not a class this change
|
|
105
|
+
// introduces. What a lost write leaves behind is whatever the competing writer
|
|
106
|
+
// wrote, which may be a perfectly valid sentinel; it does not reliably mean
|
|
107
|
+
// "defaults on the next call". Advisory debounce bookkeeping is the right place
|
|
108
|
+
// to accept that.
|
|
109
|
+
// The read-side twin of writeSentinel (review of #3808, round 9). Both
|
|
110
|
+
// sentinel files this hook reads — the compaction watermark and the warn
|
|
111
|
+
// state — must be read the same way: lstat first so a planted link, FIFO or
|
|
112
|
+
// directory is refused before any open; O_NOFOLLOW so a link raced in between
|
|
113
|
+
// is refused by the kernel too (0 on Windows, where lstat already carries the
|
|
114
|
+
// check); a 4096-byte bound so a planted large file cannot stall a
|
|
115
|
+
// synchronous read. Rounds 4 and 7 each wrote that sequence inline at their
|
|
116
|
+
// own call site, which left two copies to keep in step by hand. One place
|
|
117
|
+
// now. Refusal THROWS; every caller already wraps the read in a try/catch and
|
|
118
|
+
// degrades to "no file", which is the same behaviour the inline copies had.
|
|
119
|
+
function readSentinel(target) {
|
|
120
|
+
const st = fs.lstatSync(target);
|
|
121
|
+
if (!st.isFile() || st.size > 4096) throw new Error('not a plain sentinel');
|
|
122
|
+
const fd = fs.openSync(target, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
123
|
+
try {
|
|
124
|
+
const buf = Buffer.alloc(st.size);
|
|
125
|
+
// The RETURN VALUE, not just the call (review of #3808, round 11). A file that shrinks
|
|
126
|
+
// between the lstat above and this read — a concurrent legitimate writer truncating
|
|
127
|
+
// mid-write, not the planted-object case the rest of this function guards — leaves the tail
|
|
128
|
+
// of `buf` zero-filled, and those NULs reach JSON.parse as garbage. Every caller already
|
|
129
|
+
// treats a throw here as "no file", so refusing a short read is both safer and the same
|
|
130
|
+
// outcome the caller would reach one line later, stated on purpose rather than by accident.
|
|
131
|
+
const bytesRead = fs.readSync(fd, buf, 0, st.size, 0);
|
|
132
|
+
if (bytesRead !== st.size) throw new Error('sentinel shrank under the read');
|
|
133
|
+
return buf.toString('utf8');
|
|
134
|
+
} finally { fs.closeSync(fd); }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function writeSentinel(target, payload) {
|
|
138
|
+
try {
|
|
139
|
+
try {
|
|
140
|
+
fs.unlinkSync(target);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
if (!e || e.code !== 'ENOENT') throw e;
|
|
143
|
+
}
|
|
144
|
+
const fd = fs.openSync(
|
|
145
|
+
target,
|
|
146
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL
|
|
147
|
+
);
|
|
148
|
+
try {
|
|
149
|
+
// LOOP, and check progress (Codex review of round 11). A single `fs.writeSync` is
|
|
150
|
+
// permitted to write fewer bytes than it was given, and the return value was discarded —
|
|
151
|
+
// a short write left a truncated sentinel that JSON.parse rejects, silently defeating the
|
|
152
|
+
// debounce accounting or the compaction watermark this write exists to record. Node's own
|
|
153
|
+
// `writeFileSync` loops for exactly this reason; the explicit no-progress guard keeps a
|
|
154
|
+
// pathological fd from spinning. Symmetric with the bytesRead check in readSentinel.
|
|
155
|
+
const buf = Buffer.from(payload, 'utf8');
|
|
156
|
+
let written = 0;
|
|
157
|
+
while (written < buf.length) {
|
|
158
|
+
const n = fs.writeSync(fd, buf, written, buf.length - written);
|
|
159
|
+
if (!(n > 0)) throw new Error('sentinel write made no progress');
|
|
160
|
+
written += n;
|
|
161
|
+
}
|
|
162
|
+
} finally { fs.closeSync(fd); }
|
|
163
|
+
} catch (e) { /* best effort — see above */ }
|
|
164
|
+
}
|
|
30
165
|
|
|
31
166
|
let input = '';
|
|
32
167
|
// Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
|
|
33
168
|
// Windows/Git Bash, or slow Claude Code piping during large outputs),
|
|
34
169
|
// exit silently instead of hanging until Claude Code kills the process
|
|
35
170
|
// and reports "hook error". See #775, #1162.
|
|
36
|
-
const stdinTimeout = setTimeout(() =>
|
|
171
|
+
const stdinTimeout = setTimeout(() => allow(undefined), 10000);
|
|
37
172
|
process.stdin.setEncoding('utf8');
|
|
38
173
|
process.stdin.on('data', chunk => input += chunk);
|
|
39
174
|
process.stdin.on('end', () => {
|
|
@@ -43,14 +178,92 @@ process.stdin.on('end', () => {
|
|
|
43
178
|
const sessionId = data.session_id;
|
|
44
179
|
|
|
45
180
|
if (!sessionId) {
|
|
46
|
-
|
|
181
|
+
allow(undefined);
|
|
47
182
|
}
|
|
48
183
|
|
|
49
184
|
// Reject session IDs that contain path traversal sequences or path separators.
|
|
50
185
|
// session_id is used to construct file paths in /tmp — an unsanitized value
|
|
51
186
|
// could escape the temp directory and read or write arbitrary files.
|
|
52
187
|
if (/[/\\]|\.\./.test(sessionId)) {
|
|
53
|
-
|
|
188
|
+
allow(undefined);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const tmpDir = os.tmpdir();
|
|
192
|
+
const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
|
|
193
|
+
const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
|
|
194
|
+
const watermarkPath = path.join(tmpDir, `claude-ctx-${sessionId}-compacted.json`);
|
|
195
|
+
|
|
196
|
+
// #3709: a compaction RESTARTS the context lifecycle, so neither the warn
|
|
197
|
+
// sentinel nor the pre-compaction statusline reading may survive it. Full
|
|
198
|
+
// rationale — what dies when the sentinel outlives a compaction, why the
|
|
199
|
+
// reset sits ahead of the config gate and the metrics read, and why an
|
|
200
|
+
// aborted compaction deliberately stays cleared — lives in ONE place:
|
|
201
|
+
// docs/context-monitor.md, "PreCompact reset". Constraints the code itself
|
|
202
|
+
// must keep are stated at their lines below.
|
|
203
|
+
if (readEventName(data) === 'PreCompact') {
|
|
204
|
+
// ORDERING ASSUMPTION, stated rather than enforced (review of #3808,
|
|
205
|
+
// round 9): this reset and the debounce writeSentinel(warnPath) further
|
|
206
|
+
// down are two writers to the same file, and nothing here serialises
|
|
207
|
+
// them. A debounce invocation that read the pre-compaction state and
|
|
208
|
+
// lands its write AFTER this unlink would resurrect exactly the stale
|
|
209
|
+
// sentinel this block removes. The hook relies on the host dispatching a
|
|
210
|
+
// session's hooks one at a time, which Claude Code does; the other
|
|
211
|
+
// runtimes this hook is installed for are assumed to, and that is not
|
|
212
|
+
// tested. A lock file would close it at the cost of a second file to
|
|
213
|
+
// harden on every platform; not taken here.
|
|
214
|
+
// BOTH files: with the sentinel gone but the bridge still holding the
|
|
215
|
+
// pre-compaction reading (fresh for STALE_SECONDS), the next PostToolUse
|
|
216
|
+
// would fire a spurious CRITICAL off a context the compaction just freed
|
|
217
|
+
// (review of #3709).
|
|
218
|
+
for (const stale of [warnPath, metricsPath]) {
|
|
219
|
+
try {
|
|
220
|
+
fs.unlinkSync(stale);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
if (e && e.code === 'ENOENT') continue; // already absent — that IS the reset
|
|
223
|
+
// Best-effort fallback for a held handle (Windows EPERM/EBUSY):
|
|
224
|
+
// truncate to EMPTY — the one state both readers treat exactly like
|
|
225
|
+
// deletion, because JSON.parse('') throws. A well-formed "neutral"
|
|
226
|
+
// value is NOT equivalent: '{}' debounces the first post-compaction
|
|
227
|
+
// warning, '{"timestamp":0}' is never stale (falsy guard) and emits
|
|
228
|
+
// "undefined%" (review of #3808). Never through a LINK: lstat
|
|
229
|
+
// rejects non-regular files on every platform (Windows has no
|
|
230
|
+
// effective O_NOFOLLOW — libuv defines it as 0 — and TEMP/TMP means
|
|
231
|
+
// its tmpdir is not guaranteed per-user); O_NOFOLLOW additionally
|
|
232
|
+
// closes the lstat→open substitution race where honored. Every
|
|
233
|
+
// refusal lands in this give-up arm — including a Windows runner
|
|
234
|
+
// refusing the write-open of a freshly written file outright —
|
|
235
|
+
// which is why the fallback is best-effort, never asserted-on.
|
|
236
|
+
try {
|
|
237
|
+
if (fs.lstatSync(stale).isFile()) {
|
|
238
|
+
fs.closeSync(fs.openSync(
|
|
239
|
+
stale,
|
|
240
|
+
fs.constants.O_WRONLY | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW || 0)
|
|
241
|
+
));
|
|
242
|
+
}
|
|
243
|
+
} catch (e2) { /* give up, never throw */ }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// COMPACTION WATERMARK (review of #3808, round 3). Deleting the bridge
|
|
248
|
+
// only NARROWS the stale-reading window: the statusline is an
|
|
249
|
+
// uncoordinated process that re-writes the bridge on every render, so a
|
|
250
|
+
// render landing between this clear and the compaction's completion
|
|
251
|
+
// re-creates the PRE-compaction reading with a CURRENT timestamp — and
|
|
252
|
+
// it would sail past STALE_SECONDS as freshly valid. The watermark makes
|
|
253
|
+
// the pre-compaction reading identifiable rather than merely absent: the
|
|
254
|
+
// metrics read drops any reading not strictly newer than it. Written
|
|
255
|
+
// through writeSentinel (review of #3808, round 10 — this block was the
|
|
256
|
+
// shape writeSentinel was lifted from in round 7 and kept its own copy):
|
|
257
|
+
// unlink-then-O_EXCL so an existing file — or a planted symlink — is
|
|
258
|
+
// never followed or overwritten in place; failure to write degrades to
|
|
259
|
+
// the old narrowing, never throws.
|
|
260
|
+
writeSentinel(watermarkPath, JSON.stringify({ at: Math.floor(Date.now() / 1000) }));
|
|
261
|
+
// allow(), not raw process.exit: #3911/ADR-3889 moved this hook onto the
|
|
262
|
+
// declared-policy exit vocabulary while this PR was in review, and the
|
|
263
|
+
// PreCompact branch is new here, so it needs the same conversion.
|
|
264
|
+
// A compaction is never blocked by this hook — ALLOW is the policy the
|
|
265
|
+
// rest of the file already declares.
|
|
266
|
+
allow(undefined);
|
|
54
267
|
}
|
|
55
268
|
|
|
56
269
|
// Check if context warnings are disabled via config.
|
|
@@ -61,31 +274,78 @@ process.stdin.on('end', () => {
|
|
|
61
274
|
const configPath = path.join(cwd, '.planning', 'config.json');
|
|
62
275
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
63
276
|
if (config.hooks?.context_warnings === false) {
|
|
64
|
-
|
|
277
|
+
allow(undefined);
|
|
65
278
|
}
|
|
66
279
|
} catch (e) {
|
|
67
280
|
// Missing or unparseable config → proceed with defaults (context warnings enabled)
|
|
68
281
|
}
|
|
69
282
|
|
|
70
|
-
const tmpDir = os.tmpdir();
|
|
71
|
-
const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
|
|
72
|
-
|
|
73
283
|
// If no metrics file, this is a subagent or fresh session -- exit silently.
|
|
74
284
|
// Collapsed existsSync+readFileSync: ENOENT → exit 0 (identical to old !existsSync branch),
|
|
75
285
|
// other errors rethrow to the outer catch (swallowed → exit 0, as before).
|
|
286
|
+
//
|
|
287
|
+
// Through readSentinel, like the other two (review of #3808, round 11). This read was the
|
|
288
|
+
// asymmetry left in this file: `metricsPath` is built one line away from `warnPath` and
|
|
289
|
+
// `watermarkPath` (same tmpdir, same predictable `claude-ctx-{sessionId}` shape), it is the
|
|
290
|
+
// only one of the three read on EVERY invocation, and it was the only one still reached by a
|
|
291
|
+
// bare readFileSync — so the symlink-to-FIFO stall the other two are hardened against was
|
|
292
|
+
// still reachable here, on the highest-traffic path in the file. The 4096-byte bound is
|
|
293
|
+
// ample: the statusline writes four fixed fields (`gsd-statusline.js`, ~140 bytes with a
|
|
294
|
+
// UUID session id), so no legitimate bridge approaches it. A refusal throws and lands in the
|
|
295
|
+
// rethrow below exactly as an unreadable or malformed bridge already did.
|
|
76
296
|
let metricsRaw;
|
|
77
297
|
try {
|
|
78
|
-
metricsRaw =
|
|
298
|
+
metricsRaw = readSentinel(metricsPath);
|
|
79
299
|
} catch (e) {
|
|
80
|
-
if (e && e.code === 'ENOENT')
|
|
300
|
+
if (e && e.code === 'ENOENT') allow(undefined);
|
|
81
301
|
throw e;
|
|
82
302
|
}
|
|
83
303
|
const metrics = JSON.parse(metricsRaw);
|
|
84
304
|
const now = Math.floor(Date.now() / 1000);
|
|
85
305
|
|
|
306
|
+
// #3709 (round 3): a reading not clearly PAST the compaction is suspect,
|
|
307
|
+
// whatever its timestamp says — the statusline re-writes the bridge on
|
|
308
|
+
// every render, and a render during the compaction stamps the OLD
|
|
309
|
+
// remaining_percentage with a current time. The watermark records the
|
|
310
|
+
// compaction's START, so "newer than the watermark" alone still admits a
|
|
311
|
+
// mid-compaction render (Codex review of #3808, round 3): the grace
|
|
312
|
+
// window covers the compaction's own duration. `!(>)` rather than `<=` so
|
|
313
|
+
// a missing/zero/garbage timestamp is also dropped once a compaction has
|
|
314
|
+
// happened — an unstamped reading cannot prove it is post-compaction.
|
|
315
|
+
//
|
|
316
|
+
// The watermark itself must be SANE to count: one stamped in the future
|
|
317
|
+
// (a clock step backwards, a stray file) would otherwise drop every
|
|
318
|
+
// reading indefinitely and silently self-disable monitoring — so it is
|
|
319
|
+
// honored only when its own timestamp is not ahead of this process's
|
|
320
|
+
// clock (small skew allowed). No watermark, an unreadable one, or an
|
|
321
|
+
// insane one all degrade to the plain STALE_SECONDS behaviour below.
|
|
322
|
+
//
|
|
323
|
+
// READ HARDENING (Codex review of #3808, round 4). The WRITE side already
|
|
324
|
+
// refuses to follow or overwrite a planted object (unlink-then-O_EXCL
|
|
325
|
+
// above), but this read was a bare readFileSync — so on any write-side
|
|
326
|
+
// give-up the planted object survived and every later invocation followed
|
|
327
|
+
// it. In a shared sticky os.tmpdir() that is a mute primitive (a planted
|
|
328
|
+
// recent watermark suppresses monitoring) and a stall primitive (a symlink
|
|
329
|
+
// to a FIFO blocks this synchronous read indefinitely; measured: such a
|
|
330
|
+
// read is still running after 300ms). The same lstat + O_NOFOLLOW pair the
|
|
331
|
+
// sentinel path uses, plus a size bound, applied to the file this PR adds.
|
|
332
|
+
// Every refusal degrades to "no watermark", never throws.
|
|
333
|
+
try {
|
|
334
|
+
const watermark = JSON.parse(readSentinel(watermarkPath));
|
|
335
|
+
if (
|
|
336
|
+
watermark && typeof watermark.at === 'number'
|
|
337
|
+
&& watermark.at <= now + WATERMARK_SKEW_SECONDS
|
|
338
|
+
&& !(metrics.timestamp > watermark.at + COMPACT_GRACE_SECONDS)
|
|
339
|
+
) {
|
|
340
|
+
// Same #3911/ADR-3889 conversion as the PreCompact branch above: this
|
|
341
|
+
// gate is new in this PR, so it did not exist to be migrated.
|
|
342
|
+
allow(undefined);
|
|
343
|
+
}
|
|
344
|
+
} catch (e) { /* no watermark — nothing to compare against */ }
|
|
345
|
+
|
|
86
346
|
// Ignore stale metrics
|
|
87
347
|
if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
|
|
88
|
-
|
|
348
|
+
allow(undefined);
|
|
89
349
|
}
|
|
90
350
|
|
|
91
351
|
const remaining = metrics.remaining_percentage;
|
|
@@ -93,18 +353,35 @@ process.stdin.on('end', () => {
|
|
|
93
353
|
|
|
94
354
|
// No warning needed
|
|
95
355
|
if (remaining > WARNING_THRESHOLD) {
|
|
96
|
-
|
|
356
|
+
allow(undefined);
|
|
97
357
|
}
|
|
98
358
|
|
|
99
|
-
// Debounce: check if we warned recently
|
|
100
|
-
|
|
359
|
+
// Debounce: check if we warned recently. `warnPath` is resolved above, next to
|
|
360
|
+
// metricsPath, because the PreCompact reset needs it before this point.
|
|
101
361
|
let warnData = { callsSinceWarn: 0, lastLevel: null };
|
|
102
362
|
let firstWarn = true;
|
|
103
363
|
|
|
104
364
|
// Collapsed existsSync+readFileSync: ENOENT or parse error → keep default warnData
|
|
105
365
|
// (same as old "file absent" branch). firstWarn tracks whether we read a valid sentinel.
|
|
366
|
+
//
|
|
367
|
+
// READ HARDENING (self-found while addressing round 7; same class, same
|
|
368
|
+
// file). Hardening the writes above leaves this read as a bare
|
|
369
|
+
// readFileSync on warnPath, which is the exact asymmetry round 7 asks be
|
|
370
|
+
// removed from the write side — and the watermark's read was hardened in
|
|
371
|
+
// round 4 for this same reason, so leaving this one recreates it. It was
|
|
372
|
+
// not the LAST bare read in the file: the statusline bridge kept its own
|
|
373
|
+
// until round 11 found it. All three go through readSentinel now. The
|
|
374
|
+
// exposure is real but bounded: the writes now unlink any planted object,
|
|
375
|
+
// so only a read reaching this line BEFORE the first write of an
|
|
376
|
+
// invocation can follow one, and re-planting reopens it every invocation.
|
|
377
|
+
// Following it is a mute primitive — attacker-chosen callsSinceWarn keeps
|
|
378
|
+
// the debounce arm below taken so no warning is ever emitted — and a
|
|
379
|
+
// symlink to a FIFO stalls this synchronous read, the same two primitives
|
|
380
|
+
// measured on the watermark. Same lstat + O_NOFOLLOW + size bound; every
|
|
381
|
+
// refusal degrades to the default warnData this catch already produces,
|
|
382
|
+
// so a normal regular file behaves exactly as before.
|
|
106
383
|
try {
|
|
107
|
-
warnData = JSON.parse(
|
|
384
|
+
warnData = JSON.parse(readSentinel(warnPath));
|
|
108
385
|
firstWarn = false;
|
|
109
386
|
} catch (e) {
|
|
110
387
|
// Missing or corrupted sentinel → firstWarn stays true, warnData stays at defaults
|
|
@@ -120,14 +397,14 @@ process.stdin.on('end', () => {
|
|
|
120
397
|
const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
|
|
121
398
|
if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
|
|
122
399
|
// Update counter and exit without warning
|
|
123
|
-
|
|
124
|
-
|
|
400
|
+
writeSentinel(warnPath, JSON.stringify(warnData));
|
|
401
|
+
allow(undefined);
|
|
125
402
|
}
|
|
126
403
|
|
|
127
404
|
// Reset debounce counter
|
|
128
405
|
warnData.callsSinceWarn = 0;
|
|
129
406
|
warnData.lastLevel = currentLevel;
|
|
130
|
-
|
|
407
|
+
writeSentinel(warnPath, JSON.stringify(warnData));
|
|
131
408
|
|
|
132
409
|
// Detect if GSD is active (has .planning/STATE.md in working directory)
|
|
133
410
|
const isGsdActive = fs.existsSync(path.join(cwd, '.planning', 'STATE.md'));
|
|
@@ -154,7 +431,7 @@ process.stdin.on('end', () => {
|
|
|
154
431
|
).unref();
|
|
155
432
|
warnData.criticalRecorded = true;
|
|
156
433
|
// Persist the sentinel so subsequent debounce cycles don't re-fire
|
|
157
|
-
|
|
434
|
+
writeSentinel(warnPath, JSON.stringify(warnData));
|
|
158
435
|
} catch { /* non-critical — don't let state recording break the hook */ }
|
|
159
436
|
}
|
|
160
437
|
|
|
@@ -191,7 +468,7 @@ process.stdin.on('end', () => {
|
|
|
191
468
|
// not enough — a missing name would still fall through to the injection path.
|
|
192
469
|
// All side effects above (debounce counter, one-time critical-session
|
|
193
470
|
// recording) have already run regardless of whether output is emitted.
|
|
194
|
-
const eventName = (data
|
|
471
|
+
const eventName = readEventName(data);
|
|
195
472
|
// Preserve the pre-#2289 Gemini fallback: a missing event name under a
|
|
196
473
|
// Gemini-dialect runtime (GEMINI_API_KEY set) still means AfterTool, so its
|
|
197
474
|
// advisory output is unchanged. A missing name on any other host is silent.
|
|
@@ -202,13 +479,16 @@ process.stdin.on('end', () => {
|
|
|
202
479
|
const output = {
|
|
203
480
|
hookSpecificOutput: {
|
|
204
481
|
hookEventName: eventName || "AfterTool",
|
|
205
|
-
additionalContext: message
|
|
482
|
+
additionalContext: message,
|
|
483
|
+
severity: currentLevel
|
|
206
484
|
}
|
|
207
485
|
};
|
|
208
486
|
process.stdout.write(JSON.stringify(output));
|
|
209
487
|
}
|
|
210
488
|
} catch (e) {
|
|
211
|
-
// Silent fail -- never block tool execution
|
|
212
|
-
|
|
489
|
+
// Silent fail -- never block tool execution.
|
|
490
|
+
// ON_CRASH is declared ALLOW at module top: this preserves today's
|
|
491
|
+
// exit(0) fail-open behavior exactly (#3911).
|
|
492
|
+
crash(ON_CRASH, undefined);
|
|
213
493
|
}
|
|
214
494
|
});
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
'use strict';
|
|
24
24
|
|
|
25
|
+
const { allow } = require('./lib/hook-exit.js');
|
|
26
|
+
|
|
25
27
|
const WRITE_TOOL_RE = /write|edit|replace|create|delete|remove|append|apply|patch|insert|mkdir/i;
|
|
26
28
|
const PATH_KEY_RE = /^(path|file|file_?path|filepath|target_?path|target|dir|directory|uri|filename)$/i;
|
|
27
29
|
const PLANNING_PATH_RE = /(^|[\\/])\.planning([\\/]|$)/;
|
|
@@ -29,7 +31,7 @@ const PLANNING_PATH_RE = /(^|[\\/])\.planning([\\/]|$)/;
|
|
|
29
31
|
let raw = '';
|
|
30
32
|
const stdinTimeout = setTimeout(() => {
|
|
31
33
|
// Timeout guard: exit silently rather than hanging.
|
|
32
|
-
|
|
34
|
+
allow(undefined);
|
|
33
35
|
}, 10000);
|
|
34
36
|
|
|
35
37
|
process.stdin.setEncoding('utf8');
|
|
@@ -22,13 +22,15 @@
|
|
|
22
22
|
|
|
23
23
|
'use strict';
|
|
24
24
|
|
|
25
|
+
const { allow } = require('./lib/hook-exit.js');
|
|
26
|
+
|
|
25
27
|
const WRITE_TOOL_RE = /write|edit|replace|create|delete|remove|append|apply|patch|insert|mkdir/i;
|
|
26
28
|
const PATH_KEY_RE = /^(path|file|file_?path|filepath|target_?path|target|dir|directory|uri|filename)$/i;
|
|
27
29
|
const PLANNING_PATH_RE = /(^|[\\/])\.planning([\\/]|$)/;
|
|
28
30
|
|
|
29
31
|
let raw = '';
|
|
30
32
|
const stdinTimeout = setTimeout(() => {
|
|
31
|
-
|
|
33
|
+
allow(undefined);
|
|
32
34
|
}, 10000);
|
|
33
35
|
|
|
34
36
|
process.stdin.setEncoding('utf8');
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
'use strict';
|
|
24
24
|
|
|
25
25
|
const fs = require('fs');
|
|
26
|
+
const { allow } = require('./lib/hook-exit.js');
|
|
26
27
|
|
|
27
28
|
const MSG_PRESENT =
|
|
28
29
|
'GSD: .planning/STATE.md is present — review the current phase and any blockers before acting.';
|
|
@@ -37,7 +38,7 @@ const { resolveStatePath } = require('./lib/cursor-workspace.js');
|
|
|
37
38
|
let raw = '';
|
|
38
39
|
const stdinTimeout = setTimeout(() => {
|
|
39
40
|
// Timeout guard: exit silently rather than hanging.
|
|
40
|
-
|
|
41
|
+
allow(undefined);
|
|
41
42
|
}, 10000);
|
|
42
43
|
|
|
43
44
|
process.stdin.setEncoding('utf8');
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
'use strict';
|
|
22
22
|
|
|
23
23
|
const fs = require('fs');
|
|
24
|
+
const { allow } = require('./lib/hook-exit.js');
|
|
24
25
|
|
|
25
26
|
// Workspace resolution is shared across the Cursor hooks (#2587) — see
|
|
26
27
|
// hooks/lib/cursor-workspace.js. Staged next to these scripts by
|
|
@@ -29,7 +30,7 @@ const { resolveStatePath } = require('./lib/cursor-workspace.js');
|
|
|
29
30
|
|
|
30
31
|
let raw = '';
|
|
31
32
|
const stdinTimeout = setTimeout(() => {
|
|
32
|
-
|
|
33
|
+
allow(undefined);
|
|
33
34
|
}, 10000);
|
|
34
35
|
|
|
35
36
|
process.stdin.setEncoding('utf8');
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
const fs = require('fs');
|
|
57
57
|
const path = require('path');
|
|
58
58
|
const os = require('os');
|
|
59
|
+
const { allow } = require('./lib/hook-exit.js');
|
|
59
60
|
|
|
60
61
|
// Workspace resolution is shared across the Cursor hooks (#2587) — see
|
|
61
62
|
// hooks/lib/cursor-workspace.js. Staged next to these scripts by
|
|
@@ -342,35 +343,39 @@ function resolveIsolationDecision(data, { clock = Date, realpath = fs.realpathSy
|
|
|
342
343
|
return { action: 'allow' };
|
|
343
344
|
}
|
|
344
345
|
|
|
345
|
-
// ─── #
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
// this hook
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
346
|
+
// ─── #3897 rung 2: per-install runtime marker, single canonical owner ────────
|
|
347
|
+
// bin/install.js writes `<install>/gsd-core/.gsd-runtime` beside VERSION for
|
|
348
|
+
// every runtime install (#2297); this hook ships at `<install>/hooks/`, so the
|
|
349
|
+
// marker is the `gsd-core` sibling of this file's own directory. Previously
|
|
350
|
+
// this hook held its own private reader/cache (one of four #3897 found); it
|
|
351
|
+
// now delegates to the single canonical owner, `src/runtime-slash.cts`
|
|
352
|
+
// (compiled to gsd-core/bin/lib/runtime-slash.cjs), reached through
|
|
353
|
+
// `ensureRuntimeBuild()` like the other compiled-lib requires in this file
|
|
354
|
+
// (`scripts/lint-hooks-runtime-build-seam.cjs`).
|
|
355
355
|
function readInstallRuntimeMarker() {
|
|
356
|
-
if (_installMarkerCache !== undefined) return _installMarkerCache;
|
|
357
356
|
try {
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
357
|
+
ensureRuntimeBuild();
|
|
358
|
+
const runtimeSlash = require('../gsd-core/bin/lib/runtime-slash.cjs');
|
|
359
|
+
return runtimeSlash.readInstallRuntimeMarker();
|
|
361
360
|
} catch {
|
|
362
|
-
//
|
|
363
|
-
// from this rung", never a resolution failure.
|
|
364
|
-
|
|
361
|
+
// Unbuilt runtime library, or any other failure reaching the canonical
|
|
362
|
+
// owner — "no signal from this rung", never a resolution failure.
|
|
363
|
+
return null;
|
|
365
364
|
}
|
|
366
|
-
return _installMarkerCache;
|
|
367
365
|
}
|
|
368
366
|
|
|
369
|
-
// Test seam —
|
|
370
|
-
//
|
|
371
|
-
// are unaffected.
|
|
367
|
+
// Test seam — forwards to the canonical owner's seam so this hook and
|
|
368
|
+
// runtime-slash.cjs always share one cache (#3897 rung 2). Spawned-hook tests
|
|
369
|
+
// (fresh process, no marker) are unaffected.
|
|
372
370
|
function _setInstallRuntimeMarkerForTests(value) {
|
|
373
|
-
|
|
371
|
+
try {
|
|
372
|
+
ensureRuntimeBuild();
|
|
373
|
+
const runtimeSlash = require('../gsd-core/bin/lib/runtime-slash.cjs');
|
|
374
|
+
runtimeSlash._setInstallRuntimeMarkerForTests(value);
|
|
375
|
+
} catch {
|
|
376
|
+
// Test-only seam; an unbuilt library here means the test itself will fail
|
|
377
|
+
// downstream, which is a louder and more actionable signal than throwing here.
|
|
378
|
+
}
|
|
374
379
|
}
|
|
375
380
|
|
|
376
381
|
/**
|
|
@@ -564,7 +569,7 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
|
|
|
564
569
|
function main() {
|
|
565
570
|
let raw = '';
|
|
566
571
|
const stdinTimeout = setTimeout(() => {
|
|
567
|
-
|
|
572
|
+
allow(undefined);
|
|
568
573
|
}, 10000);
|
|
569
574
|
|
|
570
575
|
process.stdin.setEncoding('utf8');
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
const fs = require('fs');
|
|
35
35
|
const path = require('path');
|
|
36
36
|
const os = require('os');
|
|
37
|
+
const { allow } = require('./lib/hook-exit.js');
|
|
37
38
|
|
|
38
39
|
// Immutable, bundled subdirectories that the canonical path must expose. These
|
|
39
40
|
// are the directories `@~/.claude/gsd-core/<subdir>/...` includes point into.
|
|
@@ -301,5 +302,5 @@ if (require.main === module) {
|
|
|
301
302
|
} catch (_) {
|
|
302
303
|
// Best-effort: a canonical-path failure must never abort a session.
|
|
303
304
|
}
|
|
304
|
-
|
|
305
|
+
allow(undefined);
|
|
305
306
|
}
|