session-orchestrator 5.11.0 → 5.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 +12 -1
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/agents/explorer.toml +1 -1
- package/.codex-plugin/agents/session-reviewer.toml +2 -2
- package/.codex-plugin/agents/wave-worker.toml +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +8 -0
- package/.orchestrator/policy/public-export.json +30 -0
- package/CHANGELOG.md +180 -3
- package/README.md +126 -117
- package/agents/analyst.md +7 -2
- package/agents/architect-reviewer.md +7 -2
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +9 -2
- package/agents/dialectic-deriver.md +9 -3
- package/agents/docs-writer.md +7 -2
- package/agents/eval-judge.md +7 -1
- package/agents/qa-strategist.md +7 -2
- package/agents/security-reviewer.md +9 -2
- package/agents/session-reviewer.md +18 -2
- package/agents/skill-applied-judge.md +7 -1
- package/agents/test-writer.md +9 -2
- package/agents/ui-developer.md +9 -2
- package/agents/ux-evaluator.md +6 -1
- package/commands/session.md +4 -2
- package/commands/templates-ack.md +89 -18
- package/docs/agent-authoring.md +30 -11
- package/docs/ci-setup.md +38 -2
- package/docs/codex-setup.md +33 -9
- package/docs/components.md +13 -2
- package/docs/cursor-setup.md +3 -3
- package/docs/events-schema.md +5 -4
- package/docs/evidence-candidates.md +63 -0
- package/docs/fleet-checkin-audit.md +7 -4
- package/docs/github-mirror-protection.md +150 -3
- package/docs/install.md +102 -25
- package/docs/instruction-delivery.md +1 -1
- package/docs/migration-v4.md +2 -2
- package/docs/mods.md +118 -0
- package/docs/owner-config-schema.md +1 -0
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/pm-skills-marketplace.md +4 -3
- package/docs/rule-authoring.md +24 -13
- package/docs/scope-collision-guard.md +2 -1
- package/docs/session-config-reference.md +55 -21
- package/docs/session-config-template.md +32 -24
- package/docs/sessions-ledger-repair.md +137 -0
- package/docs/vault-docs-architecture.md +1 -1
- package/hooks/_lib/hook-import-set.json +406 -73
- package/hooks/_lib/hook-root.mjs +301 -0
- package/hooks/_lib/lock-bootstrap.mjs +434 -4
- package/hooks/_lib/orchestrator-ignore.mjs +3 -0
- package/hooks/_lib/subagent-transcript.mjs +2 -2
- package/hooks/_lib/vcs-create-matcher.mjs +13 -17
- package/hooks/cwd-change-restore.mjs +5 -1
- package/hooks/enforce-commands.mjs +4 -3
- package/hooks/enforce-scope.mjs +11 -1
- package/hooks/hooks-codex.json +48 -2
- package/hooks/hooks.json +31 -31
- package/hooks/on-session-end.mjs +38 -15
- package/hooks/on-session-start.mjs +158 -59
- package/hooks/on-stop.mjs +11 -6
- package/hooks/post-bash-issue-budget-refund.mjs +6 -1
- package/hooks/post-bash-write-verify.mjs +8 -4
- package/hooks/post-edit-validate.mjs +45 -52
- package/hooks/post-subagent-discovery-validator.mjs +2 -2
- package/hooks/post-tool-batch-wave-signal.mjs +8 -4
- package/hooks/post-tool-failure-corrective-context.mjs +5 -1
- package/hooks/pre-bash-destructive-guard.mjs +220 -8
- package/hooks/pre-bash-issue-budget.mjs +6 -1
- package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
- package/hooks/pre-bash-staging-fence.mjs +258 -14
- package/hooks/pre-bash-templates-first.mjs +28 -27
- package/hooks/run-node.sh +1 -1
- package/hooks/skill-invocation-telemetry.mjs +15 -8
- package/hooks/subagent-telemetry.mjs +77 -14
- package/hooks/wave-scope-commit-guard.mjs +143 -14
- package/mods/so-hud/.claude-plugin/plugin.json +11 -0
- package/mods/so-hud/README.md +57 -0
- package/mods/so-hud/hooks/beacon-io.mjs +101 -0
- package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
- package/mods/so-hud/hooks/hooks.json +1 -0
- package/mods/so-hud/hooks/logic.ts +182 -0
- package/mods/so-hud/hooks/register.tsx +670 -0
- package/mods/so-hud/hooks/rules.ts +2998 -0
- package/mods/so-hud/tsconfig.json +8 -0
- package/mods/so-hud/types/index.d.ts +36 -0
- package/package.json +4 -3
- package/rules/_index.md +1 -1
- package/rules/always-on/ask-via-tool.md +1 -1
- package/rules/always-on/loop-and-monitor.md +17 -194
- package/rules/always-on/parallel-sessions.md +3 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +1 -1
- package/rules/opt-in-stack/security-web.md +5 -1
- package/scripts/agent-usage-census.mjs +79 -0
- package/scripts/apply-session-learnings.mjs +34 -6
- package/scripts/archive-closed-prds.mjs +21 -5
- package/scripts/autopilot.mjs +11 -5
- package/scripts/backfill-abandoned-sessions.mjs +13 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +61 -6
- package/scripts/backfill-sessions.mjs +16 -2
- package/scripts/check-package-manager.mjs +26 -8
- package/scripts/check-sessions-integrity.mjs +4 -1
- package/scripts/ci/assert-vitest-green.mjs +3 -0
- package/scripts/ci/windows-smoke.mjs +201 -0
- package/scripts/close-precheck.mjs +55 -0
- package/scripts/dialectic-deriver.mjs +20 -12
- package/scripts/emit-event.mjs +2 -2
- package/scripts/emit-session.mjs +239 -20
- package/scripts/eval-session.mjs +17 -3
- package/scripts/export-hw-learnings.mjs +29 -2
- package/scripts/fleet-checkin-audit.mjs +8 -4
- package/scripts/fleet-checkin.mjs +73 -3
- package/scripts/gc-stale-worktrees.mjs +6 -1
- package/scripts/github-export.mjs +128 -0
- package/scripts/github-protection-audit.mjs +3 -1
- package/scripts/handover-queue-check.mjs +209 -0
- package/scripts/harness-audit.mjs +6 -4
- package/scripts/lib/agent-frontmatter.mjs +57 -10
- package/scripts/lib/agent-status.mjs +125 -7
- package/scripts/lib/agent-usage-census.mjs +394 -0
- package/scripts/lib/auto-dialectic.mjs +17 -10
- package/scripts/lib/auto-dream.mjs +22 -14
- package/scripts/lib/autopilot/loop.mjs +3 -1
- package/scripts/lib/autopilot/recent-runs.mjs +4 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
- package/scripts/lib/autopilot/telemetry.mjs +5 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/baseline-archetypes.mjs +10 -2
- package/scripts/lib/blocked-commands-policy.mjs +4 -3
- package/scripts/lib/build-live-signals.mjs +13 -9
- package/scripts/lib/ci-status-banner.mjs +329 -24
- package/scripts/lib/codex/plugin-contract.mjs +8 -0
- package/scripts/lib/cold-start-detector.mjs +5 -2
- package/scripts/lib/command-blocker.mjs +603 -43
- package/scripts/lib/common.mjs +3 -2
- package/scripts/lib/config/cross-repo.mjs +74 -7
- package/scripts/lib/config/host-paths.mjs +6 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config/section-extractor.mjs +65 -11
- package/scripts/lib/config/template-keys.mjs +153 -0
- package/scripts/lib/config/vault-integration.mjs +1 -0
- package/scripts/lib/config.mjs +3 -3
- package/scripts/lib/coordinator-snapshot.mjs +1 -1
- package/scripts/lib/discovery/triage-state.mjs +2 -1
- package/scripts/lib/dispatcher/rank.mjs +5 -7
- package/scripts/lib/ecosystem-health.mjs +2 -2
- package/scripts/lib/eval/engine.mjs +3 -2
- package/scripts/lib/eval/session-resolve.mjs +6 -2
- package/scripts/lib/eval/sink.mjs +4 -3
- package/scripts/lib/events-retention-banner.mjs +3 -2
- package/scripts/lib/events-schema.mjs +10 -0
- package/scripts/lib/events.mjs +23 -12
- package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
- package/scripts/lib/fleet-protocol.mjs +21 -0
- package/scripts/lib/gates/gate-full.mjs +172 -11
- package/scripts/lib/gates/gate-helpers.mjs +236 -23
- package/scripts/lib/git-config-drift.mjs +141 -7
- package/scripts/lib/github-export.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
- package/scripts/lib/handover-queue-marks.mjs +159 -0
- package/scripts/lib/hardware-pattern-detector.mjs +103 -13
- package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
- package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
- package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
- package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
- package/scripts/lib/host-identity.mjs +9 -5
- package/scripts/lib/http-request.mjs +64 -0
- package/scripts/lib/instruction-budget-guard.mjs +30 -22
- package/scripts/lib/io.mjs +6 -3
- package/scripts/lib/issue-budget-reconcile.mjs +3 -2
- package/scripts/lib/issue-budget.mjs +36 -7
- package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
- package/scripts/lib/learnings/io.mjs +289 -15
- package/scripts/lib/learnings/select.mjs +53 -4
- package/scripts/lib/learnings/store-role.mjs +209 -0
- package/scripts/lib/learnings/surface.mjs +46 -7
- package/scripts/lib/ledger-path.mjs +113 -0
- package/scripts/lib/maintenance-due-banner.mjs +63 -12
- package/scripts/lib/memory-banner.mjs +5 -6
- package/scripts/lib/memory-proposals/collector.mjs +5 -2
- package/scripts/lib/memory-proposals/sink.mjs +7 -4
- package/scripts/lib/memory-proposals/store.mjs +22 -5
- package/scripts/lib/mode-selector-accuracy.mjs +2 -1
- package/scripts/lib/no-follow-open.mjs +57 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
- package/scripts/lib/own-lock-root.mjs +95 -0
- package/scripts/lib/owner-interview.mjs +2 -1
- package/scripts/lib/owner-yaml.mjs +70 -7
- package/scripts/lib/platform.mjs +161 -9
- package/scripts/lib/plugin-update-banner.mjs +15 -11
- package/scripts/lib/pre-push-local-partial.mjs +117 -0
- package/scripts/lib/pre-push-remote-gate.mjs +151 -25
- package/scripts/lib/process-group.mjs +270 -34
- package/scripts/lib/project-hygiene.mjs +28 -0
- package/scripts/lib/quality-gate.mjs +33 -9
- package/scripts/lib/reconcile/backlog.mjs +29 -15
- package/scripts/lib/reconcile/engine.mjs +26 -2
- package/scripts/lib/reconcile/idempotency.mjs +65 -3
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
- package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
- package/scripts/lib/resource-probe/evaluate.mjs +32 -9
- package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
- package/scripts/lib/rule-loader.mjs +20 -10
- package/scripts/lib/scope-baseline.mjs +123 -8
- package/scripts/lib/scope-echo.mjs +5 -2
- package/scripts/lib/scope-gate.mjs +55 -20
- package/scripts/lib/session-close-backfill.mjs +75 -7
- package/scripts/lib/session-end/close-precheck.mjs +151 -0
- package/scripts/lib/session-end/phase-skip.mjs +8 -7
- package/scripts/lib/session-end/tail-runner.mjs +7 -2
- package/scripts/lib/session-end/vault-orphans.mjs +205 -0
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-invocation.mjs +14 -3
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +85 -12
- package/scripts/lib/session-schema/constants.mjs +9 -0
- package/scripts/lib/session-schema/filters.mjs +56 -1
- package/scripts/lib/session-schema/validator.mjs +15 -5
- package/scripts/lib/session-start-probes.mjs +149 -22
- package/scripts/lib/session-token-rollup.mjs +2 -1
- package/scripts/lib/session-work-trees.mjs +166 -0
- package/scripts/lib/sessions-canonical.mjs +61 -3
- package/scripts/lib/sessions-integrity-banner.mjs +17 -39
- package/scripts/lib/sessions-staleness-banner.mjs +8 -5
- package/scripts/lib/sh-quote.mjs +1 -0
- package/scripts/lib/skill-evidence-window.mjs +5 -1
- package/scripts/lib/skill-health/join.mjs +10 -9
- package/scripts/lib/skill-invocations-schema.mjs +13 -0
- package/scripts/lib/skill-judgments-schema.mjs +6 -2
- package/scripts/lib/snapshot-probe.mjs +414 -0
- package/scripts/lib/so-hud/collect.mjs +174 -0
- package/scripts/lib/so-hud/runtime.mjs +87 -0
- package/scripts/lib/spiral-carryover.mjs +8 -1
- package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
- package/scripts/lib/state-md/idle-reset.mjs +248 -0
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md/recommendations.mjs +26 -4
- package/scripts/lib/state-md-peer-guard.mjs +2 -0
- package/scripts/lib/state-md.mjs +31 -8
- package/scripts/lib/sunset/walker.mjs +4 -3
- package/scripts/lib/telemetry/consent.mjs +4 -2
- package/scripts/lib/telemetry/queue.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +45 -9
- package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
- package/scripts/lib/tmux-layout/layouts.mjs +20 -3
- package/scripts/lib/ux-grill/paths.mjs +5 -1
- package/scripts/lib/validate/check-agents.mjs +37 -6
- package/scripts/lib/validate/check-component-paths.mjs +25 -1
- package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
- package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
- package/scripts/lib/validate/check-json-files.mjs +3 -1
- package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
- package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
- package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
- package/scripts/lib/validate/check-unwired-features.mjs +4 -2
- package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
- package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
- package/scripts/lib/validate/confidential-names.mjs +3 -1
- package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
- package/scripts/lib/vault-mirror/namespace.mjs +22 -9
- package/scripts/lib/vault-mirror/process.mjs +197 -65
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +8 -7
- package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
- package/scripts/lib/vault-mirror/retention.mjs +2 -2
- package/scripts/lib/vault-mirror/utils.mjs +106 -13
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +34 -5
- package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
- package/scripts/lib/win-process-tree.mjs +137 -0
- package/scripts/lib/workspace.mjs +1 -1
- package/scripts/lib/worktree/constants.mjs +1 -1
- package/scripts/lib/worktree/lifecycle.mjs +1 -1
- package/scripts/lib/worktree/listing.mjs +1 -1
- package/scripts/lib/zx.mjs +50 -0
- package/scripts/mcp-server.mjs +16 -7
- package/scripts/mcp-server.sh +13 -83
- package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
- package/scripts/memory-propose.mjs +10 -3
- package/scripts/migrate-cold-start-seed.mjs +7 -0
- package/scripts/migrate-legacy-learnings.sh +22 -0
- package/scripts/migrate-sessions-jsonl.mjs +16 -2
- package/scripts/migrate-vault-paths.mjs +29 -13
- package/scripts/parse-config.mjs +71 -14
- package/scripts/print-learnings-index.mjs +25 -2
- package/scripts/release-session-lock.mjs +94 -7
- package/scripts/release.mjs +451 -24
- package/scripts/relocate-vault-corpus.mjs +11 -7
- package/scripts/repair-invalid-sessions.mjs +68 -3
- package/scripts/resolve-session-invocation.mjs +57 -8
- package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
- package/scripts/run-quality-gate.mjs +33 -7
- package/scripts/self-update.mjs +56 -7
- package/scripts/session-commit-files.mjs +133 -0
- package/scripts/site-numbers.mjs +52 -9
- package/scripts/snapshot-probe.mjs +156 -0
- package/scripts/so-hud.mjs +68 -0
- package/scripts/state-md-idle-reset.mjs +82 -0
- package/scripts/sweep-expired-learnings.mjs +54 -6
- package/scripts/sweep-expired-rules.mjs +4 -1
- package/scripts/validate-plugin.mjs +22 -0
- package/scripts/vault-integration-watcher.mjs +54 -6
- package/scripts/vault-mirror.mjs +9 -0
- package/scripts/vault-orphans.mjs +171 -0
- package/skills/_shared/fleet-protocol.md +166 -3
- package/skills/_shared/instruction-file-resolution.md +1 -1
- package/skills/_shared/monitor-patterns.md +44 -25
- package/skills/_shared/platform-tools.md +7 -0
- package/skills/_shared/state-ownership.md +5 -4
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/autopilot/SKILL.md +3 -2
- package/skills/bootstrap/SKILL.md +3 -1
- package/skills/bootstrap/_shared-template.md +4 -4
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +1 -1
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
- package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
- package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
- package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
- package/skills/claude-md-drift-check/SKILL.md +3 -3
- package/skills/claude-md-drift-check/checker.mjs +61 -101
- package/skills/close/SKILL.md +20 -7
- package/skills/discovery/probes/docs-staleness.mjs +7 -5
- package/skills/discovery/probes/frontend-slop.mjs +7 -3
- package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
- package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
- package/skills/discovery/probes/vault-staleness.mjs +5 -3
- package/skills/discovery/probes-session.md +3 -0
- package/skills/discovery/probes-vault.md +2 -0
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eval/SKILL.md +3 -1
- package/skills/evolve/SKILL.md +2 -0
- package/skills/evolve/references/evolve-analyze-mode.md +3 -3
- package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
- package/skills/gitlab-ops/SKILL.md +7 -6
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/go/SKILL.md +13 -1
- package/skills/npm-publish/SKILL.md +5 -2
- package/skills/plan/SKILL.md +23 -10
- package/skills/plan/mode-feature.md +13 -8
- package/skills/plan/mode-new.md +6 -14
- package/skills/plan/prd-feature-template.md +19 -0
- package/skills/plan/prd-full-template.md +12 -5
- package/skills/plan/prd-premortem-panel.md +48 -0
- package/skills/plan/prd-reviewer-prompt.md +11 -3
- package/skills/plan/soul.md +1 -1
- package/skills/portfolio/SKILL.md +5 -6
- package/skills/quality-gates/SKILL.md +2 -4
- package/skills/reconcile/SKILL.md +27 -4
- package/skills/release/SKILL.md +16 -8
- package/skills/session-end/SKILL.md +97 -13
- package/skills/session-end/phase-3-2-docs-verification.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +11 -7
- package/skills/session-end/phase-3-7a-recommendations.md +9 -2
- package/skills/session-end/plan-verification.md +16 -1
- package/skills/session-end/references/phase-3-7e-vault-orphans.md +63 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +25 -13
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +8 -0
- package/skills/session-end/session-metrics-write.md +7 -4
- package/skills/session-plan/SKILL.md +5 -3
- package/skills/session-start/SKILL.md +10 -4
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-7-5-mode-selector.md +3 -1
- package/skills/session-start/phase-8-5-express-path.md +4 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
- package/skills/session-start/references/phase-1-5-session-continuity.md +13 -5
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
- package/skills/tmux-layout/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +3 -2
- package/skills/wave-executor/SKILL.md +5 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
- package/skills/wave-executor/references/wave-loop-review.md +13 -5
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
- package/AGENTS.md +0 -207
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -14,22 +14,23 @@
|
|
|
14
14
|
* G3 file_path present
|
|
15
15
|
* G4 file extension filter — only .ts|.tsx|.js|.jsx|.mjs|.cjs
|
|
16
16
|
* G5 gate check — wave-scope.json.gates['post-edit-validate'] === false → exit 0
|
|
17
|
-
* G6 resolve typecheck command: Session Config → tsgo → tsc →
|
|
18
|
-
* G7 run typecheck with 2s
|
|
17
|
+
* G6 resolve typecheck command: Session Config → tsgo → tsc → local tsc → skip
|
|
18
|
+
* G7 run typecheck with 2s process-group timeout
|
|
19
19
|
* G8 emit JSONL result to stderr
|
|
20
20
|
*
|
|
21
|
-
* Output (stderr): {"check":"typecheck","status":"pass|fail|skip","file":"<rel>","duration_ms":<int>}
|
|
21
|
+
* Output (stderr): {"check":"typecheck","status":"pass|fail|timeout|skip","file":"<rel>","duration_ms":<int>}
|
|
22
22
|
* Exit codes: 0 — always (PostToolUse hooks are informational)
|
|
23
23
|
*
|
|
24
24
|
* SECURITY notes:
|
|
25
25
|
* REQ-01 top-level try/catch → silent exit 0 on unexpected error (never blocks)
|
|
26
|
-
* REQ-02 typecheck
|
|
26
|
+
* REQ-02 typecheck runs the intact shell command; timeout kills its process tree
|
|
27
27
|
* REQ-03 relative path computed via path.relative(projectRoot, absFilePath)
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import path from 'node:path';
|
|
31
|
+
import { existsSync } from 'node:fs';
|
|
31
32
|
import { spawnSync } from 'node:child_process';
|
|
32
|
-
import {
|
|
33
|
+
import { spawnInGroup } from '../scripts/lib/process-group.mjs';
|
|
33
34
|
|
|
34
35
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
35
36
|
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
@@ -54,9 +55,9 @@ const TS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
|
54
55
|
/**
|
|
55
56
|
* Emit the JSONL result line to stderr.
|
|
56
57
|
* @param {string} file - relative file path
|
|
57
|
-
* @param {'pass'|'fail'|'skip'} status
|
|
58
|
+
* @param {'pass'|'fail'|'timeout'|'skip'} status
|
|
58
59
|
* @param {number} [duration_ms] - omit for skip
|
|
59
|
-
* @param {string} [reason] - human-readable reason for skip or fail
|
|
60
|
+
* @param {string} [reason] - human-readable reason for skip, timeout or fail
|
|
60
61
|
* @param {string} [remediation] - actionable fix hint for fail status only
|
|
61
62
|
*/
|
|
62
63
|
function emitResult(file, status, duration_ms, reason, remediation) {
|
|
@@ -69,10 +70,10 @@ function emitResult(file, status, duration_ms, reason, remediation) {
|
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
72
|
* Try to resolve the typecheck command.
|
|
72
|
-
* Order: Session Config typecheck-command → tsgo → tsc →
|
|
73
|
+
* Order: Session Config typecheck-command → tsgo → tsc → local tsc → null
|
|
73
74
|
*
|
|
74
75
|
* @param {string} projectRoot
|
|
75
|
-
* @returns {Promise<
|
|
76
|
+
* @returns {Promise<string|string[]|null>}
|
|
76
77
|
*/
|
|
77
78
|
async function resolveTypecheckCommand(projectRoot) {
|
|
78
79
|
// 1. Session Config typecheck-command
|
|
@@ -81,8 +82,8 @@ async function resolveTypecheckCommand(projectRoot) {
|
|
|
81
82
|
const config = parseSessionConfig(md);
|
|
82
83
|
const cmd = config['typecheck-command'];
|
|
83
84
|
if (cmd && typeof cmd === 'string') {
|
|
84
|
-
const
|
|
85
|
-
return
|
|
85
|
+
const command = cmd.trim();
|
|
86
|
+
return command === 'skip' ? null : command || null;
|
|
86
87
|
}
|
|
87
88
|
} catch {
|
|
88
89
|
// config unavailable — fall through
|
|
@@ -90,15 +91,16 @@ async function resolveTypecheckCommand(projectRoot) {
|
|
|
90
91
|
|
|
91
92
|
// 2. tsgo
|
|
92
93
|
const tsgoParts = _tryWhich('tsgo');
|
|
93
|
-
if (tsgoParts) return
|
|
94
|
+
if (tsgoParts) return 'tsgo --noEmit';
|
|
94
95
|
|
|
95
96
|
// 3. tsc
|
|
96
97
|
const tscParts = _tryWhich('tsc');
|
|
97
|
-
if (tscParts) return
|
|
98
|
+
if (tscParts) return 'tsc --noEmit';
|
|
98
99
|
|
|
99
|
-
// 4. npx
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// 4. Only an installed project compiler; npx may install the unrelated
|
|
101
|
+
// npm package `tsc` in projects without TypeScript (#1612).
|
|
102
|
+
const localTsc = path.join(projectRoot, 'node_modules', 'typescript', 'bin', 'tsc');
|
|
103
|
+
if (existsSync(localTsc)) return [process.execPath, localTsc, '--noEmit'];
|
|
102
104
|
|
|
103
105
|
return null;
|
|
104
106
|
}
|
|
@@ -111,9 +113,11 @@ async function resolveTypecheckCommand(projectRoot) {
|
|
|
111
113
|
* @returns {string|null}
|
|
112
114
|
*/
|
|
113
115
|
function _tryWhich(name) {
|
|
114
|
-
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
116
|
+
const whichCmd = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
117
|
+
// Two lookups get at most 1s total; revisit together with the outer
|
|
118
|
+
// 5s hook budget if a host needs slower executable discovery.
|
|
115
119
|
try {
|
|
116
|
-
const result = spawnSync(whichCmd, [name], { encoding: 'utf8', timeout:
|
|
120
|
+
const result = spawnSync(whichCmd, [name], { encoding: 'utf8', timeout: 500 });
|
|
117
121
|
if (result.status === 0 && result.stdout.trim()) return name;
|
|
118
122
|
} catch {
|
|
119
123
|
// ignore
|
|
@@ -122,43 +126,32 @@ function _tryWhich(name) {
|
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* @param {string} cmd
|
|
128
|
-
* @param {string
|
|
129
|
-
* @returns {{status: 'pass'|'fail', duration_ms: number, reason?: string}}
|
|
129
|
+
* Configured commands retain shell semantics; the local compiler uses Node argv.
|
|
130
|
+
* Keep cleanup short for hooks.json's 5s budget; Windows skips CIM verification.
|
|
131
|
+
* @param {string|string[]} cmd
|
|
132
|
+
* @param {string} projectRoot
|
|
133
|
+
* @returns {Promise<{status: 'pass'|'fail'|'timeout', duration_ms: number, reason?: string}>}
|
|
130
134
|
*/
|
|
131
|
-
function runTypecheck(cmd,
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const duration_ms = Math.max(0, Math.round(performance.now() - start));
|
|
149
|
-
|
|
150
|
-
// AbortError or SIGTERM from timeout
|
|
151
|
-
if (spawnResult.error?.name === 'AbortError' || spawnResult.signal === 'SIGTERM') {
|
|
152
|
-
return { status: 'fail', duration_ms, reason: 'typecheck timed out after 2s' };
|
|
135
|
+
async function runTypecheck(cmd, projectRoot) {
|
|
136
|
+
const result = await spawnInGroup(cmd, {
|
|
137
|
+
cwd: projectRoot,
|
|
138
|
+
timeoutMs: 2000,
|
|
139
|
+
// Hook-specific ceiling: escalate quickly so an uncooperative compiler
|
|
140
|
+
// cannot consume the outer 5s hook budget. Revisit if compiler cleanup
|
|
141
|
+
// needs more than 100ms; the hook's outer budget must change with it.
|
|
142
|
+
killGraceMs: 100,
|
|
143
|
+
verifyWaitMs: 100,
|
|
144
|
+
// Two PowerShell startups would exceed the remaining hook budget (R1-04).
|
|
145
|
+
verifyTree: false,
|
|
146
|
+
});
|
|
147
|
+
const duration_ms = Math.max(0, Math.round(result.durationMs));
|
|
148
|
+
if (result.timedOut) {
|
|
149
|
+
return { status: 'timeout', duration_ms, reason: 'typecheck timed out after 2s' };
|
|
153
150
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// Capture the first non-empty line of stderr/stdout as a compact reason.
|
|
157
|
-
const combined = ((spawnResult.stderr ?? '') + (spawnResult.stdout ?? '')).trim();
|
|
158
|
-
const firstLine = combined.split('\n').find((l) => l.trim()) ?? 'typecheck exited non-zero';
|
|
151
|
+
if (result.exitCode !== 0) {
|
|
152
|
+
const firstLine = result.fullOutput.split('\n').find((line) => line.trim()) ?? 'typecheck exited non-zero';
|
|
159
153
|
return { status: 'fail', duration_ms, reason: firstLine.slice(0, 256) };
|
|
160
154
|
}
|
|
161
|
-
|
|
162
155
|
return { status: 'pass', duration_ms };
|
|
163
156
|
}
|
|
164
157
|
|
|
@@ -216,7 +209,7 @@ async function main() {
|
|
|
216
209
|
}
|
|
217
210
|
|
|
218
211
|
// G7: run typecheck with 2s timeout (REQ-02)
|
|
219
|
-
const { status, duration_ms, reason } = runTypecheck(tc
|
|
212
|
+
const { status, duration_ms, reason } = await runTypecheck(tc, projectRoot);
|
|
220
213
|
|
|
221
214
|
// G8: emit JSONL result to stderr
|
|
222
215
|
const remediation =
|
|
@@ -226,7 +226,7 @@ const AGENT_DESCRIPTION_MAX = 120;
|
|
|
226
226
|
* Resolve WHO made the claim, and say where the answer came from (#1218).
|
|
227
227
|
*
|
|
228
228
|
* Measured 2026-09-06 over the fleet's worst-affected repo
|
|
229
|
-
* (
|
|
229
|
+
* (a consumer repo, 3,360 `discovery_validator_violation`
|
|
230
230
|
* records): `agent` was the literal string `"unknown"` on 3,026 of them
|
|
231
231
|
* (90.1%), and NOT ONE record carried an `agent_id`. Both the sidecar-meta
|
|
232
232
|
* fallback and the `agent_id` field landed in the same commit (936dae8a,
|
|
@@ -364,7 +364,7 @@ function dedupSentinelPath({ projectRoot, sessionId, agentId, claimText }) {
|
|
|
364
364
|
* same sentence re-asserted by a second agent of the same session is the
|
|
365
365
|
* same finding, and writing it twice is what produced a duplication factor
|
|
366
366
|
* of 16.4 (3,360 records over 205 distinct `claim_text` values, measured
|
|
367
|
-
* 2026-09-06 in
|
|
367
|
+
* 2026-09-06 in a consumer repo).
|
|
368
368
|
*
|
|
369
369
|
* Returns null without a session id — a claim that cannot be bound to a session
|
|
370
370
|
* must not collide with an unrelated one under a global "unknown" key, so it is
|
|
@@ -74,6 +74,7 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
|
74
74
|
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
75
75
|
|
|
76
76
|
import { getProjectDir, resolveSessionRoot } from '../scripts/lib/platform.mjs';
|
|
77
|
+
import { resolveOwnLockRoot } from './_lib/hook-root.mjs';
|
|
77
78
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
78
79
|
import { findOwnScopeFile } from '../scripts/lib/scope-gate.mjs';
|
|
79
80
|
import {
|
|
@@ -356,13 +357,16 @@ async function main() {
|
|
|
356
357
|
};
|
|
357
358
|
|
|
358
359
|
// Two roots, by writer (#1511 point a). `current-session.json` and
|
|
359
|
-
// `session.lock`
|
|
360
|
-
//
|
|
360
|
+
// `session.lock` are read where on-session-start.mjs wrote them: the launch
|
|
361
|
+
// dir, or the entered worktree after a /clear, compact or resume there
|
|
362
|
+
// (#1605), found by the lock carrying this session's raw id
|
|
363
|
+
// (`resolveOwnLockRoot`); a reader elsewhere finds no record. Everything
|
|
361
364
|
// about the WAVE — its manifest, its start sha, the files it changed — is read
|
|
362
365
|
// at the SESSION root: after EnterWorktree the launch dir is a different
|
|
363
366
|
// checkout, whose HEAD and working tree say nothing about this wave.
|
|
364
367
|
const sessionRoot = resolveSessionRoot(input?.cwd);
|
|
365
|
-
const
|
|
368
|
+
const lockRoot = resolveOwnLockRoot(input, getProjectDir());
|
|
369
|
+
const sessionFile = path.join(lockRoot, '.orchestrator', 'current-session.json');
|
|
366
370
|
|
|
367
371
|
const lastBatchResult = await atomicMutateJson(sessionFile, {}, (current) => ({
|
|
368
372
|
...current,
|
|
@@ -391,7 +395,7 @@ async function main() {
|
|
|
391
395
|
const sessionId = await resolveSessionIdForHeartbeat(input, sessionFile);
|
|
392
396
|
if (typeof sessionId === 'string' && sessionId.length > 0) {
|
|
393
397
|
const { updateHeartbeat } = await import('../scripts/lib/session-lock.mjs');
|
|
394
|
-
updateHeartbeat({ repoRoot:
|
|
398
|
+
updateHeartbeat({ repoRoot: lockRoot, sessionId });
|
|
395
399
|
}
|
|
396
400
|
} catch { /* best effort — hook must remain non-blocking */ }
|
|
397
401
|
|
|
@@ -31,6 +31,7 @@ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
|
31
31
|
|
|
32
32
|
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
33
33
|
import { atomicMutateJson } from './_lib/atomic-json.mjs';
|
|
34
|
+
import { resolveOwnLockRoot } from './_lib/hook-root.mjs';
|
|
34
35
|
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
36
37
|
// Constants
|
|
@@ -178,7 +179,10 @@ async function main() {
|
|
|
178
179
|
exit_code: exitCode,
|
|
179
180
|
};
|
|
180
181
|
|
|
181
|
-
|
|
182
|
+
// #1605 B5 — annotate THIS session's current-session.json where
|
|
183
|
+
// on-session-start.mjs put it (the entered worktree after a /clear there),
|
|
184
|
+
// found by the lock carrying the raw payload id; launch dir otherwise.
|
|
185
|
+
const sessionFile = path.join(resolveOwnLockRoot(input, getProjectDir()), '.orchestrator', 'current-session.json');
|
|
182
186
|
|
|
183
187
|
const result = await atomicMutateJson(sessionFile, {}, (current) => {
|
|
184
188
|
const existing = Array.isArray(current.corrective_context)
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
* redirect class answers "what does this command WRITE"; nothing
|
|
36
36
|
* answered "what does it REMOVE", so `rm -f <ledger>` was allowed
|
|
37
37
|
* while `> <ledger>` was denied — 53,896 lines of telemetry lost.
|
|
38
|
+
* rule.type === 'hooks-path-missing' (#1732): decided by the directory
|
|
39
|
+
* of each `git commit`/`git push` (findHookedGitCalls) and the repo's
|
|
40
|
+
* relative core.hooksPath (missingWorktreeHooks) — a linked worktree
|
|
41
|
+
* without the generated hooks dir runs no hook at all, silently.
|
|
38
42
|
* G6 no match → exit 0
|
|
39
43
|
*
|
|
40
44
|
* Telemetry (Epic #803 process-safety dimension): best-effort
|
|
@@ -45,6 +49,7 @@
|
|
|
45
49
|
* finalized; a telemetry failure never changes the guard's decision.
|
|
46
50
|
*/
|
|
47
51
|
|
|
52
|
+
import { execFileSync } from 'node:child_process';
|
|
48
53
|
import fs from 'node:fs';
|
|
49
54
|
import os from 'node:os';
|
|
50
55
|
import path from 'node:path';
|
|
@@ -326,7 +331,7 @@ async function blockCommand(pattern, ruleId, rationale, command, sessionId) {
|
|
|
326
331
|
*/
|
|
327
332
|
async function isGitStashNonEmpty(projectDir) {
|
|
328
333
|
try {
|
|
329
|
-
const { $: zx$ } = await import('zx');
|
|
334
|
+
const { $: zx$ } = await import('../scripts/lib/zx.mjs');
|
|
330
335
|
zx$.verbose = false;
|
|
331
336
|
zx$.quiet = true;
|
|
332
337
|
const cwd = projectDir || process.cwd();
|
|
@@ -479,6 +484,181 @@ function commandHasRecursiveForceRm(command) {
|
|
|
479
484
|
return false;
|
|
480
485
|
}
|
|
481
486
|
|
|
487
|
+
/** #1732 — git subcommands that run the repo's own hooks: pre-commit/commit-msg and pre-push. */
|
|
488
|
+
const HOOKED_GIT_SUBCOMMANDS = new Set(['commit', 'push']);
|
|
489
|
+
/** git global options that take their value as the NEXT token (`git -c k=v push`). */
|
|
490
|
+
const GIT_OPTIONS_WITH_VALUE = new Set(['-c', '--namespace', '--config-env', '--attr-source']);
|
|
491
|
+
/** Options and env prefixes after which the guard cannot tell which repo or hooks git uses. */
|
|
492
|
+
const GIT_UNKNOWABLE_OPTION = /^--(git-dir|work-tree)(=|$)/;
|
|
493
|
+
const GIT_UNKNOWABLE_ENV = /^GIT_(DIR|WORK_TREE|COMMON_DIR|CONFIG\w*)=/;
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Resolve a literal path operand against `base`, or null when the shell would
|
|
497
|
+
* expand it into something the guard cannot read (`$X`, a substitution, `cd -`).
|
|
498
|
+
*
|
|
499
|
+
* @param {string|null} base
|
|
500
|
+
* @param {{ text: string } | undefined} tok
|
|
501
|
+
* @returns {string|null}
|
|
502
|
+
*/
|
|
503
|
+
function literalDir(base, tok) {
|
|
504
|
+
if (base === null || tok === undefined) return null;
|
|
505
|
+
const text = tok.text;
|
|
506
|
+
if (text === '-' || /[$`*?[]/.test(text)) return null;
|
|
507
|
+
if (text === '~' || text.startsWith('~/')) return path.join(os.homedir(), text.slice(1));
|
|
508
|
+
if (text.startsWith('~')) return null;
|
|
509
|
+
return path.resolve(base, text);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Every `git commit` / `git push` in the command with the directory it runs in
|
|
514
|
+
* (#1732). The directory starts at `cwd` and follows literal `cd <dir>` segments
|
|
515
|
+
* and `git -C <dir>`. A `cd` the guard cannot read (variable, `cd -`,
|
|
516
|
+
* `pushd`/`popd`) turns the directory unknown (`dir: null`) for everything after
|
|
517
|
+
* it. The tokenizer appends command-substitution bodies after a `substitution`
|
|
518
|
+
* marker and frames `( … )` with `subshell` markers, so only the tokens before
|
|
519
|
+
* the first substitution marker are the main shell's, and a `cd` after the
|
|
520
|
+
* first subshell marker turns the directory unknown (`(cd /wt && make) && git
|
|
521
|
+
* push` pushes from the start directory). A `)` inside a quoted commit message
|
|
522
|
+
* changes nothing. The caller never judges an unknown directory — unknown is
|
|
523
|
+
* "not judged", never a hit. An explicit skip (`--no-verify`, `commit -n` also
|
|
524
|
+
* inside a short-flag group, `HUSKY=0`) is not judged either: the skip this rule
|
|
525
|
+
* catches is the silent one.
|
|
526
|
+
*
|
|
527
|
+
* @param {string} command
|
|
528
|
+
* @param {string} cwd
|
|
529
|
+
* @returns {Array<{ subcommand: string, dir: string|null }>}
|
|
530
|
+
*/
|
|
531
|
+
function findHookedGitCalls(command, cwd) {
|
|
532
|
+
const calls = [];
|
|
533
|
+
const tokens = blocker.tokenizeCommand(command);
|
|
534
|
+
const substitutionAt = tokens.findIndex((t) => t.operator === 'substitution');
|
|
535
|
+
const mainShell = substitutionAt === -1 ? tokens : tokens.slice(0, substitutionAt);
|
|
536
|
+
const subshellAt = mainShell.findIndex((t) => t.operator === 'subshell');
|
|
537
|
+
const parts = subshellAt === -1
|
|
538
|
+
? [{ tokens: mainShell, cdKnown: true }]
|
|
539
|
+
: [{ tokens: mainShell.slice(0, subshellAt), cdKnown: true }, { tokens: mainShell.slice(subshellAt), cdKnown: false }];
|
|
540
|
+
let dir = cwd;
|
|
541
|
+
for (const part of parts) {
|
|
542
|
+
for (const segment of blocker.splitChainSegments(part.tokens)) {
|
|
543
|
+
const { verb, index } = blocker.resolveSegmentVerb(segment);
|
|
544
|
+
if (verb === 'pushd' || verb === 'popd') { dir = null; continue; }
|
|
545
|
+
if (verb === 'cd') {
|
|
546
|
+
const operands = segment.slice(index + 1).filter((t) => t.quoted || !/^-[LPe@]+$/.test(t.text));
|
|
547
|
+
if (!part.cdKnown) dir = null;
|
|
548
|
+
else if (operands.length === 0) dir = os.homedir();
|
|
549
|
+
else dir = operands.length === 1 ? literalDir(dir, operands[0]) : null;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (verb !== 'git' || segment[index].quoted) continue;
|
|
553
|
+
const call = hookedGitCall(segment, index, dir);
|
|
554
|
+
if (call !== null) calls.push(call);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return calls;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Short `git commit` options that take a value: in `-mn` the `n` is the message. */
|
|
561
|
+
const COMMIT_SHORT_WITH_VALUE = 'mFCctSu';
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Whether a short-flag group of `git commit` carries `-n` (`-nm x` does, `-mn` does not).
|
|
565
|
+
*
|
|
566
|
+
* @param {string} group
|
|
567
|
+
* @returns {boolean}
|
|
568
|
+
*/
|
|
569
|
+
function commitGroupHasNoVerify(group) {
|
|
570
|
+
for (const ch of group.slice(1)) {
|
|
571
|
+
if (ch === 'n') return true;
|
|
572
|
+
if (COMMIT_SHORT_WITH_VALUE.includes(ch)) return false;
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Read one `git` segment: its subcommand and the directory it runs in, or null
|
|
579
|
+
* when it is no hooked call or skips hooks explicitly.
|
|
580
|
+
*
|
|
581
|
+
* @param {Array<{ text: string, quoted: boolean }>} segment
|
|
582
|
+
* @param {number} index - the `git` token
|
|
583
|
+
* @param {string|null} dir - the directory before git's own `-C`
|
|
584
|
+
* @returns {{ subcommand: string, dir: string|null } | null}
|
|
585
|
+
*/
|
|
586
|
+
function hookedGitCall(segment, index, dir) {
|
|
587
|
+
const env = segment.slice(0, index);
|
|
588
|
+
if (env.some((t) => t.text === 'HUSKY=0')) return null;
|
|
589
|
+
let gitDir = env.some((t) => GIT_UNKNOWABLE_ENV.test(t.text)) ? null : dir;
|
|
590
|
+
let i = index + 1;
|
|
591
|
+
for (; i < segment.length; i++) {
|
|
592
|
+
const t = segment[i];
|
|
593
|
+
if (t.quoted || !t.text.startsWith('-')) break;
|
|
594
|
+
if (t.text === '-C') { gitDir = literalDir(gitDir, segment[++i]); continue; }
|
|
595
|
+
if (GIT_UNKNOWABLE_OPTION.test(t.text)) {
|
|
596
|
+
gitDir = null;
|
|
597
|
+
if (!t.text.includes('=')) i++;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (GIT_OPTIONS_WITH_VALUE.has(t.text)) {
|
|
601
|
+
const value = segment[++i]?.text ?? '';
|
|
602
|
+
if (t.text === '-c' && /^core\.hookspath=/i.test(value)) gitDir = null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const sub = segment[i];
|
|
606
|
+
if (sub === undefined || sub.quoted || !HOOKED_GIT_SUBCOMMANDS.has(sub.text)) return null;
|
|
607
|
+
const flags = segment.slice(i + 1).filter((t) => !t.quoted).map((t) => t.text);
|
|
608
|
+
if (flags.includes('--no-verify')) return null;
|
|
609
|
+
if (sub.text === 'commit' && flags.some((f) => /^-[a-zA-Z]+$/.test(f) && commitGroupHasNoVerify(f))) return null;
|
|
610
|
+
return { subcommand: sub.text, dir: gitDir };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Run git read-only in `dir`; trimmed stdout, or null on any failure.
|
|
615
|
+
*
|
|
616
|
+
* @param {string} dir
|
|
617
|
+
* @param {string[]} args
|
|
618
|
+
* @returns {string|null}
|
|
619
|
+
*/
|
|
620
|
+
function gitRead(dir, args) {
|
|
621
|
+
try {
|
|
622
|
+
const out = execFileSync('git', ['-C', dir, ...args], {
|
|
623
|
+
encoding: 'utf8',
|
|
624
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
625
|
+
timeout: 3000,
|
|
626
|
+
}).trim();
|
|
627
|
+
return out.length > 0 ? out : null;
|
|
628
|
+
} catch {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* The silent hookless case of #1732: a LINKED worktree whose relative
|
|
635
|
+
* `core.hooksPath` (husky: `.husky/_`) does not exist in it, while the main
|
|
636
|
+
* checkout has it. `.husky/_` is generated by `husky` (npm `prepare`), and
|
|
637
|
+
* `ignore-scripts=true` keeps `npm ci` from creating it in a fresh worktree, so
|
|
638
|
+
* every commit and push there runs no hook and says nothing (measured
|
|
639
|
+
* 2026-10-22: a push went out without its pre-push full gate).
|
|
640
|
+
*
|
|
641
|
+
* Narrow on purpose — each condition that does not hold is an allow: no
|
|
642
|
+
* hooksPath, an absolute or `~` one (a global setting), the directory exists, the
|
|
643
|
+
* main checkout has none either (the repo installs no hooks), or this IS the main
|
|
644
|
+
* checkout. Any git failure is an allow.
|
|
645
|
+
*
|
|
646
|
+
* @param {string} dir
|
|
647
|
+
* @returns {{ hooksPath: string, worktree: string } | null}
|
|
648
|
+
*/
|
|
649
|
+
function missingWorktreeHooks(dir) {
|
|
650
|
+
const hooksPath = gitRead(dir, ['config', '--get', 'core.hooksPath']);
|
|
651
|
+
if (hooksPath === null || path.isAbsolute(hooksPath) || hooksPath.startsWith('~')) return null;
|
|
652
|
+
const worktree = gitRead(dir, ['rev-parse', '--show-toplevel']);
|
|
653
|
+
if (worktree === null || fs.existsSync(path.join(worktree, hooksPath))) return null;
|
|
654
|
+
const commonDir = gitRead(dir, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
655
|
+
if (commonDir === null) return null;
|
|
656
|
+
const mainCheckout = path.dirname(commonDir);
|
|
657
|
+
if (path.resolve(mainCheckout) === path.resolve(worktree)) return null;
|
|
658
|
+
if (!fs.existsSync(path.join(mainCheckout, hooksPath))) return null;
|
|
659
|
+
return { hooksPath, worktree };
|
|
660
|
+
}
|
|
661
|
+
|
|
482
662
|
/** Probe operator per redirect mode — see findMatchedRedirectEntry. */
|
|
483
663
|
const REDIRECT_PROBE_OPS = { truncate: '>', append: '>>', read: '<' };
|
|
484
664
|
|
|
@@ -939,6 +1119,31 @@ async function main() {
|
|
|
939
1119
|
continue; // unreachable (blockCommand never returns) — kept for clarity
|
|
940
1120
|
}
|
|
941
1121
|
|
|
1122
|
+
// #1732 — a `git commit`/`git push` that would run WITHOUT the repo's hooks,
|
|
1123
|
+
// because a linked worktree lacks the generated hooks directory. Decided by
|
|
1124
|
+
// the git call's directory and the repo's hooksPath, never by the generic
|
|
1125
|
+
// pattern path below (its `pattern` is a label). An unknown directory is
|
|
1126
|
+
// not judged.
|
|
1127
|
+
if (rule.type === 'hooks-path-missing') {
|
|
1128
|
+
const cwd = typeof input.cwd === 'string' && input.cwd.length > 0 ? input.cwd : process.cwd();
|
|
1129
|
+
let hit = null;
|
|
1130
|
+
for (const call of findHookedGitCalls(command, cwd)) {
|
|
1131
|
+
if (call.dir === null) continue;
|
|
1132
|
+
const missing = missingWorktreeHooks(call.dir);
|
|
1133
|
+
if (missing) { hit = { ...missing, subcommand: call.subcommand }; break; }
|
|
1134
|
+
}
|
|
1135
|
+
if (!hit) continue;
|
|
1136
|
+
const label = `git ${hit.subcommand} in ${hit.worktree} — core.hooksPath ${hit.hooksPath} is missing there`;
|
|
1137
|
+
if (severity !== 'block') {
|
|
1138
|
+
const msg = `pre-bash-destructive-guard: ${label} (rule: ${id}) — ${rationale}`;
|
|
1139
|
+
process.stderr.write(`⚠ ${msg}\n`);
|
|
1140
|
+
notices.push(msg);
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
await blockCommand(label, id, rationale, command, sessionId);
|
|
1144
|
+
continue; // unreachable (blockCommand never returns) — kept for clarity
|
|
1145
|
+
}
|
|
1146
|
+
|
|
942
1147
|
// The rm-rf-destructive rule also fires for recursive+force rm flag variants
|
|
943
1148
|
// the literal "rm -rf" pattern misses (`rm -r -f`, `rm -fr`) — #641 gap closure.
|
|
944
1149
|
const matched = id === 'rm-rf-destructive'
|
|
@@ -1020,9 +1225,13 @@ async function main() {
|
|
|
1020
1225
|
// crash with 0 bytes of stdout — indistinguishable from an allow, and
|
|
1021
1226
|
// therefore invisible. Now it exits 0 (still fail-open, so a broken module
|
|
1022
1227
|
// cannot brick the session) but SAYS SO, loudly, once per session.
|
|
1023
|
-
// 2. RUNTIME failure inside `main()
|
|
1024
|
-
//
|
|
1025
|
-
//
|
|
1228
|
+
// 2. RUNTIME failure inside `main()` (#1671 item 1): the guard armed and then
|
|
1229
|
+
// tripped over a specific command. That command is DENIED (fail-closed),
|
|
1230
|
+
// like enforce-commands / enforce-scope (SECURITY-REQ-01). It used to exit
|
|
1231
|
+
// 0 = ALLOW, so any future crash on an input shape (the measured trigger was
|
|
1232
|
+
// a tokenizer RangeError on a long command) silently opened the guard for
|
|
1233
|
+
// exactly the inputs it cannot parse. The blast radius is one command, and
|
|
1234
|
+
// the reason tells the operator how out: split or simplify it.
|
|
1026
1235
|
// ---------------------------------------------------------------------------
|
|
1027
1236
|
try {
|
|
1028
1237
|
await bootstrap();
|
|
@@ -1054,11 +1263,14 @@ if (isMainModule(import.meta.url)) {
|
|
|
1054
1263
|
// #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
|
|
1055
1264
|
if (!shouldRunHook('pre-bash-destructive-guard')) process.exit(0);
|
|
1056
1265
|
|
|
1057
|
-
// Top-level error handler — never let exit 1 leak
|
|
1266
|
+
// Top-level error handler — never let exit 1 leak, never fail open (#1671).
|
|
1267
|
+
// emitDeny is bound here: bootstrap() succeeded, and it never throws.
|
|
1058
1268
|
main().catch((e) => {
|
|
1059
|
-
|
|
1060
|
-
|
|
1269
|
+
const detail = String(e?.message || e).split('\n')[0];
|
|
1270
|
+
process.stderr.write(`⚠ pre-bash-destructive-guard: internal error — ${detail}\n`);
|
|
1271
|
+
emitDeny(
|
|
1272
|
+
`pre-bash-destructive-guard hit an internal error and blocked this command for safety (${detail}).`,
|
|
1273
|
+
'Split the command into smaller, simpler Bash calls and retry. See issue #1671.'
|
|
1061
1274
|
);
|
|
1062
|
-
process.exit(0); // fail-open on internal errors to avoid blocking legitimate work
|
|
1063
1275
|
});
|
|
1064
1276
|
}
|
|
@@ -106,7 +106,12 @@ async function resolveSessionId(input, projectDir) {
|
|
|
106
106
|
|
|
107
107
|
let currentSession = null;
|
|
108
108
|
if (projectDir) {
|
|
109
|
-
|
|
109
|
+
// #1605 — read THIS session's record where on-session-start.mjs wrote it
|
|
110
|
+
// (the entered worktree after a /clear there), found by the lock carrying
|
|
111
|
+
// the raw id. Lazy: only a matched issue command pays for the lookup.
|
|
112
|
+
const { resolveOwnLockRoot } = await import('./_lib/hook-root.mjs');
|
|
113
|
+
const ownRoot = resolveOwnLockRoot({ session_id: nativeRawId, cwd: input?.cwd }, projectDir);
|
|
114
|
+
const persisted = path.join(ownRoot, '.orchestrator', 'current-session.json');
|
|
110
115
|
if (existsSync(persisted)) {
|
|
111
116
|
try {
|
|
112
117
|
currentSession = await readJson(persisted);
|