session-orchestrator 5.2.0 → 5.4.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/.agents/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/navigator/SKILL.md +22 -0
- package/.agents/skills/navigator/agents/openai.yaml +5 -0
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/navigator/SKILL.md +22 -0
- package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/navigator.md +15 -0
- package/.cursor/commands/session.md +1 -1
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/rules/060-evolve.mdc +27 -12
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/navigator/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +9 -30
- package/CHANGELOG.md +206 -0
- package/README.md +13 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +22 -7
- package/docs/ci-setup.md +57 -4
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +12 -10
- package/docs/events-schema.md +65 -15
- package/docs/persona-panel.md +5 -4
- package/docs/rule-authoring.md +84 -17
- package/docs/scope-collision-guard.md +26 -4
- package/docs/session-config-reference.md +154 -12
- package/docs/session-config-template.md +41 -3
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +87 -6
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +101 -11
- package/hooks/on-stop.mjs +170 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +202 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
- package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +792 -74
- package/hooks/subagent-telemetry.mjs +111 -38
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +3 -3
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/navigator.md +12 -0
- package/pi/prompts/session.md +1 -1
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +7 -2
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +6 -2
- package/rules/opt-in-stack/backend-data.md +10 -2
- package/rules/opt-in-stack/backend.md +10 -4
- package/rules/opt-in-stack/frontend.md +7 -2
- package/rules/opt-in-stack/security-web.md +10 -4
- package/rules/opt-in-stack/swift.md +6 -2
- package/scripts/apply-session-learnings.mjs +492 -0
- package/scripts/archive-closed-prds.mjs +2 -1
- package/scripts/autopilot.mjs +70 -53
- package/scripts/backfill-abandoned-sessions.mjs +118 -16
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/cursor-install.mjs +1 -4
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +371 -31
- package/scripts/eval-session.mjs +65 -3
- package/scripts/export-hw-learnings.mjs +22 -3
- package/scripts/fleet-checkin.mjs +147 -0
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +18 -2
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +19 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +163 -0
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +85 -10
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +545 -7
- package/scripts/lib/fleet-protocol.mjs +270 -0
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +40 -5
- package/scripts/lib/gates/gate-full.mjs +35 -5
- package/scripts/lib/gates/gate-helpers.mjs +336 -23
- package/scripts/lib/gates/gate-incremental.mjs +49 -23
- package/scripts/lib/gates/gate-per-file.mjs +42 -8
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +208 -8
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
- package/scripts/lib/learnings/io.mjs +154 -26
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +104 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
- package/scripts/lib/orphan-reaper/scan.mjs +506 -0
- package/scripts/lib/orphan-reaper.mjs +316 -0
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/process-group.mjs +950 -0
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +152 -33
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +141 -74
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +3 -4
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +27 -2
- package/scripts/lib/session-schema/constants.mjs +56 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/session-token-rollup.mjs +245 -52
- package/scripts/lib/sessions-canonical.mjs +118 -4
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/pricing.mjs +118 -71
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tests-src-ratio.mjs +140 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +210 -84
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
- package/scripts/lib/vault-status/board-writer.mjs +48 -11
- package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +306 -0
- package/scripts/release.mjs +40 -8
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
- package/scripts/run-quality-gate.mjs +357 -197
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +56 -5
- package/scripts/sweep-expired-rules.mjs +16 -4
- package/scripts/validate-plugin.mjs +19 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +81 -47
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +131 -0
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/_shared/state-ownership.md +5 -0
- package/skills/autopilot/SKILL.md +47 -18
- package/skills/bootstrap/SKILL.md +1 -1
- package/skills/bootstrap/_shared-template.md +1 -1
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +3 -3
- package/skills/evolve/references/evolve-analyze-mode.md +10 -6
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/navigator/SKILL.md +193 -0
- package/skills/navigator/references/account-check.sh +87 -0
- package/skills/navigator/references/fleet-decisions.mjs +231 -0
- package/skills/navigator/references/slot-check.sh +114 -0
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/learning-patterns.md +22 -12
- package/skills/session-end/metrics-collection.md +5 -6
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +32 -60
- package/skills/session-plan/SKILL.md +17 -8
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/SKILL.md +6 -0
- package/skills/session-start/references/operations-contract.md +4 -0
- 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 +4 -3
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +9 -5
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
- package/skills/wave-executor/references/wave-loop-review.md +3 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
|
@@ -36,10 +36,9 @@ import { randomUUID } from 'node:crypto';
|
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
|
|
38
38
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
39
|
-
// #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
|
|
40
|
-
if (!shouldRunHook('on-session-start')) process.exit(0);
|
|
41
39
|
|
|
42
40
|
import { emitEvent, eventsFilePath } from '../scripts/lib/events.mjs';
|
|
41
|
+
import { readNavigatorLease } from '../scripts/lib/fleet-protocol.mjs';
|
|
43
42
|
import { maybeRotate } from '../scripts/lib/events-rotation.mjs';
|
|
44
43
|
import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
|
|
45
44
|
import { getPlatform, resolveProjectDir } from '../scripts/lib/platform.mjs';
|
|
@@ -54,6 +53,7 @@ import { detectColdStart, consumeMarker } from '../scripts/lib/cold-start-detect
|
|
|
54
53
|
import { parseSessionId } from '../scripts/lib/session-id.mjs';
|
|
55
54
|
import { readTelemetryState, resolveConsent, isCiEnv } from '../scripts/lib/telemetry/consent.mjs';
|
|
56
55
|
import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
|
|
56
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
57
57
|
// SSOT for the "is this a re-entry into the same logical session?" question
|
|
58
58
|
// (#1091). Defined in the lock-bootstrap leaf module, which this hook already
|
|
59
59
|
// loads, so the preservation branch below and the lock force-refresh gate can
|
|
@@ -99,6 +99,38 @@ const bannerLines = [];
|
|
|
99
99
|
*/
|
|
100
100
|
let pendingAdditionalContext = null;
|
|
101
101
|
|
|
102
|
+
/** Byte budget of the navigator line (#1462, PRD 4.5 row "Bannerzeile B6"). */
|
|
103
|
+
const NAVIGATOR_BANNER_MAX_BYTES = 120;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Render the one-line navigator status (#1462, fleet protocol v1). The verdict
|
|
107
|
+
* comes from `readNavigatorLease()`: only a VALID lease is "aktiv"; a missing or
|
|
108
|
+
* expired one is "nicht nachweisbar" (ADR-004: absence is no proof of a free
|
|
109
|
+
* seat), an unreadable one says so instead of guessing. A long id is cut from
|
|
110
|
+
* the label — never from the fixed text — so the whole line stays ≤ 120 bytes.
|
|
111
|
+
* Same wording on every harness (Codex runs this hook too).
|
|
112
|
+
*/
|
|
113
|
+
export function navigatorBannerLine(verdict) {
|
|
114
|
+
if (verdict?.state === 'active') {
|
|
115
|
+
const lease = verdict.lease ?? {};
|
|
116
|
+
const prefix = '🧭 Navigator aktiv: ';
|
|
117
|
+
const suffix = ', Check-in nach ~/.config/navigator/checkin/';
|
|
118
|
+
const room = NAVIGATOR_BANNER_MAX_BYTES - Buffer.byteLength(prefix + suffix, 'utf8');
|
|
119
|
+
// Defence in depth: the lease reader already refuses control bytes in these
|
|
120
|
+
// two fields; a newline / ANSI / bidi byte must still never reach systemMessage.
|
|
121
|
+
let shown = String(lease.adresse ?? lease.session_id ?? 'unknown')
|
|
122
|
+
.replace(/[\p{Cc}\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/gu, '?');
|
|
123
|
+
if (Buffer.byteLength(shown, 'utf8') > room) {
|
|
124
|
+
const chars = [...shown];
|
|
125
|
+
while (chars.length > 0 && Buffer.byteLength(`${chars.join('')}…`, 'utf8') > room) chars.pop();
|
|
126
|
+
shown = `${chars.join('')}…`;
|
|
127
|
+
}
|
|
128
|
+
return `${prefix}${shown}${suffix}`;
|
|
129
|
+
}
|
|
130
|
+
if (verdict?.state === 'unreadable') return '🧭 Navigator: Lease unlesbar, Status nicht nachweisbar';
|
|
131
|
+
return '🧭 Navigator: keiner nachweisbar (keine gültige Lease)';
|
|
132
|
+
}
|
|
133
|
+
|
|
102
134
|
/**
|
|
103
135
|
* Queue one or more banner lines for the single end-of-hook flush.
|
|
104
136
|
* @param {string|null|undefined} line — multi-line strings are pushed verbatim
|
|
@@ -453,8 +485,10 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
453
485
|
// observe. A naive full overwrite of current-session.json drops the
|
|
454
486
|
// `last_wave` / `last_batch` markers written mid-session by
|
|
455
487
|
// post-tool-batch-wave-signal.mjs, which makes the next PostToolBatch
|
|
456
|
-
// re-read last_wave as absent→0 and
|
|
457
|
-
//
|
|
488
|
+
// re-read last_wave as absent→0 and RE-OPEN wave N mid-wave, re-stamping
|
|
489
|
+
// wave_start_sha and so moving the #980 files_changed start point. (Until
|
|
490
|
+
// 2026-09-19 it also re-emitted a duplicate wave.started{N}; that event
|
|
491
|
+
// has since been removed.)
|
|
458
492
|
//
|
|
459
493
|
// Three cases, exactly one of which preserves nothing:
|
|
460
494
|
// 'raw-id' — same-logical-session source AND the recorded raw
|
|
@@ -761,11 +795,22 @@ async function main() {
|
|
|
761
795
|
// Resolve current branch; fall back to "unknown" when detached HEAD or no git.
|
|
762
796
|
const branch = (await gitOutput(['branch', '--show-current'], projectRoot)) ?? 'unknown';
|
|
763
797
|
|
|
798
|
+
// #1443 — the commit this session started on, the fallback source for
|
|
799
|
+
// `session_start_ref` (STATE.md's `session-start-ref` had no writer). Kept
|
|
800
|
+
// only when it is a full object name; no repo, an unborn HEAD or a missing
|
|
801
|
+
// git leaves it null and the payload key is OMITTED, never fabricated.
|
|
802
|
+
const rawHeadSha = await gitOutput(['rev-parse', 'HEAD'], projectRoot);
|
|
803
|
+
const headSha = rawHeadSha && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(rawHeadSha) ? rawHeadSha : null;
|
|
804
|
+
|
|
764
805
|
// v3.1.0 env-aware banner (opt-out via enable-host-banner: false in Session Config).
|
|
765
806
|
// The ask-via-tool nudge rides the same opt-out flag — both are coordinator
|
|
766
807
|
// reminders shown at session start; users who silence one expect silence.
|
|
808
|
+
// Hoisted because the cold-start DISPLAY below rides the same opt-out (#1133)
|
|
809
|
+
// and `bannerData` is not a usable proxy for it: `emitHostBanner()` returns
|
|
810
|
+
// null on its own error paths, which would silently re-open the nudge.
|
|
811
|
+
const hostBannerEnabled = await isHostBannerEnabled(projectRoot);
|
|
767
812
|
let bannerData = null;
|
|
768
|
-
if (
|
|
813
|
+
if (hostBannerEnabled) {
|
|
769
814
|
bannerData = await emitHostBanner(projectRoot);
|
|
770
815
|
// Always-on nudge: a user decision has three legitimate forms and AUQ-001
|
|
771
816
|
// routes between them in order — operator verb first (nothing is blocked
|
|
@@ -795,7 +840,14 @@ async function main() {
|
|
|
795
840
|
enabled: coldStartCfg.enabled !== false,
|
|
796
841
|
});
|
|
797
842
|
if (decision.shouldEmit) {
|
|
798
|
-
|
|
843
|
+
// #1133 — the DISPLAY rides the enable-host-banner opt-out like every
|
|
844
|
+
// other start-of-session emitter ("users who silence one expect
|
|
845
|
+
// silence"). The one-shot marker consumption below does NOT: the
|
|
846
|
+
// opt-out silences the display, never the measurement or the state
|
|
847
|
+
// transition. Consuming it unconditionally is what keeps the nudge
|
|
848
|
+
// one-shot for an operator who has banners off — otherwise it re-arms
|
|
849
|
+
// on every start forever.
|
|
850
|
+
if (hostBannerEnabled) pushBanner(decision.bannerLines.join('\n'));
|
|
799
851
|
if (decision.markerPath) {
|
|
800
852
|
await consumeMarker(decision.markerPath).catch(() => false);
|
|
801
853
|
}
|
|
@@ -975,6 +1027,18 @@ async function main() {
|
|
|
975
1027
|
pushBanner(' (registry view: repo/branch/wave. Liveness (busy/idle) is model-side — not available in this hook.)');
|
|
976
1028
|
}
|
|
977
1029
|
|
|
1030
|
+
// #1462 — Navigator line (fleet protocol v1). Exactly one line, read from the
|
|
1031
|
+
// lease file only (`~/.config/navigator/leases/navigator.json`, override
|
|
1032
|
+
// NAVIGATOR_CONFIG_DIR); registry `role` and ListAgents are hints, never proof.
|
|
1033
|
+
// Gated like the peer lines above: no host banner → no navigator line. A read
|
|
1034
|
+
// failure must never block the hook — the helper is fail-closed itself, the
|
|
1035
|
+
// try/catch only guards against the unexpected.
|
|
1036
|
+
if (bannerData) {
|
|
1037
|
+
try {
|
|
1038
|
+
pushBanner(navigatorBannerLine(await readNavigatorLease()));
|
|
1039
|
+
} catch { /* swallow — hook must remain non-blocking */ }
|
|
1040
|
+
}
|
|
1041
|
+
|
|
978
1042
|
// Epic #583 W3-P3 — Mechanical peer-detection banner (independent of the
|
|
979
1043
|
// v3.1.0 registry-driven detectPeers() output). discoverActiveSessions()
|
|
980
1044
|
// reads every worktree's session.lock and the host registry; this is the
|
|
@@ -1114,6 +1178,7 @@ async function main() {
|
|
|
1114
1178
|
// makes the supersession rate measurable instead of inferred (HR-105).
|
|
1115
1179
|
peers_superseded: mechanicalPeersSuperseded,
|
|
1116
1180
|
};
|
|
1181
|
+
if (headSha) payload.head_sha = headSha;
|
|
1117
1182
|
|
|
1118
1183
|
// #1091 — record the native SessionStart source so "does the same raw
|
|
1119
1184
|
// session_id repeat under source=resume?" becomes answerable from
|
|
@@ -1219,6 +1284,14 @@ async function main() {
|
|
|
1219
1284
|
// Size-based rotation of events.jsonl (#251). Session-start is the single
|
|
1220
1285
|
// rotation trigger — per-append overhead is rejected design. Any failure
|
|
1221
1286
|
// is swallowed: rotation must NEVER block the hook.
|
|
1287
|
+
//
|
|
1288
|
+
// #1401 — the DURABLE record of a rotation is no longer this block. Until
|
|
1289
|
+
// 2026-09-19 the only evidence a rotation had happened was the `console.error`
|
|
1290
|
+
// below, and this hook's stderr is discarded by the harness: a rotation and a
|
|
1291
|
+
// DELETED archive were byte-identical from outside. `maybeRotate()` now writes
|
|
1292
|
+
// an `orchestrator.events.rotated` record as the first line of the new active
|
|
1293
|
+
// file, so the ledger carries its own break whatever happens to this stderr
|
|
1294
|
+
// line. The line stays as an operator convenience only — never as the record.
|
|
1222
1295
|
try {
|
|
1223
1296
|
let rotCfg = { enabled: true, 'max-size-mb': 10, 'max-backups': 5 };
|
|
1224
1297
|
try {
|
|
@@ -1234,7 +1307,13 @@ async function main() {
|
|
|
1234
1307
|
enabled: rotCfg.enabled !== false,
|
|
1235
1308
|
});
|
|
1236
1309
|
if (result.rotated) {
|
|
1237
|
-
|
|
1310
|
+
const pruned = result.pruned?.length ? `, pruned ${result.pruned.length}` : '';
|
|
1311
|
+
const ledger = result.recordWritten ? '' : ' [WARN: rotation record NOT written]';
|
|
1312
|
+
console.error(
|
|
1313
|
+
`events-rotation: archived ${result.archivedAs} ` +
|
|
1314
|
+
`(${result.sizeBefore} bytes, ${result.lines ?? '?'} lines, ` +
|
|
1315
|
+
`${result.firstTs ?? 'unknown'} → ${result.lastTs ?? 'unknown'}${pruned})${ledger}`,
|
|
1316
|
+
);
|
|
1238
1317
|
}
|
|
1239
1318
|
} catch (err) {
|
|
1240
1319
|
console.error(`events-rotation: skipped (${err?.message ?? err})`);
|
|
@@ -1245,7 +1324,18 @@ async function main() {
|
|
|
1245
1324
|
// flushBanner() runs here too so a throw partway through main() still surfaces
|
|
1246
1325
|
// whatever was already collected; it is idempotent, so the normal path (which
|
|
1247
1326
|
// flushes at the end of main) does not double-emit.
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1327
|
+
//
|
|
1328
|
+
// Entry guard (#1298 P7): every harness execs this file as the node script
|
|
1329
|
+
// (`sh run-node.sh <this file>` → `exec node "$@"`), so argv[1] IS this module
|
|
1330
|
+
// on every real path. A bare `import()` — a probe, a test, a curious agent —
|
|
1331
|
+
// must not run main() against the live repo: incident W4-FX1 overwrote
|
|
1332
|
+
// `.orchestrator/current-session.json` exactly that way.
|
|
1333
|
+
if (isMainModule(import.meta.url)) {
|
|
1334
|
+
// #1393: the profile gate sits INSIDE the entry guard — at module top level
|
|
1335
|
+
// its `process.exit(0)` exited every process that merely imported this hook.
|
|
1336
|
+
if (!shouldRunHook('on-session-start')) process.exit(0);
|
|
1337
|
+
main().catch(() => {}).finally(() => {
|
|
1338
|
+
flushBanner();
|
|
1339
|
+
process.exit(0);
|
|
1340
|
+
});
|
|
1341
|
+
}
|
package/hooks/on-stop.mjs
CHANGED
|
@@ -30,28 +30,28 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import path from 'node:path';
|
|
33
|
+
import { spawn } from 'node:child_process';
|
|
34
|
+
import { performance } from 'node:perf_hooks';
|
|
33
35
|
import {
|
|
34
36
|
promises as fs,
|
|
35
|
-
closeSync,
|
|
36
37
|
existsSync,
|
|
37
|
-
fstatSync,
|
|
38
|
-
openSync,
|
|
39
38
|
readFileSync,
|
|
40
|
-
readSync,
|
|
41
39
|
statSync,
|
|
42
40
|
writeFileSync,
|
|
43
41
|
} from 'node:fs';
|
|
42
|
+
import { fileURLToPath } from 'node:url';
|
|
44
43
|
|
|
45
44
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
45
|
+
import { readTailWindow } from '../scripts/lib/tail-window.mjs';
|
|
46
46
|
import { AGENT_ID_RE, resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
|
|
47
|
-
// #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
|
|
48
|
-
if (!shouldRunHook('on-stop')) process.exit(0);
|
|
49
47
|
|
|
50
48
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
51
49
|
import { detectPlatform, getProjectDir } from '../scripts/lib/platform.mjs';
|
|
52
50
|
import { parseSessionId } from '../scripts/lib/session-id.mjs';
|
|
51
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
53
52
|
import { heartbeat, logSweepEvent } from '../scripts/lib/session-registry.mjs';
|
|
54
53
|
import { readLock, updateHeartbeat } from '../scripts/lib/session-lock.mjs';
|
|
54
|
+
import { _parseReaper } from '../scripts/lib/config/reaper.mjs';
|
|
55
55
|
|
|
56
56
|
// ---------------------------------------------------------------------------
|
|
57
57
|
// stdin reading (inline — no io.mjs because Stop hooks exit 0 always, never deny)
|
|
@@ -743,17 +743,11 @@ function firstNonEmptyString(input, keys) {
|
|
|
743
743
|
* @returns {string|null}
|
|
744
744
|
*/
|
|
745
745
|
function readStatusFromTranscriptTail(transcriptPath) {
|
|
746
|
-
let fd = null;
|
|
747
746
|
try {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const length = Math.min(size, STATUS_TAIL_BYTES);
|
|
751
|
-
const start = size - length;
|
|
752
|
-
const buf = Buffer.alloc(length);
|
|
753
|
-
const read = readSync(fd, buf, 0, length, start);
|
|
754
|
-
let lines = buf.subarray(0, read).toString('utf8').split('\n');
|
|
747
|
+
const window = readTailWindow(transcriptPath, STATUS_TAIL_BYTES);
|
|
748
|
+
let lines = window.text.split('\n');
|
|
755
749
|
// Drop the leading partial line when the window did not start at byte 0.
|
|
756
|
-
if (
|
|
750
|
+
if (window.cut) lines = lines.slice(1);
|
|
757
751
|
|
|
758
752
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
759
753
|
const line = lines[i].trim();
|
|
@@ -774,10 +768,6 @@ function readStatusFromTranscriptTail(transcriptPath) {
|
|
|
774
768
|
return null;
|
|
775
769
|
} catch {
|
|
776
770
|
return null;
|
|
777
|
-
} finally {
|
|
778
|
-
if (fd !== null) {
|
|
779
|
-
try { closeSync(fd); } catch { /* best-effort */ }
|
|
780
|
-
}
|
|
781
771
|
}
|
|
782
772
|
}
|
|
783
773
|
|
|
@@ -785,10 +775,160 @@ function readStatusFromTranscriptTail(transcriptPath) {
|
|
|
785
775
|
// main
|
|
786
776
|
// ---------------------------------------------------------------------------
|
|
787
777
|
|
|
778
|
+
// ---------------------------------------------------------------------------
|
|
779
|
+
// orphan-reaper trigger (#1432 B4)
|
|
780
|
+
// ---------------------------------------------------------------------------
|
|
781
|
+
//
|
|
782
|
+
// DUPLICATED VERBATIM between the two trigger hooks —
|
|
783
|
+
// hooks/post-tool-batch-wave-signal.mjs <-> hooks/on-stop.mjs. Deliberate, with
|
|
784
|
+
// a named revisit trigger (BV-004): they are the only two trigger points the
|
|
785
|
+
// PRD declares, and a shared `hooks/_lib/reaper-trigger.mjs` would be a third
|
|
786
|
+
// file in the hook import graph. SIZE, measured 2026-09-22 by diffing the two
|
|
787
|
+
// blocks: 138 identical lines — this note said "~40 lines of glue" until then,
|
|
788
|
+
// which is what made the duplicate look cheaper than it is. Extract it the
|
|
789
|
+
// moment a THIRD hook needs the trigger, or the moment the copies must differ.
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Filesystem path of the scan CLI, spawned as a PLAIN argv call.
|
|
793
|
+
*
|
|
794
|
+
* It used to be a `file://` URL handed to `node --input-type=module -e
|
|
795
|
+
* <program>`, because `scripts/lib/orphan-reaper.mjs` had no entry guard. It
|
|
796
|
+
* has one now (`parseReaperCliArgs` + the `isMainModule` tail), so the child's
|
|
797
|
+
* contract lives in that module instead of as source text duplicated here —
|
|
798
|
+
* and nothing this hook builds is a program any more: every variable part is
|
|
799
|
+
* an argv value, which cannot become code whatever the checkout path contains.
|
|
800
|
+
*
|
|
801
|
+
* `fileURLToPath`, not `new URL(...).pathname`: the latter leaves a
|
|
802
|
+
* percent-encoded path for any checkout directory containing a space.
|
|
803
|
+
*/
|
|
804
|
+
const ORPHAN_REAPER_SCRIPT = fileURLToPath(
|
|
805
|
+
new URL('../scripts/lib/orphan-reaper.mjs', import.meta.url),
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Read the `reaper:` block from the project's Session Config host file.
|
|
810
|
+
* Sync + inline, mirroring `hooks/loop-guard.mjs` `loadConfig()` — a hot hook
|
|
811
|
+
* path must not import the full config orchestrator. A missing or unreadable
|
|
812
|
+
* file yields the parser defaults, i.e. DISABLED.
|
|
813
|
+
*
|
|
814
|
+
* @param {string} projectDir
|
|
815
|
+
* @returns {ReturnType<typeof _parseReaper>}
|
|
816
|
+
*/
|
|
817
|
+
function loadReaperConfig(projectDir) {
|
|
818
|
+
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
819
|
+
try {
|
|
820
|
+
return _parseReaper(readFileSync(path.join(projectDir, name), 'utf8'));
|
|
821
|
+
} catch {
|
|
822
|
+
// missing or unreadable — try the next candidate
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return _parseReaper('');
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Trigger the orphan scan, throttled and NON-BLOCKING (PRD FA4).
|
|
830
|
+
*
|
|
831
|
+
* The hook itself does exactly two pieces of I/O — one config read and one
|
|
832
|
+
* `stat` of the throttle marker — and then hands the work to a DETACHED,
|
|
833
|
+
* unref'd child. The scan is never run inline: one `ps` round-trip out of Node
|
|
834
|
+
* was measured at ~47 ms over 287 KB of output, which alone would blow the
|
|
835
|
+
* 50 ms `reaper.max-hook-latency-ms` budget this hook has to stay inside.
|
|
836
|
+
*
|
|
837
|
+
* The marker is stamped BEFORE the spawn, so a spawn that fails still consumes
|
|
838
|
+
* the throttle window — otherwise a broken spawn would be retried on every
|
|
839
|
+
* single tool batch.
|
|
840
|
+
*
|
|
841
|
+
* `reaper.max-hook-latency-ms` is the ceiling on the work above — measured here
|
|
842
|
+
* with `performance.now()` and reported as ONE stderr WARN line when exceeded.
|
|
843
|
+
* A WARN and not an event: this fires from a `PostToolBatch`-class hook, and a
|
|
844
|
+
* per-fire telemetry record is exactly the always-on signal
|
|
845
|
+
* `.claude/rules/host-resources.md` HR-101 calls a broken instrument. The
|
|
846
|
+
* measurement covers preparation only — the scan itself runs detached, which is
|
|
847
|
+
* the whole reason the budget can be held.
|
|
848
|
+
*
|
|
849
|
+
* Never throws: any failure degrades silently (PRD FA4 "lautlos degradieren").
|
|
850
|
+
*
|
|
851
|
+
* @param {object} [opts]
|
|
852
|
+
* @param {string} [opts.projectDir] Repo root; defaults to `getProjectDir()`.
|
|
853
|
+
* @param {number} [opts.now] Injected clock (ms).
|
|
854
|
+
* @param {Function} [opts.spawnFn] Injected `spawn` (tests).
|
|
855
|
+
* @param {Function} [opts.statFn] Injected `statSync` (tests).
|
|
856
|
+
* @param {Function} [opts.writeFn] Injected marker writer (tests).
|
|
857
|
+
* @param {() => number} [opts.clockFn] Injected monotonic clock for the latency
|
|
858
|
+
* budget (tests); defaults to `performance.now`.
|
|
859
|
+
* @returns {Promise<{spawned: boolean, reason: string}>}
|
|
860
|
+
*/
|
|
861
|
+
export async function maybeTriggerOrphanScan({
|
|
862
|
+
projectDir,
|
|
863
|
+
now,
|
|
864
|
+
spawnFn = spawn,
|
|
865
|
+
statFn,
|
|
866
|
+
writeFn,
|
|
867
|
+
clockFn = () => performance.now(),
|
|
868
|
+
} = {}) {
|
|
869
|
+
const startedAt = clockFn();
|
|
870
|
+
/** One WARN line when the preparation overran `reaper.max-hook-latency-ms`. */
|
|
871
|
+
const checkLatency = (budgetMs) => {
|
|
872
|
+
const elapsed = clockFn() - startedAt;
|
|
873
|
+
if (Number.isFinite(budgetMs) && budgetMs > 0 && elapsed > budgetMs) {
|
|
874
|
+
process.stderr.write(
|
|
875
|
+
`orphan-reaper trigger: hook latency ${elapsed.toFixed(1)} ms exceeded `
|
|
876
|
+
+ `reaper.max-hook-latency-ms (${budgetMs} ms)\n`,
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
try {
|
|
881
|
+
const root = typeof projectDir === 'string' && projectDir ? projectDir : getProjectDir();
|
|
882
|
+
const cfg = loadReaperConfig(root);
|
|
883
|
+
// Cheapest gate first: disabled means no stat, no spawn, no module load.
|
|
884
|
+
if (cfg.enabled !== true) return { spawned: false, reason: 'disabled' };
|
|
885
|
+
|
|
886
|
+
// The throttle module only — the scan's ledger, `ps` and kill-ladder code
|
|
887
|
+
// load in the detached child, never on this hook path (#1437).
|
|
888
|
+
const reaper = await import('../scripts/lib/orphan-reaper/scan-throttle.mjs');
|
|
889
|
+
const markerPath = reaper.scanMarkerPath(root);
|
|
890
|
+
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
891
|
+
|
|
892
|
+
if (!reaper.shouldScanNow(markerPath, nowMs, cfg['min-scan-interval-seconds'], { statFn })) {
|
|
893
|
+
checkLatency(cfg['max-hook-latency-ms']);
|
|
894
|
+
return { spawned: false, reason: 'throttled' };
|
|
895
|
+
}
|
|
896
|
+
reaper.touchScanMarker(markerPath, { writeFn });
|
|
897
|
+
|
|
898
|
+
const child = spawnFn(
|
|
899
|
+
process.execPath,
|
|
900
|
+
[
|
|
901
|
+
ORPHAN_REAPER_SCRIPT,
|
|
902
|
+
'--repo-root', root,
|
|
903
|
+
'--mode', cfg.mode,
|
|
904
|
+
'--min-age-seconds', String(cfg['min-age-seconds']),
|
|
905
|
+
'--kill-grace-ms', String(cfg['kill-grace-ms']),
|
|
906
|
+
'--verify-wait-ms', String(cfg['verify-wait-ms']),
|
|
907
|
+
'--false-alarm-window', String(cfg['false-alarm-window']),
|
|
908
|
+
],
|
|
909
|
+
{ detached: true, stdio: 'ignore' },
|
|
910
|
+
);
|
|
911
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
912
|
+
checkLatency(cfg['max-hook-latency-ms']);
|
|
913
|
+
return { spawned: true, reason: 'spawned' };
|
|
914
|
+
} catch {
|
|
915
|
+
return { spawned: false, reason: 'error' };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
788
919
|
async function main() {
|
|
789
920
|
const input = await readStdinJson();
|
|
790
921
|
const eventType = discriminate(input);
|
|
791
922
|
|
|
923
|
+
// Orphan-reaper trigger (#1432 B4) — SubagentStop is the PRD's second trigger
|
|
924
|
+
// point: it lands exactly when gate children have just finished and their PIDs
|
|
925
|
+
// are still known. Placed BEFORE the branch dispatch (unlike the PostToolBatch
|
|
926
|
+
// hook, which triggers last) because both branches below may throw — a
|
|
927
|
+
// `handleStop` failure must not silently disarm the watchdog. The cost is one
|
|
928
|
+
// config read plus one stat, bounded by `reaper.max-hook-latency-ms` (50 ms),
|
|
929
|
+
// and the scan itself runs in a detached child.
|
|
930
|
+
await maybeTriggerOrphanScan();
|
|
931
|
+
|
|
792
932
|
if (eventType === 'subagent_stop') {
|
|
793
933
|
const additionalContext = await handleSubagentStop(input);
|
|
794
934
|
// v2.1.163+: emit hookSpecificOutput for SubagentStop path.
|
|
@@ -860,6 +1000,14 @@ function buildTerminalSequenceJson(platform) {
|
|
|
860
1000
|
}
|
|
861
1001
|
|
|
862
1002
|
// Exit 0 always — informational hook must never block Claude.
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1003
|
+
// Entry guard (#1298 P7): run only as the node script every harness execs
|
|
1004
|
+
// (`sh run-node.sh <this file>`); a bare `import()` must not run the Stop
|
|
1005
|
+
// handler against the live repo.
|
|
1006
|
+
if (isMainModule(import.meta.url)) {
|
|
1007
|
+
// #1393: the profile gate sits INSIDE the entry guard — at module top level
|
|
1008
|
+
// its `process.exit(0)` exited every process that merely imported this hook.
|
|
1009
|
+
if (!shouldRunHook('on-stop')) process.exit(0);
|
|
1010
|
+
main()
|
|
1011
|
+
.catch(() => {})
|
|
1012
|
+
.finally(() => process.exit(0));
|
|
1013
|
+
}
|
package/hooks/operator-steer.mjs
CHANGED
|
@@ -34,8 +34,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
34
34
|
import path from 'node:path';
|
|
35
35
|
|
|
36
36
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
37
|
-
|
|
38
|
-
if (!shouldRunHook('operator-steer')) process.exit(0);
|
|
37
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
39
38
|
|
|
40
39
|
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
41
40
|
|
|
@@ -61,4 +60,13 @@ async function main() {
|
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
// Exit 0 always — informational hook must never block Claude.
|
|
64
|
-
|
|
63
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
64
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
65
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
66
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
67
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
68
|
+
// process that merely imported this hook.
|
|
69
|
+
if (isMainModule(import.meta.url)) {
|
|
70
|
+
if (!shouldRunHook('operator-steer')) process.exit(0);
|
|
71
|
+
main().catch(() => {}).finally(() => process.exit(0));
|
|
72
|
+
}
|
|
@@ -68,12 +68,13 @@ import {
|
|
|
68
68
|
} from '../scripts/lib/issue-budget.mjs';
|
|
69
69
|
|
|
70
70
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
71
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
71
72
|
import { existsSync } from 'node:fs';
|
|
72
73
|
import path from 'node:path';
|
|
73
74
|
|
|
74
75
|
// Opt-out per session via SO_DISABLED_HOOKS=post-bash-issue-budget-refund; the
|
|
75
|
-
// "minimal"/"off" profiles disable it like every other non-core hook
|
|
76
|
-
|
|
76
|
+
// "minimal"/"off" profiles disable it like every other non-core hook — the gate
|
|
77
|
+
// itself runs inside the entry guard at the bottom of this file (#1393).
|
|
77
78
|
|
|
78
79
|
// ---------------------------------------------------------------------------
|
|
79
80
|
// Helpers
|
|
@@ -367,9 +368,18 @@ async function main() {
|
|
|
367
368
|
}
|
|
368
369
|
|
|
369
370
|
// Top-level error handler — fail open, same posture as the sibling hooks.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
371
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
372
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
373
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
374
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
375
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
376
|
+
// process that merely imported this hook.
|
|
377
|
+
if (isMainModule(import.meta.url)) {
|
|
378
|
+
if (!shouldRunHook('post-bash-issue-budget-refund')) process.exit(0);
|
|
379
|
+
main().catch((e) => {
|
|
380
|
+
process.stderr.write(
|
|
381
|
+
`⚠ post-bash-issue-budget-refund: internal error — ${e?.message || e}\n`,
|
|
382
|
+
);
|
|
383
|
+
process.exit(0);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
@@ -143,8 +143,6 @@
|
|
|
143
143
|
*/
|
|
144
144
|
|
|
145
145
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
146
|
-
// Exit 0 immediately (silent no-op) when disabled via profile/env (#211).
|
|
147
|
-
if (!shouldRunHook('post-bash-write-verify')) process.exit(0);
|
|
148
146
|
|
|
149
147
|
import path from 'node:path';
|
|
150
148
|
import os from 'node:os';
|
|
@@ -1136,6 +1134,9 @@ function invokedAsScript() {
|
|
|
1136
1134
|
}
|
|
1137
1135
|
const isMain = invokedAsScript();
|
|
1138
1136
|
if (isMain) {
|
|
1137
|
+
// #1393: the profile gate sits INSIDE the entry guard — at module top level
|
|
1138
|
+
// its `process.exit(0)` exited every process that merely imported this hook.
|
|
1139
|
+
if (!shouldRunHook('post-bash-write-verify')) process.exit(0);
|
|
1139
1140
|
// Advisory hook: never block, never surface an error to the tool call.
|
|
1140
1141
|
main().catch(() => {}).finally(() => process.exit(0));
|
|
1141
1142
|
}
|
|
@@ -37,8 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
40
|
-
|
|
41
|
-
if (!shouldRunHook('post-edit-import-probe')) process.exit(0);
|
|
40
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
42
41
|
|
|
43
42
|
import path from 'node:path';
|
|
44
43
|
import { readFileSync, existsSync } from 'node:fs';
|
|
@@ -335,10 +334,19 @@ async function main() {
|
|
|
335
334
|
// Exit 0 always — a hook that guards against hook breakage must never block.
|
|
336
335
|
// The catch stays silent towards the TOOL CALL but not towards the operator:
|
|
337
336
|
// a probe whose own breakage is invisible is a probe nobody can trust.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
337
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
338
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
339
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
340
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
341
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
342
|
+
// process that merely imported this hook.
|
|
343
|
+
if (isMainModule(import.meta.url)) {
|
|
344
|
+
if (!shouldRunHook('post-edit-import-probe')) process.exit(0);
|
|
345
|
+
main()
|
|
346
|
+
.catch((err) => {
|
|
347
|
+
try {
|
|
348
|
+
process.stderr.write(`post-edit-import-probe: ${err?.message ?? String(err)}\n`);
|
|
349
|
+
} catch { /* stderr gone — nothing left to report to */ }
|
|
350
|
+
})
|
|
351
|
+
.finally(() => process.exit(0));
|
|
352
|
+
}
|
|
@@ -32,8 +32,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
32
32
|
import { performance } from 'node:perf_hooks';
|
|
33
33
|
|
|
34
34
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
35
|
-
|
|
36
|
-
if (!shouldRunHook('post-edit-validate')) process.exit(0);
|
|
35
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
37
36
|
|
|
38
37
|
import { readStdin } from '../scripts/lib/io.mjs';
|
|
39
38
|
import { resolveProjectDir } from '../scripts/lib/platform.mjs';
|
|
@@ -220,6 +219,15 @@ async function main() {
|
|
|
220
219
|
}
|
|
221
220
|
|
|
222
221
|
// REQ-01: top-level catch — PostToolUse must never block; always exit 0
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
223
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
224
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
225
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
226
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
227
|
+
// process that merely imported this hook.
|
|
228
|
+
if (isMainModule(import.meta.url)) {
|
|
229
|
+
if (!shouldRunHook('post-edit-validate')) process.exit(0);
|
|
230
|
+
main().catch(() => {
|
|
231
|
+
process.exit(0);
|
|
232
|
+
});
|
|
233
|
+
}
|