@opengsd/gsd-core 1.12.0 → 1.14.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-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +458 -0
- package/agents/gsd-code-fixer.md +5 -5
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +345 -0
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-executor.md +63 -35
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-plan-checker.md +76 -57
- package/agents/gsd-planner.md +14 -0
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-checker.md +19 -3
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-ui-researcher.md +29 -0
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/agents/gsd-verifier.md +23 -1
- package/bin/install.js +444 -134
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +1 -0
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +2 -1
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/surface.md +18 -8
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +532 -174
- package/gsd-core/bin/lib/adr-parser.cjs +1 -1
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +39 -22
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +528 -116
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-state.cjs +7 -1
- package/gsd-core/bin/lib/capability-validator.cjs +134 -5
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +198 -38
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/commands.cjs +981 -79
- package/gsd-core/bin/lib/config-loader.cjs +4 -0
- package/gsd-core/bin/lib/config.cjs +153 -38
- package/gsd-core/bin/lib/core-utils.cjs +34 -7
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +343 -28
- package/gsd-core/bin/lib/edge-probe.cjs +14 -1
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +137 -23
- package/gsd-core/bin/lib/gap-checker.cjs +22 -13
- package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- 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 +54 -11
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +87 -23
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/host-integration.cjs +57 -5
- package/gsd-core/bin/lib/init-command-router.cjs +14 -0
- package/gsd-core/bin/lib/init.cjs +539 -60
- package/gsd-core/bin/lib/install-engine.cjs +199 -14
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
- package/gsd-core/bin/lib/install-profiles.cjs +36 -14
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
- package/gsd-core/bin/lib/io.cjs +35 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +64 -39
- package/gsd-core/bin/lib/markdown-table.cjs +123 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +41 -10
- package/gsd-core/bin/lib/model-resolver.cjs +101 -10
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -7
- package/gsd-core/bin/lib/phase-id.cjs +412 -31
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase.cjs +941 -98
- package/gsd-core/bin/lib/plan-document.cjs +10 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
- package/gsd-core/bin/lib/planning-snapshot.cjs +206 -30
- package/gsd-core/bin/lib/planning-workspace.cjs +153 -29
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- 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/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
- package/gsd-core/bin/lib/review-lane-invocation.cjs +96 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +555 -41
- package/gsd-core/bin/lib/roadmap.cjs +292 -69
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +260 -43
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +28 -20
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +294 -108
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -47
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
- package/gsd-core/bin/lib/smart-entry.cjs +7 -9
- package/gsd-core/bin/lib/state-document.cjs +159 -32
- package/gsd-core/bin/lib/state-md-schema.cjs +44 -27
- package/gsd-core/bin/lib/state-transition.cjs +465 -62
- package/gsd-core/bin/lib/state.cjs +906 -151
- package/gsd-core/bin/lib/surface.cjs +83 -10
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/uat.cjs +1420 -516
- package/gsd-core/bin/lib/update-context.cjs +36 -26
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +316 -23
- package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +531 -36
- package/gsd-core/bin/lib/workstream-inventory.cjs +21 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +21 -7
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +507 -81
- package/gsd-core/references/agent-contracts.md +3 -3
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/edge-probe.md +17 -13
- package/gsd-core/references/execute-mvp-tdd.md +18 -16
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +12 -3
- package/gsd-core/references/mvp-concepts.md +2 -2
- 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-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +75 -2
- package/gsd-core/references/planning-config.md +5 -1
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/tdd.md +17 -9
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/references/worktree-path-safety.md +112 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/phase-prompt.md +4 -0
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/add-backlog.md +2 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +4 -3
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +2 -0
- package/gsd-core/workflows/audit-uat.md +2 -0
- package/gsd-core/workflows/autonomous.md +15 -10
- package/gsd-core/workflows/check-todos.md +5 -3
- package/gsd-core/workflows/cleanup.md +4 -2
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +22 -13
- package/gsd-core/workflows/code-review-fix.md +5 -3
- package/gsd-core/workflows/code-review.md +211 -43
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +40 -254
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
- 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 +2 -0
- 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 +2 -0
- 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.md +1 -1
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +43 -13
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +15 -156
- package/gsd-core/workflows/edit-phase.md +2 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +20 -3
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +24 -3
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +78 -159
- package/gsd-core/workflows/execute-plan.md +28 -15
- package/gsd-core/workflows/explore.md +2 -0
- package/gsd-core/workflows/extract-learnings.md +2 -0
- package/gsd-core/workflows/fast.md +6 -0
- package/gsd-core/workflows/forensics.md +2 -0
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- 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.compact.md +398 -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 +3 -1
- package/gsd-core/workflows/import.md +3 -3
- package/gsd-core/workflows/inbox.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +2 -0
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +2 -0
- package/gsd-core/workflows/list-workspaces.md +2 -0
- package/gsd-core/workflows/manager.md +3 -3
- package/gsd-core/workflows/map-codebase.md +52 -3
- package/gsd-core/workflows/milestone-summary.md +2 -0
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +55 -13
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project.md +37 -205
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -0
- 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 +19 -4
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
- package/gsd-core/workflows/plan-phase.md +144 -185
- package/gsd-core/workflows/plan-review-convergence.md +102 -10
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/pr-branch.md +30 -10
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +25 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
- package/gsd-core/workflows/quick/steps/research-phase.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 +21 -4
- package/gsd-core/workflows/reapply-patches.md +79 -3
- package/gsd-core/workflows/remove-phase.md +2 -0
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +6 -2
- package/gsd-core/workflows/review.md +215 -10
- package/gsd-core/workflows/scan.md +2 -0
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/settings-integrations.md +9 -8
- package/gsd-core/workflows/settings.md +19 -6
- package/gsd-core/workflows/ship.md +10 -10
- package/gsd-core/workflows/sketch-wrap-up.md +2 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/spec-phase.md +24 -19
- package/gsd-core/workflows/spike-wrap-up.md +2 -0
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +2 -0
- package/gsd-core/workflows/sync-skills.md +12 -4
- package/gsd-core/workflows/thread.md +2 -0
- package/gsd-core/workflows/transition.md +2 -0
- package/gsd-core/workflows/ui-phase.md +26 -5
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +2 -0
- package/gsd-core/workflows/undo.md +1 -1
- package/gsd-core/workflows/update.md +48 -43
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work.md +68 -182
- package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-context-monitor.js +371 -27
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-node-runner.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +30 -5
- package/hooks/dist/gsd-read-guard.js +2 -0
- package/hooks/dist/gsd-read-injection-scanner.js +5 -5
- package/hooks/dist/gsd-secret-read-guard.js +1105 -0
- package/hooks/dist/gsd-statusline.js +18 -10
- package/hooks/dist/gsd-validate-commit.sh +474 -7
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-agent-isolation-guard.js +42 -16
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-context-monitor.js +371 -27
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-node-runner.sh +1 -0
- package/hooks/gsd-prompt-guard.js +30 -5
- package/hooks/gsd-read-guard.js +2 -0
- package/hooks/gsd-read-injection-scanner.js +5 -5
- package/hooks/gsd-secret-read-guard.js +1105 -0
- package/hooks/gsd-statusline.js +18 -10
- package/hooks/gsd-validate-commit.sh +474 -7
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/hooks.json +6 -0
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +13 -9
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/build-hooks.js +11 -4
- package/scripts/check-contract-drift.cjs +4 -1
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +133 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +17 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-loop-host-contract.cjs +67 -15
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lib/suite-detection.cjs +32 -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-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +24 -2
- package/scripts/lint-phase-enumeration-drift.cjs +24 -6
- package/scripts/lint-phase-id-drift.cjs +465 -15
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-response-language-coverage.cjs +530 -0
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-test-file-count.allowlist.json +4 -1
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +1112 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +22 -0
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +1 -0
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +2 -1
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
// Thresholds:
|
|
15
15
|
// WARNING (remaining <= 35%): Agent should wrap up current task
|
|
16
16
|
// CRITICAL (remaining <= 25%): Agent should stop immediately and save state
|
|
17
|
+
// Both fire-points are overridable per project via .planning/config.json
|
|
18
|
+
// (hooks.context_warning_threshold / hooks.context_critical_threshold, #4285);
|
|
19
|
+
// the values above are the defaults used when the keys are absent or unusable.
|
|
17
20
|
//
|
|
18
21
|
// Debounce: 5 tool uses between warnings to avoid spam
|
|
19
22
|
// Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
|
|
@@ -30,20 +33,180 @@ const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
|
|
|
30
33
|
// context warning is far cheaper than stalling the agent's work (#3911).
|
|
31
34
|
const ON_CRASH = HOOK_ON_CRASH.ALLOW;
|
|
32
35
|
|
|
33
|
-
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
|
|
34
|
-
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
|
|
36
|
+
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35% (default, see resolveThresholds)
|
|
37
|
+
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25% (default, see resolveThresholds)
|
|
35
38
|
const STALE_SECONDS = 60; // ignore metrics older than 60s
|
|
36
39
|
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
|
|
40
|
+
// How long after a PreCompact readings stay suspect. The watermark records the
|
|
41
|
+
// compaction's START; the compaction keeps running after it, and a statusline
|
|
42
|
+
// render during it stamps the PRE-compaction reading with a CURRENT timestamp
|
|
43
|
+
// (Codex review of #3808, round 3) — so "newer than the watermark" alone still
|
|
44
|
+
// admits it. Everything inside this window is dropped instead. The cost is
|
|
45
|
+
// bounded: a healthy reading dropped here behaves identically to an accepted
|
|
46
|
+
// one (it would exit above-threshold anyway). A genuine exhaustion reading
|
|
47
|
+
// inside the window is SKIPPED, not queued — its warning and its #1974
|
|
48
|
+
// breadcrumb both fire on the next reading after the window, so they are
|
|
49
|
+
// delayed by at most this window plus the accepted skew below when a later
|
|
50
|
+
// reading comes, and lost when
|
|
51
|
+
// none does, i.e. when the session ends inside the window (review of #3808,
|
|
52
|
+
// round 9). That loss is accepted over the alternative, which is trusting a
|
|
53
|
+
// reading that may be the pre-compaction value under a fresh timestamp.
|
|
54
|
+
const COMPACT_GRACE_SECONDS = 60;
|
|
55
|
+
// How far AHEAD of this process's clock a watermark may be and still be
|
|
56
|
+
// honored. PreCompact stamps it from the same clock as the reader, so the
|
|
57
|
+
// legitimate skew is 0; this tolerance only absorbs a clock step. It is a
|
|
58
|
+
// THRESHOLD, so it is named rather than inlined and carries its own boundary
|
|
59
|
+
// trio (Codex review of #3808, round 4). Note it also extends the mute: a
|
|
60
|
+
// watermark this far ahead pushes first recovery from +61 to +66 (measured).
|
|
61
|
+
const WATERMARK_SKEW_SECONDS = 5;
|
|
62
|
+
|
|
63
|
+
// Resolve the two fire-points from the project's `.planning/config.json`
|
|
64
|
+
// (#4285). The constants above are the DEFAULTS; a project overrides either one
|
|
65
|
+
// through `hooks.context_warning_threshold` / `hooks.context_critical_threshold`,
|
|
66
|
+
// which is what keeps a tuned fire-point alive across updates — this file is in
|
|
67
|
+
// the MANAGED registry, so an edit to the constants is re-staged away by the
|
|
68
|
+
// next install.
|
|
69
|
+
//
|
|
70
|
+
// TOTAL and never-throwing: this hook must not block the tool call it rides in
|
|
71
|
+
// on, so every unusable input degrades to the default instead of raising.
|
|
72
|
+
// Unusable is decided by Number.isFinite, which is type-strict (the string
|
|
73
|
+
// "30" and true are both rejected, unlike the global isFinite), plus the 0-100
|
|
74
|
+
// domain of the remaining_percentage these are compared against.
|
|
75
|
+
//
|
|
76
|
+
// The PAIR is validated too, and falls back TOGETHER. `critical >= warning` has
|
|
77
|
+
// no coherent reading — critical fires deeper into the window than warning —
|
|
78
|
+
// and honouring one side of an inconsistent pair silently picks which of the
|
|
79
|
+
// operator's two numbers to discard. This also rejects a single override that
|
|
80
|
+
// contradicts the OTHER key's default (warning 20 with critical absent, i.e.
|
|
81
|
+
// 25); the resulting pair is the same nonsense either way. Set-time validation
|
|
82
|
+
// cannot stand in for this check: `config-set` writes one key per call, so
|
|
83
|
+
// tuning both (warning first, then critical) is transiently inconsistent on
|
|
84
|
+
// disk, and refusing it there would block a legitimate configuration.
|
|
85
|
+
function resolveThresholds(hooks) {
|
|
86
|
+
const defaults = { warning: WARNING_THRESHOLD, critical: CRITICAL_THRESHOLD };
|
|
87
|
+
if (!hooks || typeof hooks !== 'object') return defaults;
|
|
88
|
+
|
|
89
|
+
const usable = (value, fallback) =>
|
|
90
|
+
(Number.isFinite(value) && value >= 0 && value <= 100) ? value : fallback;
|
|
91
|
+
|
|
92
|
+
const warning = usable(hooks.context_warning_threshold, WARNING_THRESHOLD);
|
|
93
|
+
const critical = usable(hooks.context_critical_threshold, CRITICAL_THRESHOLD);
|
|
94
|
+
|
|
95
|
+
return critical < warning ? { warning, critical } : defaults;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// One DEFINITION of what counts as a lifecycle event name, shared by the #3709
|
|
99
|
+
// PreCompact reset and the #2289 output-envelope allowlist. Two call sites, one
|
|
100
|
+
// rule — so the two cannot drift into disagreeing about what "no event name" is.
|
|
101
|
+
// TOTAL, and STRICT about type: only an actual string is an event name. The old
|
|
102
|
+
// inline expression threw on a truthy non-string, and hoisting it ahead of the
|
|
103
|
+
// pipeline would have moved that throw ahead of the side effects #2289
|
|
104
|
+
// documents as always running; a String() coercion is no better — it renders
|
|
105
|
+
// ['PreCompact'] as 'PreCompact' and would run the reset off a malformed
|
|
106
|
+
// payload, and a hostile toString still throws (Codex review of #3808,
|
|
107
|
+
// round 3). typeof does neither: any non-string reads as "no event" — silent,
|
|
108
|
+
// side effects intact — on both call sites.
|
|
109
|
+
function readEventName(data) {
|
|
110
|
+
const name = data && data.hook_event_name;
|
|
111
|
+
if (typeof name === 'string') return name.trim();
|
|
112
|
+
// ABSENT vs MALFORMED are not the same event (Codex review of #3808, round 7,
|
|
113
|
+
// measured base-vs-head). A MISSING name is the documented pre-#2289 Gemini
|
|
114
|
+
// fallback: under GEMINI_API_KEY it means AfterTool and still emits. A name
|
|
115
|
+
// that is PRESENT but not a string is a malformed payload and must not
|
|
116
|
+
// inherit that fallback — at the merge-base it threw on `.trim()` after the
|
|
117
|
+
// side effects, so no envelope was ever produced, and collapsing both onto ''
|
|
118
|
+
// silently turned `42`, `{}` and `['PreCompact']` into emitting AfterTool
|
|
119
|
+
// events. Measured: base silent, head emitted, for both `42` and
|
|
120
|
+
// `['PreCompact']`. null keeps them distinguishable while staying unequal to
|
|
121
|
+
// every event name, so the PreCompact reset and the allowlist below are
|
|
122
|
+
// byte-for-byte unchanged for every well-formed payload.
|
|
123
|
+
return (name === undefined || name === null) ? '' : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// SENTINEL WRITE HARDENING (review of #3808, round 7). `warnPath` lives in
|
|
127
|
+
// os.tmpdir(), which may resolve to a shared sticky directory — not guaranteed
|
|
128
|
+
// per-user, and the file persists across invocations — so an object already
|
|
129
|
+
// sitting there may be a planted symlink. The three routine debounce-accounting writes were bare
|
|
130
|
+
// writeFileSync, which follows one and writes through to its target, while the
|
|
131
|
+
// PreCompact clear and the compaction watermark in this same file already
|
|
132
|
+
// refuse to. Unlink-then-O_EXCL is the watermark's own shape (the watermark
|
|
133
|
+
// write itself now calls this helper — review round 10): the unlink
|
|
134
|
+
// removes any existing object (regular file or link) and O_EXCL then refuses
|
|
135
|
+
// to create through one, so the write can only ever land on a fresh regular
|
|
136
|
+
// file this process made. Best effort by design — a lost sentinel write costs
|
|
137
|
+
// only debounce accounting, which is never worth breaking the hook over, so
|
|
138
|
+
// every failure is swallowed exactly as the watermark write's is.
|
|
139
|
+
// NOT an atomic read-modify-write, and not claimed to be (Codex review of
|
|
140
|
+
// #3808, round 7): two concurrent invocations can read the same state and race
|
|
141
|
+
// through unlink/create, so one invocation's accounting can be lost — the same
|
|
142
|
+
// lost-update race the bare writeFileSync already had, not a class this change
|
|
143
|
+
// introduces. What a lost write leaves behind is whatever the competing writer
|
|
144
|
+
// wrote, which may be a perfectly valid sentinel; it does not reliably mean
|
|
145
|
+
// "defaults on the next call". Advisory debounce bookkeeping is the right place
|
|
146
|
+
// to accept that.
|
|
147
|
+
// The read-side twin of writeSentinel (review of #3808, round 9). Both
|
|
148
|
+
// sentinel files this hook reads — the compaction watermark and the warn
|
|
149
|
+
// state — must be read the same way: lstat first so a planted link, FIFO or
|
|
150
|
+
// directory is refused before any open; O_NOFOLLOW so a link raced in between
|
|
151
|
+
// is refused by the kernel too (0 on Windows, where lstat already carries the
|
|
152
|
+
// check); a 4096-byte bound so a planted large file cannot stall a
|
|
153
|
+
// synchronous read. Rounds 4 and 7 each wrote that sequence inline at their
|
|
154
|
+
// own call site, which left two copies to keep in step by hand. One place
|
|
155
|
+
// now. Refusal THROWS; every caller already wraps the read in a try/catch and
|
|
156
|
+
// degrades to "no file", which is the same behaviour the inline copies had.
|
|
157
|
+
function readSentinel(target) {
|
|
158
|
+
const st = fs.lstatSync(target);
|
|
159
|
+
if (!st.isFile() || st.size > 4096) throw new Error('not a plain sentinel');
|
|
160
|
+
const fd = fs.openSync(target, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
161
|
+
try {
|
|
162
|
+
const buf = Buffer.alloc(st.size);
|
|
163
|
+
// The RETURN VALUE, not just the call (review of #3808, round 11). A file that shrinks
|
|
164
|
+
// between the lstat above and this read — a concurrent legitimate writer truncating
|
|
165
|
+
// mid-write, not the planted-object case the rest of this function guards — leaves the tail
|
|
166
|
+
// of `buf` zero-filled, and those NULs reach JSON.parse as garbage. Every caller already
|
|
167
|
+
// treats a throw here as "no file", so refusing a short read is both safer and the same
|
|
168
|
+
// outcome the caller would reach one line later, stated on purpose rather than by accident.
|
|
169
|
+
const bytesRead = fs.readSync(fd, buf, 0, st.size, 0);
|
|
170
|
+
if (bytesRead !== st.size) throw new Error('sentinel shrank under the read');
|
|
171
|
+
return buf.toString('utf8');
|
|
172
|
+
} finally { fs.closeSync(fd); }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function writeSentinel(target, payload) {
|
|
176
|
+
try {
|
|
177
|
+
try {
|
|
178
|
+
fs.unlinkSync(target);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
if (!e || e.code !== 'ENOENT') throw e;
|
|
181
|
+
}
|
|
182
|
+
const fd = fs.openSync(
|
|
183
|
+
target,
|
|
184
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL
|
|
185
|
+
);
|
|
186
|
+
try {
|
|
187
|
+
// LOOP, and check progress (Codex review of round 11). A single `fs.writeSync` is
|
|
188
|
+
// permitted to write fewer bytes than it was given, and the return value was discarded —
|
|
189
|
+
// a short write left a truncated sentinel that JSON.parse rejects, silently defeating the
|
|
190
|
+
// debounce accounting or the compaction watermark this write exists to record. Node's own
|
|
191
|
+
// `writeFileSync` loops for exactly this reason; the explicit no-progress guard keeps a
|
|
192
|
+
// pathological fd from spinning. Symmetric with the bytesRead check in readSentinel.
|
|
193
|
+
const buf = Buffer.from(payload, 'utf8');
|
|
194
|
+
let written = 0;
|
|
195
|
+
while (written < buf.length) {
|
|
196
|
+
const n = fs.writeSync(fd, buf, written, buf.length - written);
|
|
197
|
+
if (!(n > 0)) throw new Error('sentinel write made no progress');
|
|
198
|
+
written += n;
|
|
199
|
+
}
|
|
200
|
+
} finally { fs.closeSync(fd); }
|
|
201
|
+
} catch (e) { /* best effort — see above */ }
|
|
202
|
+
}
|
|
37
203
|
|
|
38
204
|
let input = '';
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
process.stdin.setEncoding('utf8');
|
|
45
|
-
process.stdin.on('data', chunk => input += chunk);
|
|
46
|
-
process.stdin.on('end', () => {
|
|
205
|
+
// Assigned by main(); the handler below clears it. Declared out here rather
|
|
206
|
+
// than inside main() because the handler closes over it.
|
|
207
|
+
let stdinTimeout = null;
|
|
208
|
+
|
|
209
|
+
const handleStdinEnd = () => {
|
|
47
210
|
clearTimeout(stdinTimeout);
|
|
48
211
|
try {
|
|
49
212
|
const data = JSON.parse(input);
|
|
@@ -60,29 +223,122 @@ process.stdin.on('end', () => {
|
|
|
60
223
|
allow(undefined);
|
|
61
224
|
}
|
|
62
225
|
|
|
63
|
-
|
|
226
|
+
const tmpDir = os.tmpdir();
|
|
227
|
+
const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
|
|
228
|
+
const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
|
|
229
|
+
const watermarkPath = path.join(tmpDir, `claude-ctx-${sessionId}-compacted.json`);
|
|
230
|
+
|
|
231
|
+
// #3709: a compaction RESTARTS the context lifecycle, so neither the warn
|
|
232
|
+
// sentinel nor the pre-compaction statusline reading may survive it. Full
|
|
233
|
+
// rationale — what dies when the sentinel outlives a compaction, why the
|
|
234
|
+
// reset sits ahead of the config gate and the metrics read, and why an
|
|
235
|
+
// aborted compaction deliberately stays cleared — lives in ONE place:
|
|
236
|
+
// docs/context-monitor.md, "PreCompact reset". Constraints the code itself
|
|
237
|
+
// must keep are stated at their lines below.
|
|
238
|
+
if (readEventName(data) === 'PreCompact') {
|
|
239
|
+
// ORDERING ASSUMPTION, stated rather than enforced (review of #3808,
|
|
240
|
+
// round 9): this reset and the debounce writeSentinel(warnPath) further
|
|
241
|
+
// down are two writers to the same file, and nothing here serialises
|
|
242
|
+
// them. A debounce invocation that read the pre-compaction state and
|
|
243
|
+
// lands its write AFTER this unlink would resurrect exactly the stale
|
|
244
|
+
// sentinel this block removes. The hook relies on the host dispatching a
|
|
245
|
+
// session's hooks one at a time, which Claude Code does; the other
|
|
246
|
+
// runtimes this hook is installed for are assumed to, and that is not
|
|
247
|
+
// tested. A lock file would close it at the cost of a second file to
|
|
248
|
+
// harden on every platform; not taken here.
|
|
249
|
+
// BOTH files: with the sentinel gone but the bridge still holding the
|
|
250
|
+
// pre-compaction reading (fresh for STALE_SECONDS), the next PostToolUse
|
|
251
|
+
// would fire a spurious CRITICAL off a context the compaction just freed
|
|
252
|
+
// (review of #3709).
|
|
253
|
+
for (const stale of [warnPath, metricsPath]) {
|
|
254
|
+
try {
|
|
255
|
+
fs.unlinkSync(stale);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
if (e && e.code === 'ENOENT') continue; // already absent — that IS the reset
|
|
258
|
+
// Best-effort fallback for a held handle (Windows EPERM/EBUSY):
|
|
259
|
+
// truncate to EMPTY — the one state both readers treat exactly like
|
|
260
|
+
// deletion, because JSON.parse('') throws. A well-formed "neutral"
|
|
261
|
+
// value is NOT equivalent: '{}' debounces the first post-compaction
|
|
262
|
+
// warning, '{"timestamp":0}' is never stale (falsy guard) and emits
|
|
263
|
+
// "undefined%" (review of #3808). Never through a LINK: lstat
|
|
264
|
+
// rejects non-regular files on every platform (Windows has no
|
|
265
|
+
// effective O_NOFOLLOW — libuv defines it as 0 — and TEMP/TMP means
|
|
266
|
+
// its tmpdir is not guaranteed per-user); O_NOFOLLOW additionally
|
|
267
|
+
// closes the lstat→open substitution race where honored. Every
|
|
268
|
+
// refusal lands in this give-up arm — including a Windows runner
|
|
269
|
+
// refusing the write-open of a freshly written file outright —
|
|
270
|
+
// which is why the fallback is best-effort, never asserted-on.
|
|
271
|
+
try {
|
|
272
|
+
if (fs.lstatSync(stale).isFile()) {
|
|
273
|
+
fs.closeSync(fs.openSync(
|
|
274
|
+
stale,
|
|
275
|
+
fs.constants.O_WRONLY | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW || 0)
|
|
276
|
+
));
|
|
277
|
+
}
|
|
278
|
+
} catch (e2) { /* give up, never throw */ }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// COMPACTION WATERMARK (review of #3808, round 3). Deleting the bridge
|
|
283
|
+
// only NARROWS the stale-reading window: the statusline is an
|
|
284
|
+
// uncoordinated process that re-writes the bridge on every render, so a
|
|
285
|
+
// render landing between this clear and the compaction's completion
|
|
286
|
+
// re-creates the PRE-compaction reading with a CURRENT timestamp — and
|
|
287
|
+
// it would sail past STALE_SECONDS as freshly valid. The watermark makes
|
|
288
|
+
// the pre-compaction reading identifiable rather than merely absent: the
|
|
289
|
+
// metrics read drops any reading not strictly newer than it. Written
|
|
290
|
+
// through writeSentinel (review of #3808, round 10 — this block was the
|
|
291
|
+
// shape writeSentinel was lifted from in round 7 and kept its own copy):
|
|
292
|
+
// unlink-then-O_EXCL so an existing file — or a planted symlink — is
|
|
293
|
+
// never followed or overwritten in place; failure to write degrades to
|
|
294
|
+
// the old narrowing, never throws.
|
|
295
|
+
writeSentinel(watermarkPath, JSON.stringify({ at: Math.floor(Date.now() / 1000) }));
|
|
296
|
+
// allow(), not raw process.exit: #3911/ADR-3889 moved this hook onto the
|
|
297
|
+
// declared-policy exit vocabulary while this PR was in review, and the
|
|
298
|
+
// PreCompact branch is new here, so it needs the same conversion.
|
|
299
|
+
// A compaction is never blocked by this hook — ALLOW is the policy the
|
|
300
|
+
// rest of the file already declares.
|
|
301
|
+
allow(undefined);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check if context warnings are disabled via config, and resolve the two
|
|
305
|
+
// fire-points from the same read (#4285 — one config read, not two).
|
|
64
306
|
// Collapsed existsSync+readFileSync into a single read guarded by try/catch
|
|
65
307
|
// (ENOENT or parse error → use defaults, same as old "planningDir absent" branch).
|
|
66
308
|
const cwd = data.cwd || process.cwd();
|
|
309
|
+
let thresholds = { warning: WARNING_THRESHOLD, critical: CRITICAL_THRESHOLD };
|
|
67
310
|
try {
|
|
68
311
|
const configPath = path.join(cwd, '.planning', 'config.json');
|
|
69
312
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
70
313
|
if (config.hooks?.context_warnings === false) {
|
|
71
314
|
allow(undefined);
|
|
72
315
|
}
|
|
316
|
+
// After the disable check, not before: a disabled monitor exits above and
|
|
317
|
+
// never reaches a threshold, so resolving first would only add work to the
|
|
318
|
+
// path that does nothing. allow() exits the process (it does not throw),
|
|
319
|
+
// so this line is unreachable when warnings are off.
|
|
320
|
+
thresholds = resolveThresholds(config.hooks);
|
|
73
321
|
} catch (e) {
|
|
74
|
-
// Missing or unparseable config → proceed with defaults (context warnings
|
|
322
|
+
// Missing or unparseable config → proceed with defaults (context warnings
|
|
323
|
+
// enabled, thresholds at the constants above, which `thresholds` already holds)
|
|
75
324
|
}
|
|
76
325
|
|
|
77
|
-
const tmpDir = os.tmpdir();
|
|
78
|
-
const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
|
|
79
|
-
|
|
80
326
|
// If no metrics file, this is a subagent or fresh session -- exit silently.
|
|
81
327
|
// Collapsed existsSync+readFileSync: ENOENT → exit 0 (identical to old !existsSync branch),
|
|
82
328
|
// other errors rethrow to the outer catch (swallowed → exit 0, as before).
|
|
329
|
+
//
|
|
330
|
+
// Through readSentinel, like the other two (review of #3808, round 11). This read was the
|
|
331
|
+
// asymmetry left in this file: `metricsPath` is built one line away from `warnPath` and
|
|
332
|
+
// `watermarkPath` (same tmpdir, same predictable `claude-ctx-{sessionId}` shape), it is the
|
|
333
|
+
// only one of the three read on EVERY invocation, and it was the only one still reached by a
|
|
334
|
+
// bare readFileSync — so the symlink-to-FIFO stall the other two are hardened against was
|
|
335
|
+
// still reachable here, on the highest-traffic path in the file. The 4096-byte bound is
|
|
336
|
+
// ample: the statusline writes four fixed fields (`gsd-statusline.js`, ~140 bytes with a
|
|
337
|
+
// UUID session id), so no legitimate bridge approaches it. A refusal throws and lands in the
|
|
338
|
+
// rethrow below exactly as an unreadable or malformed bridge already did.
|
|
83
339
|
let metricsRaw;
|
|
84
340
|
try {
|
|
85
|
-
metricsRaw =
|
|
341
|
+
metricsRaw = readSentinel(metricsPath);
|
|
86
342
|
} catch (e) {
|
|
87
343
|
if (e && e.code === 'ENOENT') allow(undefined);
|
|
88
344
|
throw e;
|
|
@@ -90,6 +346,46 @@ process.stdin.on('end', () => {
|
|
|
90
346
|
const metrics = JSON.parse(metricsRaw);
|
|
91
347
|
const now = Math.floor(Date.now() / 1000);
|
|
92
348
|
|
|
349
|
+
// #3709 (round 3): a reading not clearly PAST the compaction is suspect,
|
|
350
|
+
// whatever its timestamp says — the statusline re-writes the bridge on
|
|
351
|
+
// every render, and a render during the compaction stamps the OLD
|
|
352
|
+
// remaining_percentage with a current time. The watermark records the
|
|
353
|
+
// compaction's START, so "newer than the watermark" alone still admits a
|
|
354
|
+
// mid-compaction render (Codex review of #3808, round 3): the grace
|
|
355
|
+
// window covers the compaction's own duration. `!(>)` rather than `<=` so
|
|
356
|
+
// a missing/zero/garbage timestamp is also dropped once a compaction has
|
|
357
|
+
// happened — an unstamped reading cannot prove it is post-compaction.
|
|
358
|
+
//
|
|
359
|
+
// The watermark itself must be SANE to count: one stamped in the future
|
|
360
|
+
// (a clock step backwards, a stray file) would otherwise drop every
|
|
361
|
+
// reading indefinitely and silently self-disable monitoring — so it is
|
|
362
|
+
// honored only when its own timestamp is not ahead of this process's
|
|
363
|
+
// clock (small skew allowed). No watermark, an unreadable one, or an
|
|
364
|
+
// insane one all degrade to the plain STALE_SECONDS behaviour below.
|
|
365
|
+
//
|
|
366
|
+
// READ HARDENING (Codex review of #3808, round 4). The WRITE side already
|
|
367
|
+
// refuses to follow or overwrite a planted object (unlink-then-O_EXCL
|
|
368
|
+
// above), but this read was a bare readFileSync — so on any write-side
|
|
369
|
+
// give-up the planted object survived and every later invocation followed
|
|
370
|
+
// it. In a shared sticky os.tmpdir() that is a mute primitive (a planted
|
|
371
|
+
// recent watermark suppresses monitoring) and a stall primitive (a symlink
|
|
372
|
+
// to a FIFO blocks this synchronous read indefinitely; measured: such a
|
|
373
|
+
// read is still running after 300ms). The same lstat + O_NOFOLLOW pair the
|
|
374
|
+
// sentinel path uses, plus a size bound, applied to the file this PR adds.
|
|
375
|
+
// Every refusal degrades to "no watermark", never throws.
|
|
376
|
+
try {
|
|
377
|
+
const watermark = JSON.parse(readSentinel(watermarkPath));
|
|
378
|
+
if (
|
|
379
|
+
watermark && typeof watermark.at === 'number'
|
|
380
|
+
&& watermark.at <= now + WATERMARK_SKEW_SECONDS
|
|
381
|
+
&& !(metrics.timestamp > watermark.at + COMPACT_GRACE_SECONDS)
|
|
382
|
+
) {
|
|
383
|
+
// Same #3911/ADR-3889 conversion as the PreCompact branch above: this
|
|
384
|
+
// gate is new in this PR, so it did not exist to be migrated.
|
|
385
|
+
allow(undefined);
|
|
386
|
+
}
|
|
387
|
+
} catch (e) { /* no watermark — nothing to compare against */ }
|
|
388
|
+
|
|
93
389
|
// Ignore stale metrics
|
|
94
390
|
if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
|
|
95
391
|
allow(undefined);
|
|
@@ -99,19 +395,36 @@ process.stdin.on('end', () => {
|
|
|
99
395
|
const usedPct = metrics.used_pct;
|
|
100
396
|
|
|
101
397
|
// No warning needed
|
|
102
|
-
if (remaining >
|
|
398
|
+
if (remaining > thresholds.warning) {
|
|
103
399
|
allow(undefined);
|
|
104
400
|
}
|
|
105
401
|
|
|
106
|
-
// Debounce: check if we warned recently
|
|
107
|
-
|
|
402
|
+
// Debounce: check if we warned recently. `warnPath` is resolved above, next to
|
|
403
|
+
// metricsPath, because the PreCompact reset needs it before this point.
|
|
108
404
|
let warnData = { callsSinceWarn: 0, lastLevel: null };
|
|
109
405
|
let firstWarn = true;
|
|
110
406
|
|
|
111
407
|
// Collapsed existsSync+readFileSync: ENOENT or parse error → keep default warnData
|
|
112
408
|
// (same as old "file absent" branch). firstWarn tracks whether we read a valid sentinel.
|
|
409
|
+
//
|
|
410
|
+
// READ HARDENING (self-found while addressing round 7; same class, same
|
|
411
|
+
// file). Hardening the writes above leaves this read as a bare
|
|
412
|
+
// readFileSync on warnPath, which is the exact asymmetry round 7 asks be
|
|
413
|
+
// removed from the write side — and the watermark's read was hardened in
|
|
414
|
+
// round 4 for this same reason, so leaving this one recreates it. It was
|
|
415
|
+
// not the LAST bare read in the file: the statusline bridge kept its own
|
|
416
|
+
// until round 11 found it. All three go through readSentinel now. The
|
|
417
|
+
// exposure is real but bounded: the writes now unlink any planted object,
|
|
418
|
+
// so only a read reaching this line BEFORE the first write of an
|
|
419
|
+
// invocation can follow one, and re-planting reopens it every invocation.
|
|
420
|
+
// Following it is a mute primitive — attacker-chosen callsSinceWarn keeps
|
|
421
|
+
// the debounce arm below taken so no warning is ever emitted — and a
|
|
422
|
+
// symlink to a FIFO stalls this synchronous read, the same two primitives
|
|
423
|
+
// measured on the watermark. Same lstat + O_NOFOLLOW + size bound; every
|
|
424
|
+
// refusal degrades to the default warnData this catch already produces,
|
|
425
|
+
// so a normal regular file behaves exactly as before.
|
|
113
426
|
try {
|
|
114
|
-
warnData = JSON.parse(
|
|
427
|
+
warnData = JSON.parse(readSentinel(warnPath));
|
|
115
428
|
firstWarn = false;
|
|
116
429
|
} catch (e) {
|
|
117
430
|
// Missing or corrupted sentinel → firstWarn stays true, warnData stays at defaults
|
|
@@ -119,7 +432,7 @@ process.stdin.on('end', () => {
|
|
|
119
432
|
|
|
120
433
|
warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
|
|
121
434
|
|
|
122
|
-
const isCritical = remaining <=
|
|
435
|
+
const isCritical = remaining <= thresholds.critical;
|
|
123
436
|
const currentLevel = isCritical ? 'critical' : 'warning';
|
|
124
437
|
|
|
125
438
|
// Emit immediately on first warning, then debounce subsequent ones
|
|
@@ -127,14 +440,14 @@ process.stdin.on('end', () => {
|
|
|
127
440
|
const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
|
|
128
441
|
if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
|
|
129
442
|
// Update counter and exit without warning
|
|
130
|
-
|
|
443
|
+
writeSentinel(warnPath, JSON.stringify(warnData));
|
|
131
444
|
allow(undefined);
|
|
132
445
|
}
|
|
133
446
|
|
|
134
447
|
// Reset debounce counter
|
|
135
448
|
warnData.callsSinceWarn = 0;
|
|
136
449
|
warnData.lastLevel = currentLevel;
|
|
137
|
-
|
|
450
|
+
writeSentinel(warnPath, JSON.stringify(warnData));
|
|
138
451
|
|
|
139
452
|
// Detect if GSD is active (has .planning/STATE.md in working directory)
|
|
140
453
|
const isGsdActive = fs.existsSync(path.join(cwd, '.planning', 'STATE.md'));
|
|
@@ -161,7 +474,7 @@ process.stdin.on('end', () => {
|
|
|
161
474
|
).unref();
|
|
162
475
|
warnData.criticalRecorded = true;
|
|
163
476
|
// Persist the sentinel so subsequent debounce cycles don't re-fire
|
|
164
|
-
|
|
477
|
+
writeSentinel(warnPath, JSON.stringify(warnData));
|
|
165
478
|
} catch { /* non-critical — don't let state recording break the hook */ }
|
|
166
479
|
}
|
|
167
480
|
|
|
@@ -198,7 +511,7 @@ process.stdin.on('end', () => {
|
|
|
198
511
|
// not enough — a missing name would still fall through to the injection path.
|
|
199
512
|
// All side effects above (debounce counter, one-time critical-session
|
|
200
513
|
// recording) have already run regardless of whether output is emitted.
|
|
201
|
-
const eventName = (data
|
|
514
|
+
const eventName = readEventName(data);
|
|
202
515
|
// Preserve the pre-#2289 Gemini fallback: a missing event name under a
|
|
203
516
|
// Gemini-dialect runtime (GEMINI_API_KEY set) still means AfterTool, so its
|
|
204
517
|
// advisory output is unchanged. A missing name on any other host is silent.
|
|
@@ -209,7 +522,8 @@ process.stdin.on('end', () => {
|
|
|
209
522
|
const output = {
|
|
210
523
|
hookSpecificOutput: {
|
|
211
524
|
hookEventName: eventName || "AfterTool",
|
|
212
|
-
additionalContext: message
|
|
525
|
+
additionalContext: message,
|
|
526
|
+
severity: currentLevel
|
|
213
527
|
}
|
|
214
528
|
};
|
|
215
529
|
process.stdout.write(JSON.stringify(output));
|
|
@@ -220,4 +534,34 @@ process.stdin.on('end', () => {
|
|
|
220
534
|
// exit(0) fail-open behavior exactly (#3911).
|
|
221
535
|
crash(ON_CRASH, undefined);
|
|
222
536
|
}
|
|
223
|
-
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// The stdin adapter is the only side-effecting statement in this file, so it is
|
|
540
|
+
// the only thing that must not run on `require()`. Gating it lets a test import
|
|
541
|
+
// `resolveThresholds` and drive it directly — the repo's own conclusion for a
|
|
542
|
+
// seam like this (CONTEXT-INDEX, on the ROADMAP Requirements parser: a closure
|
|
543
|
+
// reachable only by spawning the CLI is one "no fast-check property can do").
|
|
544
|
+
// A spawn-per-case property test is not the same test: it would exercise the
|
|
545
|
+
// resolver at whatever pairs survive to an observable severity, not over its
|
|
546
|
+
// whole numeric domain.
|
|
547
|
+
/* istanbul ignore next -- stdin adapter, exercised via spawnSync in tests */
|
|
548
|
+
function main() {
|
|
549
|
+
// Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
|
|
550
|
+
// Windows/Git Bash, or slow Claude Code piping during large outputs),
|
|
551
|
+
// exit silently instead of hanging until Claude Code kills the process
|
|
552
|
+
// and reports "hook error". See #775, #1162.
|
|
553
|
+
stdinTimeout = setTimeout(() => allow(undefined), 10000);
|
|
554
|
+
process.stdin.setEncoding('utf8');
|
|
555
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
556
|
+
process.stdin.on('end', handleStdinEnd);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (require.main === module) {
|
|
560
|
+
main();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Exported for the #4285 property test only. The two constants ride along so a
|
|
564
|
+
// test asserts the fallback pair against the SOURCE of truth rather than
|
|
565
|
+
// re-hardcoding 35/25 — a test carrying its own copy of the defaults would stay
|
|
566
|
+
// green if the constants were edited.
|
|
567
|
+
module.exports = { resolveThresholds, WARNING_THRESHOLD, CRITICAL_THRESHOLD };
|
|
@@ -62,8 +62,8 @@ const { allow } = require('./lib/hook-exit.js');
|
|
|
62
62
|
// hooks/lib/cursor-workspace.js. Staged next to these scripts by
|
|
63
63
|
// writeCursorHooksJson so the require always resolves post-install.
|
|
64
64
|
const { resolveStatePath } = require('./lib/cursor-workspace.js');
|
|
65
|
-
const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch } = require('./lib/isolation-sentinel.js');
|
|
66
|
-
const { REASON_CODE } = require('./lib/isolation-deny-reason.js');
|
|
65
|
+
const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch, buildSentinelDiscard } = require('./lib/isolation-sentinel.js');
|
|
66
|
+
const { REASON_CODE, describeSentinelDiscard } = require('./lib/isolation-deny-reason.js');
|
|
67
67
|
// #3582: gsd-core/bin/lib/*.cjs (runtime-homes.cjs, worktree-safety.cjs,
|
|
68
68
|
// runtime-name-policy.cjs, capability-registry.cjs — required below, inside
|
|
69
69
|
// resolveIsolationEvidence and resolveFallbackIsolation) are tsc build
|
|
@@ -491,18 +491,25 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
|
|
|
491
491
|
`Refusing to allow this subagent to spawn until the runtime library is built — a guard ` +
|
|
492
492
|
`that cannot verify must not answer "safe" (#3050).`,
|
|
493
493
|
reasonCode: REASON_CODE.RUNTIME_BUILD_FAILED,
|
|
494
|
+
sentinelDiscarded: null,
|
|
494
495
|
};
|
|
495
496
|
}
|
|
496
497
|
|
|
498
|
+
// #3045 BLOCKER fix: a fresh sentinel is authoritative for THIS dispatch's
|
|
499
|
+
// actual resolved isolation — see the doc comment above.
|
|
500
|
+
// #3045 SECURITY F2: a fresh sentinel that names a DIFFERENT plan/phase
|
|
501
|
+
// than this dispatch is not applicable to it — fall through to the
|
|
502
|
+
// conservative fallback exactly as a stale sentinel would.
|
|
503
|
+
// Hoisted (readSentinel never throws) so the "present, fresh, but did not
|
|
504
|
+
// apply" case (#4594 row 15) can be reported on every deny path below
|
|
505
|
+
// instead of silently discarded.
|
|
506
|
+
const sentinel = readSentinel(root, { clock });
|
|
507
|
+
const applies = sentinelAppliesToDispatch(sentinel, dispatchIds);
|
|
508
|
+
const sentinelDiscarded = buildSentinelDiscard(sentinel, dispatchIds);
|
|
509
|
+
|
|
497
510
|
let declaredIsolation;
|
|
498
511
|
try {
|
|
499
|
-
|
|
500
|
-
// dispatch's actual resolved isolation — see the doc comment above.
|
|
501
|
-
// #3045 SECURITY F2: a fresh sentinel that names a DIFFERENT
|
|
502
|
-
// plan/phase than this dispatch is not applicable to it — fall through
|
|
503
|
-
// to the conservative fallback exactly as a stale sentinel would.
|
|
504
|
-
const sentinel = readSentinel(root, { clock });
|
|
505
|
-
declaredIsolation = (sentinel.present && !sentinel.stale && sentinelAppliesToDispatch(sentinel, dispatchIds))
|
|
512
|
+
declaredIsolation = (sentinel.present && !sentinel.stale && applies)
|
|
506
513
|
? sentinel.isolation
|
|
507
514
|
: resolveFallbackIsolation(root, configPath);
|
|
508
515
|
} catch {
|
|
@@ -513,8 +520,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
|
|
|
513
520
|
`dispatch-isolation configuration ('.planning/config.json' exists under "${root}"). ` +
|
|
514
521
|
`Refusing to allow this subagent to spawn without being able to verify whether ` +
|
|
515
522
|
`isolation is required — a guard that cannot verify must not answer "safe" (#3050). ` +
|
|
516
|
-
`Retry once the project configuration is readable
|
|
523
|
+
`Retry once the project configuration is readable.` +
|
|
524
|
+
(sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
|
|
517
525
|
reasonCode: REASON_CODE.CONFIG_UNREADABLE,
|
|
526
|
+
sentinelDiscarded,
|
|
518
527
|
};
|
|
519
528
|
}
|
|
520
529
|
|
|
@@ -530,8 +539,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
|
|
|
530
539
|
`GSD subagent isolation guard: this project's dispatch isolation resolves to ` +
|
|
531
540
|
`"harness-worktree", but the subagentStart payload for this dispatch carries no usable ` +
|
|
532
541
|
`subagent_type. Refusing to allow it to spawn without being able to confirm whether it ` +
|
|
533
|
-
`is a GSD executor — a guard that cannot verify must not answer "safe" (#3050)
|
|
542
|
+
`is a GSD executor — a guard that cannot verify must not answer "safe" (#3050).` +
|
|
543
|
+
(sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
|
|
534
544
|
reasonCode: REASON_CODE.NO_SUBAGENT_TYPE,
|
|
545
|
+
sentinelDiscarded,
|
|
535
546
|
};
|
|
536
547
|
}
|
|
537
548
|
|
|
@@ -547,8 +558,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
|
|
|
547
558
|
`"harness-worktree", but whether "${root}" is running in an isolated Cursor worktree ` +
|
|
548
559
|
`could not be determined (git did not respond). Refusing to allow subagent_type=` +
|
|
549
560
|
`"${subagentType}" to spawn without being able to verify isolation — a guard that ` +
|
|
550
|
-
`cannot verify must not answer "safe" (#3050). Retry once git is responsive
|
|
561
|
+
`cannot verify must not answer "safe" (#3050). Retry once git is responsive.` +
|
|
562
|
+
(sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
|
|
551
563
|
reasonCode: REASON_CODE.CANNOT_DETERMINE_ISOLATION,
|
|
564
|
+
sentinelDiscarded,
|
|
552
565
|
};
|
|
553
566
|
}
|
|
554
567
|
|
|
@@ -560,8 +573,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
|
|
|
560
573
|
`which is not an isolated Cursor worktree — it would edit the user's primary checkout ` +
|
|
561
574
|
`directly, with no consent and no warning. Start an isolated session first (the ` +
|
|
562
575
|
`"--worktree" CLI flag or the "/worktree" chat command; Cursor manages these worktrees ` +
|
|
563
|
-
`under "~/.cursor/worktrees/") and retry
|
|
576
|
+
`under "~/.cursor/worktrees/") and retry.` +
|
|
577
|
+
(sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
|
|
564
578
|
reasonCode: REASON_CODE.NOT_ISOLATED_WORKTREE,
|
|
579
|
+
sentinelDiscarded,
|
|
565
580
|
};
|
|
566
581
|
}
|
|
567
582
|
|
|
@@ -611,7 +626,12 @@ function main() {
|
|
|
611
626
|
decision = { action: 'allow' };
|
|
612
627
|
}
|
|
613
628
|
if (decision.action === 'deny') {
|
|
614
|
-
const out = {
|
|
629
|
+
const out = {
|
|
630
|
+
permission: 'deny',
|
|
631
|
+
user_message: decision.reason,
|
|
632
|
+
reason_code: decision.reasonCode,
|
|
633
|
+
sentinel_discarded: decision.sentinelDiscarded ?? null,
|
|
634
|
+
};
|
|
615
635
|
if (additionalContext !== null) out.additional_context = additionalContext;
|
|
616
636
|
process.stdout.write(JSON.stringify(out));
|
|
617
637
|
return;
|