session-orchestrator 5.1.0 → 5.3.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 +6 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +7 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +8 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +21 -0
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +7 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +7 -2
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +7 -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 +7 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +7 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +8 -3
- 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 +22 -0
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +7 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +20 -0
- 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 +6 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- 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 +6 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +8 -2
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +21 -0
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +6 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +22 -0
- 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 +6 -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 +6 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +7 -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 +21 -0
- package/.agents/skills/test/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 +7 -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/autopilot/SKILL.md +5 -4
- package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
- package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
- package/.codex-plugin/skills/close/SKILL.md +3 -3
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -1
- package/.codex-plugin/skills/debug/SKILL.md +11 -4
- package/.codex-plugin/skills/discovery/SKILL.md +8 -4
- package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
- package/.codex-plugin/skills/eli5/SKILL.md +9 -4
- package/.codex-plugin/skills/eval/SKILL.md +9 -4
- package/.codex-plugin/skills/evolve/SKILL.md +9 -4
- package/.codex-plugin/skills/go/SKILL.md +3 -3
- package/.codex-plugin/skills/grill/SKILL.md +11 -4
- package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -1
- package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
- package/.codex-plugin/skills/plan/SKILL.md +8 -4
- package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
- package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
- package/.codex-plugin/skills/release/SKILL.md +3 -3
- package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.codex-plugin/skills/spinout/SKILL.md +4 -4
- package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
- package/.codex-plugin/skills/test/SKILL.md +3 -3
- package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
- package/.cursor/commands/autopilot.md +4 -4
- package/.cursor/commands/bootstrap.md +5 -4
- package/.cursor/commands/brainstorm.md +5 -4
- package/.cursor/commands/close.md +4 -3
- package/.cursor/commands/debug.md +4 -4
- package/.cursor/commands/discovery.md +4 -4
- package/.cursor/commands/dispatcher.md +4 -4
- package/.cursor/commands/eli5.md +4 -4
- package/.cursor/commands/eval.md +4 -4
- package/.cursor/commands/evolve.md +4 -4
- package/.cursor/commands/go.md +4 -3
- package/.cursor/commands/grill.md +4 -4
- package/.cursor/commands/harness-audit.md +3 -3
- package/.cursor/commands/memory-cleanup.md +4 -4
- package/.cursor/commands/persona-panel.md +4 -4
- package/.cursor/commands/plan.md +5 -4
- package/.cursor/commands/portfolio.md +3 -3
- package/.cursor/commands/reconcile.md +4 -4
- package/.cursor/commands/release.md +4 -3
- package/.cursor/commands/repo-audit.md +4 -4
- package/.cursor/commands/session.md +1 -1
- package/.cursor/commands/spinout.md +4 -4
- package/.cursor/commands/sunset-review.md +4 -4
- package/.cursor/commands/test.md +3 -3
- package/.cursor/commands/ux-grill.md +4 -4
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/010-session-workflow.mdc +2 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/bootstrap/SKILL.md +1 -0
- package/.cursor/skills/close/SKILL.md +13 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/debug/SKILL.md +0 -1
- package/.cursor/skills/discovery/SKILL.md +0 -1
- package/.cursor/skills/dispatcher/SKILL.md +0 -1
- package/.cursor/skills/eli5/SKILL.md +0 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/evolve/SKILL.md +0 -1
- package/.cursor/skills/go/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +0 -1
- package/.cursor/skills/harness-audit/SKILL.md +12 -0
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor/skills/portfolio/SKILL.md +12 -0
- package/.cursor/skills/release/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +0 -1
- package/.cursor/skills/sunset-review/SKILL.md +0 -1
- package/.cursor/skills/test/SKILL.md +12 -0
- package/.cursor/skills/ux-grill/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +13 -4
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +197 -0
- package/README.md +11 -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 +17 -3
- package/docs/baseline.md +12 -6
- package/docs/ci-setup.md +53 -0
- package/docs/codex-setup.md +15 -3
- package/docs/components.md +13 -6
- package/docs/events-schema.md +59 -9
- package/docs/install.md +16 -0
- package/docs/persona-panel.md +1 -1
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +135 -14
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +106 -11
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +125 -8
- package/hooks/_lib/subagent-paths.mjs +15 -0
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/_lib/vcs-create-matcher.mjs +217 -62
- 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.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +72 -25
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +211 -23
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +6 -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 +200 -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 +18 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +119 -28
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +385 -5
- package/hooks/skill-invocation-telemetry.mjs +2 -1
- package/hooks/subagent-telemetry.mjs +15 -19
- 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 +2 -1
- package/pi/prompts/autopilot.md +3 -3
- package/pi/prompts/bootstrap.md +3 -3
- package/pi/prompts/brainstorm.md +3 -3
- package/pi/prompts/close.md +2 -2
- package/pi/prompts/debug.md +3 -3
- package/pi/prompts/discovery.md +3 -3
- package/pi/prompts/dispatcher.md +3 -3
- package/pi/prompts/eli5.md +3 -3
- package/pi/prompts/eval.md +3 -3
- package/pi/prompts/evolve.md +3 -3
- package/pi/prompts/go.md +2 -2
- package/pi/prompts/grill.md +3 -3
- package/pi/prompts/harness-audit.md +2 -3
- package/pi/prompts/memory-cleanup.md +3 -3
- package/pi/prompts/persona-panel.md +3 -3
- package/pi/prompts/plan.md +3 -3
- package/pi/prompts/portfolio.md +2 -2
- package/pi/prompts/reconcile.md +3 -3
- package/pi/prompts/release.md +3 -3
- package/pi/prompts/repo-audit.md +3 -4
- package/pi/prompts/session.md +2 -2
- package/pi/prompts/spinout.md +3 -3
- package/pi/prompts/sunset-review.md +3 -3
- package/pi/prompts/templates-ack.md +1 -1
- package/pi/prompts/test.md +3 -3
- package/pi/prompts/ux-grill.md +3 -3
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/archive-closed-prds.mjs +2 -2
- package/scripts/auq-audit.mjs +2 -3
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +171 -15
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +2 -2
- package/scripts/check-package-manager.mjs +2 -2
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/ci/assert-vitest-green.mjs +2 -1
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +77 -32
- package/scripts/eval-session.mjs +65 -3
- package/scripts/export-hw-learnings.mjs +2 -1
- package/scripts/express-path.mjs +1 -1
- package/scripts/gc-stale-worktrees.mjs +2 -1
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-codex-skills.mjs +48 -4
- package/scripts/generate-cursor-adapter.mjs +220 -11
- package/scripts/generate-hook-import-set.mjs +12 -27
- package/scripts/generate-pi-prompts.mjs +183 -13
- package/scripts/github-protection-audit.mjs +2 -3
- package/scripts/lib/agent-frontmatter.mjs +23 -1
- 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/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/claude-md-budget-lint.mjs +2 -5
- package/scripts/lib/command-blocker.mjs +408 -33
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/drift-check.mjs +19 -0
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +76 -13
- package/scripts/lib/cursor-hook-bridge.mjs +2 -2
- package/scripts/lib/description-surface.mjs +2 -5
- package/scripts/lib/dispatcher/cli.mjs +2 -1
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/ecosystem-wizard.mjs +2 -1
- 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 +524 -5
- package/scripts/lib/fetch-baseline.mjs +3 -8
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +332 -50
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/is-main-module.mjs +82 -0
- 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/locks/index.mjs +32 -25
- package/scripts/lib/maintenance-due-banner.mjs +122 -91
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/peer-discovery.mjs +2 -5
- package/scripts/lib/playwright-driver/runner.mjs +2 -1
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +884 -0
- 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/rule-loader.mjs +41 -12
- package/scripts/lib/rules-sync.mjs +2 -5
- package/scripts/lib/scope-echo.mjs +429 -7
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +91 -12
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- 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/state-md.mjs +84 -3
- package/scripts/lib/sunset/walker.mjs +31 -4
- package/scripts/lib/tail-window.mjs +56 -0
- 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 +2 -6
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +74 -14
- package/scripts/lib/user-invocable-skills.mjs +205 -0
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-banner-parity.mjs +2 -2
- package/scripts/lib/validate/check-cursor-adapter.mjs +3 -2
- package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
- package/scripts/lib/validate/check-entry-guard.mjs +329 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
- package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
- 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 +35 -6
- package/scripts/lib/validate/check-skill-script-paths.mjs +241 -29
- package/scripts/lib/validate/check-test-git-config-target.mjs +26 -36
- package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +9 -104
- package/scripts/lib/validate/check-unwired-features.mjs +220 -33
- package/scripts/lib/validate/check-validator-registration.mjs +36 -12
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +39 -12
- 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-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/wave-transcript-tail.mjs +2 -2
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/lock-reaper.mjs +2 -1
- package/scripts/materialize-wave-scope.mjs +87 -4
- package/scripts/migrate-sessions-jsonl.mjs +2 -1
- package/scripts/migrate-vault-paths.mjs +2 -3
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +109 -39
- package/scripts/relocate-vault-corpus.mjs +2 -3
- package/scripts/repair-invalid-sessions.mjs +2 -2
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/session-shape.mjs +2 -2
- package/scripts/site-numbers.mjs +35 -11
- package/scripts/sweep-expired-rules.mjs +227 -0
- package/scripts/validate-plugin.mjs +21 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-consolidate.mjs +2 -2
- package/scripts/vault-mirror.mjs +11 -4
- package/scripts/wave-scope-binding.mjs +2 -3
- package/skills/_shared/bootstrap-gate.md +1 -1
- package/skills/_shared/monitor-patterns.md +1 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/_shared/research-evidence.md +53 -0
- package/skills/_shared/state-ownership.md +3 -0
- package/skills/autopilot/SKILL.md +80 -11
- package/skills/bootstrap/SKILL.md +51 -1
- package/skills/brainstorm/SKILL.md +16 -0
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +49 -11
- package/{commands/close.md → skills/close/SKILL.md} +9 -3
- 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/debug/SKILL.md +10 -0
- package/skills/discovery/SKILL.md +24 -1
- package/skills/discovery/probes-session.md +2 -2
- package/skills/dispatcher/SKILL.md +38 -7
- package/skills/eli5/SKILL.md +11 -0
- package/skills/eval/SKILL.md +52 -23
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +9 -2
- package/skills/evolve/references/evolve-dialectic-mode.md +46 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/{commands/go.md → skills/go/SKILL.md} +9 -1
- package/skills/grill/SKILL.md +19 -0
- package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
- package/skills/hook-development/SKILL.md +46 -41
- package/skills/memory-cleanup/SKILL.md +7 -0
- package/skills/npm-publish/SKILL.md +2 -2
- package/skills/persona-panel/SKILL.md +56 -1
- package/skills/persona-panel/persona-format.md +1 -1
- package/skills/plan/SKILL.md +28 -1
- package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
- package/skills/reconcile/SKILL.md +21 -0
- package/{commands/release.md → skills/release/SKILL.md} +16 -2
- package/skills/repo-audit/SKILL.md +7 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/plan-verification.md +2 -2
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +18 -6
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/SKILL.md +5 -4
- package/skills/session-start/phase-8-5-express-path.md +6 -6
- package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +6 -4
- package/skills/spinout/SKILL.md +12 -1
- package/skills/sunset-review/SKILL.md +13 -0
- package/{commands/test.md → skills/test/SKILL.md} +10 -4
- package/skills/ux-grill/SKILL.md +20 -2
- package/skills/wave-executor/SKILL.md +14 -7
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +18 -4
- package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
- package/skills/wave-executor/references/wave-loop-review.md +17 -1
- package/commands/autopilot.md +0 -80
- package/commands/bootstrap.md +0 -56
- package/commands/brainstorm.md +0 -48
- package/commands/debug.md +0 -36
- package/commands/discovery.md +0 -32
- package/commands/dispatcher.md +0 -59
- package/commands/eli5.md +0 -33
- package/commands/eval.md +0 -28
- package/commands/evolve.md +0 -10
- package/commands/grill.md +0 -45
- package/commands/memory-cleanup.md +0 -26
- package/commands/persona-panel.md +0 -121
- package/commands/plan.md +0 -15
- package/commands/reconcile.md +0 -23
- package/commands/repo-audit.md +0 -24
- package/commands/spinout.md +0 -15
- package/commands/sunset-review.md +0 -27
- package/commands/ux-grill.md +0 -51
|
@@ -230,6 +230,7 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
|
230
230
|
/** @type {typeof import('../scripts/lib/io.mjs').emitWarn} */ let emitWarn;
|
|
231
231
|
/** @type {typeof import('../scripts/lib/io.mjs').writeJsonAtomicSync} */ let writeJsonAtomicSync;
|
|
232
232
|
/** @type {typeof import('../scripts/lib/file-lock.mjs').withFileLock} */ let withFileLock;
|
|
233
|
+
/** @type {typeof import('../scripts/lib/scope-echo.mjs').scopeDigest} */ let scopeDigest;
|
|
233
234
|
let findScopeCollisions;
|
|
234
235
|
|
|
235
236
|
const PLUGIN_ROOT = path.resolve(import.meta.dirname, '..');
|
|
@@ -287,6 +288,48 @@ const SHAPE_NONE = 'none';
|
|
|
287
288
|
/** The per-dispatch observability record (#1092) — see § Observability. */
|
|
288
289
|
const SCOPE_EVENT = 'orchestrator.wave_dispatch.scope_checked';
|
|
289
290
|
|
|
291
|
+
/**
|
|
292
|
+
* The worktree-base observability record (#1413).
|
|
293
|
+
*
|
|
294
|
+
* Same `wave_dispatch` domain and same `_checked` verb as `SCOPE_EVENT`, and
|
|
295
|
+
* written for EVERY `isolation: "worktree"` dispatch rather than only the
|
|
296
|
+
* alarming one. That is HR-105 applied at the source: a numerator-only stream
|
|
297
|
+
* cannot tell "genuinely rare" from "silently broken", and the firing rate this
|
|
298
|
+
* warning must stay under is then unfalsifiable. With every outcome in one
|
|
299
|
+
* stream the rate is `stale:true / all records of this name`.
|
|
300
|
+
*
|
|
301
|
+
* #1424 widened "every outcome" from two to three: the `stale: true|false`
|
|
302
|
+
* split was already covered, but each NON-measurement (peer STATE.md, no
|
|
303
|
+
* STATE.md, no `session-start-ref`, a git failure) returned early and emitted
|
|
304
|
+
* nothing — so the gate in front of the split reproduced exactly the
|
|
305
|
+
* zero-records ambiguity the split had removed. Those now carry
|
|
306
|
+
* `stale: null` plus a `skipped` reason, and only `isolation !== "worktree"`
|
|
307
|
+
* stays silent (HR-101: it is ~90% of all dispatches).
|
|
308
|
+
*/
|
|
309
|
+
const WORKTREE_BASE_EVENT = 'orchestrator.wave_dispatch.worktree_base_checked';
|
|
310
|
+
|
|
311
|
+
/** State-dir candidates, in the same order `waveKeyOf()` probes them. */
|
|
312
|
+
const STATE_DIR_CANDIDATES = ['.pi', '.cursor', '.codex', '.claude'];
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The receive-side instruction line `renderScopeEchoInstruction()` renders and
|
|
316
|
+
* the coordinator appends immediately after the fenced block
|
|
317
|
+
* (`wave-loop-dispatch.md` § Pre-Dispatch: File-Scope Injection).
|
|
318
|
+
*
|
|
319
|
+
* Matching it HERE, in the same prompt, is what makes the agent-A-block-with-
|
|
320
|
+
* agent-B-line mix-up catchable without a single filesystem read: the block and
|
|
321
|
+
* the line must come from the same `$AGENT_FILESCOPE_JSON`, so their digests
|
|
322
|
+
* must agree (`digest_consistent`). The marker half stays case-sensitive for the
|
|
323
|
+
* same reason `scope-echo.mjs` keeps it so — a lowercase lookalike is not the
|
|
324
|
+
* line; the HEX half is read case-insensitively and normalised to lowercase.
|
|
325
|
+
* Non-global on purpose: a `g` regex carries `lastIndex` between calls.
|
|
326
|
+
*/
|
|
327
|
+
const ECHO_INSTRUCTION_RE =
|
|
328
|
+
/End your final report with the line:[ \t]*`{0,3}SCOPE-DIGEST:[ \t]*`{0,3}([0-9a-fA-F]{8})(?![0-9a-fA-F])/;
|
|
329
|
+
|
|
330
|
+
/** Shape of a well-formed digest, used to reject anything a broken digest fn returns. */
|
|
331
|
+
const DIGEST_RE = /^[0-9a-f]{8}$/;
|
|
332
|
+
|
|
290
333
|
/**
|
|
291
334
|
* Clamp for the one free-form string the event carries (`agent_id`, built from
|
|
292
335
|
* the coordinator's own `description`). A dispatch description is a label, but
|
|
@@ -358,6 +401,14 @@ async function bootstrap() {
|
|
|
358
401
|
io: { specifier: lib('io.mjs') },
|
|
359
402
|
scopeGate: { specifier: lib('scope-gate.mjs') },
|
|
360
403
|
fileLock: { specifier: lib('file-lock.mjs') },
|
|
404
|
+
// ONE normalization for the digest, shared with the receive side (#1092):
|
|
405
|
+
// `scope-echo.mjs` is pure (stdlib + `crypto-digest-utils.mjs`) and its
|
|
406
|
+
// `scopeDigest` is what `--verify` joins the two halves on. A second
|
|
407
|
+
// implementation here would let the halves disagree while both looked
|
|
408
|
+
// right — the class `guard-design.md` § "zero-import predicate module"
|
|
409
|
+
// names. Late-bound like every other repo module so a load failure
|
|
410
|
+
// banners instead of disarming the guard silently (#993).
|
|
411
|
+
scopeEcho: { specifier: lib('scope-echo.mjs') },
|
|
361
412
|
},
|
|
362
413
|
{
|
|
363
414
|
hookName: HOOK_NAME,
|
|
@@ -370,6 +421,7 @@ async function bootstrap() {
|
|
|
370
421
|
({ readStdin, emitAllow, emitDeny, emitWarn, writeJsonAtomicSync } = modules.io);
|
|
371
422
|
({ findScopeCollisions } = modules.scopeGate);
|
|
372
423
|
({ withFileLock } = modules.fileLock);
|
|
424
|
+
({ scopeDigest } = modules.scopeEcho);
|
|
373
425
|
}
|
|
374
426
|
|
|
375
427
|
// ---------------------------------------------------------------------------
|
|
@@ -407,8 +459,51 @@ const SCOPE_TERMS = 'DATEI[- ]SCOPE|FILE[- ]SCOPE|FILE SCOPE|DEIN SCOPE|SCOPE \\
|
|
|
407
459
|
* hit in a region as one is the recorded failure of `parseEpicRef` (#1112).
|
|
408
460
|
*
|
|
409
461
|
* The real miss class is a different SHAPE, handled by {@link INLINE_SCOPE_DECL}.
|
|
462
|
+
*
|
|
463
|
+
* ## The marker vocabulary is CASE-SENSITIVE, and that is a measurement (#1092)
|
|
464
|
+
*
|
|
465
|
+
* Until 2026-09-16 both shapes carried the `i` flag, so the term `FILE SCOPE`
|
|
466
|
+
* also matched ordinary lowercase prose. The Learnings-Index header this repo
|
|
467
|
+
* injects into every agent prompt —
|
|
468
|
+
* `## Learnings Index (selected for your file scope) — …` — therefore matched
|
|
469
|
+
* the marker at column ~38 of a line-leading window, and the FIRST fenced block
|
|
470
|
+
* anywhere after it (a verification command, a report template) decided the
|
|
471
|
+
* class: no path survived, so the dispatch was recorded `unparseable`, i.e.
|
|
472
|
+
* matrix row 6 — "a declaration is present but the parser gave up".
|
|
473
|
+
*
|
|
474
|
+
* MEASURED in this session's own wave 1 (`.orchestrator/wave-dispatch-scopes.json`,
|
|
475
|
+
* waveKey `5de6560c-…|w1|Discovery`): `unparseable: 5, extracted: 0` for FIVE
|
|
476
|
+
* Discovery dispatches that carried no `FILE-SCOPE` block at all. Host-wide the
|
|
477
|
+
* same confusion accounts for the 52 historical `unparseable` records.
|
|
478
|
+
*
|
|
479
|
+
* Every DOCUMENTED marker is upper-case (`wave-loop-dispatch.md` § Pre-Dispatch:
|
|
480
|
+
* File-Scope Injection writes `FILE-SCOPE — exactly these:`), while every
|
|
481
|
+
* measured false positive is prose in sentence case — so dropping `i` separates
|
|
482
|
+
* them without narrowing the search window, which the § above showed recovers
|
|
483
|
+
* nothing.
|
|
484
|
+
*
|
|
485
|
+
* ## What dropping `i` costs, stated honestly (corrected 2026-09-16)
|
|
486
|
+
*
|
|
487
|
+
* The first cut of this note claimed the change "moves a CLASSIFICATION and
|
|
488
|
+
* never a decision", on the grounds that `marker-absent` and `unparseable` both
|
|
489
|
+
* resolve to ALLOW. That is only true for a declaration the parser would have
|
|
490
|
+
* given up on anyway. A MIXED-CASE declaration that WOULD have yielded paths
|
|
491
|
+
* (`File-Scope:` followed by a fenced path list) now matches nothing, so
|
|
492
|
+
* `extractScopeFromPrompt` returns `[]`, `findScopeCollisions` never runs, and
|
|
493
|
+
* the dispatch is ALLOWED unconditionally — a lost DENY capability for that
|
|
494
|
+
* spelling, i.e. a decision change, not a reclassification.
|
|
495
|
+
*
|
|
496
|
+
* It is safe today because every LIVE injector writes the upper-case canonical
|
|
497
|
+
* marker: `skills/wave-executor/references/wave-loop-dispatch.md`
|
|
498
|
+
* § Pre-Dispatch: File-Scope Injection documents `FILE-SCOPE — exactly these:`,
|
|
499
|
+
* and the repo-wide census (2026-09-16) finds the mixed-case spellings only in
|
|
500
|
+
* prose, never in an injected block. That is a property of the injectors, not of
|
|
501
|
+
* this regex, so it is PINNED rather than assumed:
|
|
502
|
+
* `tests/skills/wave-loop-scope-marker.test.mjs` asserts the documented marker
|
|
503
|
+
* line is upper-case and matches {@link SCOPE_MARKER}, so a future template edit
|
|
504
|
+
* to `File-Scope:` goes red instead of silently disarming the guard.
|
|
410
505
|
*/
|
|
411
|
-
const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, '
|
|
506
|
+
export const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, 'm');
|
|
412
507
|
|
|
413
508
|
/**
|
|
414
509
|
* SHAPE 2 (lower precedence) — the measured miss class: a declaration written
|
|
@@ -436,7 +531,7 @@ const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, 'im');
|
|
|
436
531
|
* one false extraction (`[".filter"]`) is a prose fragment, which is why the
|
|
437
532
|
* fenced shape keeps precedence and this one runs only when that found nothing.
|
|
438
533
|
*/
|
|
439
|
-
const INLINE_SCOPE_DECL = new RegExp(`(${SCOPE_TERMS})`, '
|
|
534
|
+
const INLINE_SCOPE_DECL = new RegExp(`(${SCOPE_TERMS})`, 'g');
|
|
440
535
|
const INLINE_DECL_OPERATOR = /^[^\n(:—–]{0,2}(\([^)\n]{0,80}\))?\s*(?:[—–][^\n:]{0,30})?\s*(:|—|–)/;
|
|
441
536
|
|
|
442
537
|
/** Separators a coordinator uses between paths in an inline declaration. */
|
|
@@ -909,6 +1004,163 @@ export function listTrackedFiles(cwd) {
|
|
|
909
1004
|
}
|
|
910
1005
|
}
|
|
911
1006
|
|
|
1007
|
+
// ---------------------------------------------------------------------------
|
|
1008
|
+
// Stale worktree base (#1413) — a WARNING, never a decision
|
|
1009
|
+
//
|
|
1010
|
+
// `isolation: "worktree"` makes the HARNESS create `<repo>/.claude/worktrees/
|
|
1011
|
+
// agent-<hex>`; no code in this repo creates it and the `Agent` payload carries
|
|
1012
|
+
// no base-ref field (the 147-dispatch key census in the module docblock). The
|
|
1013
|
+
// base is the SESSION-START commit, and it does not follow a mid-session commit:
|
|
1014
|
+
// measured 2026-09-19 (session s18), worktrees created 25 minutes AFTER commit
|
|
1015
|
+
// `240efda6` still stood on its parent `8f15f77b`. A fix agent then silently
|
|
1016
|
+
// edits the OLD code, and its test run is structurally red on top — `validate-
|
|
1017
|
+
// plugin` is vitest's globalSetup and `check-guard-requires-parity.mjs` compares
|
|
1018
|
+
// against `git show HEAD:` inside the stale worktree.
|
|
1019
|
+
//
|
|
1020
|
+
// We have no lever on the base, so the only available fix is to be LOUD. This
|
|
1021
|
+
// never touches the verdict: the two functions below are TOTAL (every failure
|
|
1022
|
+
// resolves to silence), and the caller ignores their result for the decision.
|
|
1023
|
+
// ---------------------------------------------------------------------------
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Facts for the worktree-base check, as a DISCRIMINATED record with three
|
|
1027
|
+
* outcomes rather than two (#1424):
|
|
1028
|
+
*
|
|
1029
|
+
* - `{stale: true|false, head, session_start_ref, subagent_type?}` — measured.
|
|
1030
|
+
* - `{stale: null, skipped: <reason>}` — the dispatch WAS on the `worktree`
|
|
1031
|
+
* branch, but the question could not be answered honestly. Still emitted,
|
|
1032
|
+
* because HR-105 needs the denominator: while every such case returned bare
|
|
1033
|
+
* `null` and the caller emitted nothing, "the check never had anything to
|
|
1034
|
+
* say" and "the check was silently disarmed" both read as zero records —
|
|
1035
|
+
* precisely the shape this event exists to close.
|
|
1036
|
+
* - `null` — `isolation !== 'worktree'`. SILENT by construction, and the one
|
|
1037
|
+
* outcome that must stay so: `isolation` appeared in 14 of 147 measured
|
|
1038
|
+
* dispatches, so recording the other ~90% would put the denominator on the
|
|
1039
|
+
* whole hot dispatch path (HR-101).
|
|
1040
|
+
*
|
|
1041
|
+
* IDENTITY GATE (`.claude/rules/identity-and-locks.md`): `session-start-ref`
|
|
1042
|
+
* lives in STATE.md, a SHARED working-copy artefact that routinely belongs to a
|
|
1043
|
+
* peer session. It is read only when STATE.md's own `session-id` equals this
|
|
1044
|
+
* process's raw id (hook payload `session_id`, else `CLAUDE_CODE_SESSION_ID`) —
|
|
1045
|
+
* a process-local witness REPLACES the shared one, never unions with it. On
|
|
1046
|
+
* mismatch, absence or any read failure this reports `stale: null`: a confident
|
|
1047
|
+
* warning about somebody else's session is worse than no warning at all. The
|
|
1048
|
+
* test runs PER CANDIDATE (`continue`, not `break`) — see the loop below.
|
|
1049
|
+
*
|
|
1050
|
+
* `parseStateMd` is imported DYNAMICALLY and only on the rare `worktree` branch
|
|
1051
|
+
* (14 of 147 measured dispatches carried `isolation` at all): it is a leaf module
|
|
1052
|
+
* with zero imports and already inside the committed hook-import closure, so
|
|
1053
|
+
* reuse costs no new edge, and binding it late keeps a load failure catchable
|
|
1054
|
+
* instead of disarming the guard at ESM link time (§ #993 late-bound deps).
|
|
1055
|
+
*
|
|
1056
|
+
* @param {{tool_input?: unknown}} input — the raw hook payload
|
|
1057
|
+
* @param {string} projectDir
|
|
1058
|
+
* @param {string} sessionId — `input.session_id`, or `'no-session'`
|
|
1059
|
+
* @returns {Promise<{head: string, session_start_ref: string, stale: boolean,
|
|
1060
|
+
* subagent_type?: string}|{stale: null, skipped: string}|null>}
|
|
1061
|
+
*/
|
|
1062
|
+
async function worktreeBaseFacts(input, projectDir, sessionId) {
|
|
1063
|
+
// The applicability gate sits OUTSIDE the try on purpose: everything below it
|
|
1064
|
+
// resolves to a `skipped` RECORD, so a throw here must not be able to mint one
|
|
1065
|
+
// for a dispatch that was never on the `worktree` branch at all.
|
|
1066
|
+
const toolInput = input?.tool_input;
|
|
1067
|
+
if (toolInput === null || typeof toolInput !== 'object') return null;
|
|
1068
|
+
if (toolInput.isolation !== 'worktree') return null;
|
|
1069
|
+
|
|
1070
|
+
try {
|
|
1071
|
+
const ownId = sessionId && sessionId !== 'no-session'
|
|
1072
|
+
? sessionId
|
|
1073
|
+
: (process.env.CLAUDE_CODE_SESSION_ID || '');
|
|
1074
|
+
if (!ownId) return { stale: null, skipped: 'no-own-id' };
|
|
1075
|
+
|
|
1076
|
+
const { parseStateMd } = await import(
|
|
1077
|
+
pathToFileURL(path.join(PLUGIN_ROOT, 'scripts', 'lib', 'state-md', 'yaml-parser.mjs')).href
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
// #1424: the identity test lives INSIDE the loop. Breaking at the first
|
|
1081
|
+
// PARSEABLE STATE.md and testing `session-id` afterwards meant a single
|
|
1082
|
+
// left-over `.pi/STATE.md` — `.pi` sorts before `.claude` in
|
|
1083
|
+
// STATE_DIR_CANDIDATES — disarmed the check for a session whose own
|
|
1084
|
+
// `.claude/STATE.md` sat right there, readable and matching. `sawStateMd`
|
|
1085
|
+
// keeps the two silences apart afterwards: "nothing to read" is a different
|
|
1086
|
+
// fact from "read, and none of it was mine".
|
|
1087
|
+
let frontmatter = null;
|
|
1088
|
+
let sawStateMd = false;
|
|
1089
|
+
for (const dir of STATE_DIR_CANDIDATES) {
|
|
1090
|
+
try {
|
|
1091
|
+
const parsed = parseStateMd(readFileSync(path.join(projectDir, dir, 'STATE.md'), 'utf8'));
|
|
1092
|
+
if (!parsed?.frontmatter) continue;
|
|
1093
|
+
sawStateMd = true;
|
|
1094
|
+
if (parsed.frontmatter['session-id'] !== ownId) continue;
|
|
1095
|
+
frontmatter = parsed.frontmatter;
|
|
1096
|
+
break;
|
|
1097
|
+
} catch { /* try the next state dir */ }
|
|
1098
|
+
}
|
|
1099
|
+
if (frontmatter === null) {
|
|
1100
|
+
return { stale: null, skipped: sawStateMd ? 'identity-mismatch' : 'no-state-md' };
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const startRef = typeof frontmatter['session-start-ref'] === 'string'
|
|
1104
|
+
? frontmatter['session-start-ref'].trim()
|
|
1105
|
+
: '';
|
|
1106
|
+
if (startRef === '') return { stale: null, skipped: 'no-start-ref' };
|
|
1107
|
+
|
|
1108
|
+
let head = '';
|
|
1109
|
+
try {
|
|
1110
|
+
head = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
1111
|
+
cwd: projectDir,
|
|
1112
|
+
encoding: 'utf8',
|
|
1113
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1114
|
+
}).trim();
|
|
1115
|
+
} catch {
|
|
1116
|
+
return { stale: null, skipped: 'git-error' };
|
|
1117
|
+
}
|
|
1118
|
+
if (head === '') return { stale: null, skipped: 'git-error' };
|
|
1119
|
+
|
|
1120
|
+
const facts = { head, session_start_ref: startRef, stale: head !== startRef };
|
|
1121
|
+
if (typeof toolInput.subagent_type === 'string' && toolInput.subagent_type !== '') {
|
|
1122
|
+
facts.subagent_type = toolInput.subagent_type;
|
|
1123
|
+
}
|
|
1124
|
+
return facts;
|
|
1125
|
+
} catch {
|
|
1126
|
+
// Total by construction: no parser, an unreadable state dir, anything else.
|
|
1127
|
+
// Still a RECORD rather than silence — the dispatch WAS on the `worktree`
|
|
1128
|
+
// branch, and a broken probe that emits nothing is indistinguishable from a
|
|
1129
|
+
// probe that had nothing to report (#1424 / HR-105).
|
|
1130
|
+
return { stale: null, skipped: 'probe-error' };
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* The operator-facing warning. It names the ACTION, not just the condition —
|
|
1136
|
+
* a warning the coordinator cannot act on is noise (HR-106).
|
|
1137
|
+
*
|
|
1138
|
+
* NO `⚠ ` PREFIX HERE: this text is handed to `emitWarn`, which prefixes it on
|
|
1139
|
+
* both channels it writes (stderr and the `systemMessage` payload). Prefixing
|
|
1140
|
+
* here too produced `⚠ ⚠ …`.
|
|
1141
|
+
*
|
|
1142
|
+
* @param {{head: string, session_start_ref: string}} facts
|
|
1143
|
+
* @returns {string}
|
|
1144
|
+
*/
|
|
1145
|
+
function staleWorktreeWarning(facts) {
|
|
1146
|
+
const head = facts.head.slice(0, 12);
|
|
1147
|
+
const base = facts.session_start_ref.slice(0, 12);
|
|
1148
|
+
return [
|
|
1149
|
+
`${HOOK_NAME}: STALE WORKTREE BASE (#1413) — this dispatch uses `
|
|
1150
|
+
+ `isolation: "worktree", but HEAD (${head}) has moved past this session's `
|
|
1151
|
+
+ `session-start-ref (${base}).`,
|
|
1152
|
+
' The harness bases a new agent worktree on the SESSION-START commit and offers',
|
|
1153
|
+
' no base-ref field, so the agent will silently get the OLDER code — and its test',
|
|
1154
|
+
' run is structurally red on top (check-guard-requires-parity compares against',
|
|
1155
|
+
' `git show HEAD:`, and validate-plugin is vitest\'s globalSetup).',
|
|
1156
|
+
' DO ONE OF: dispatch this wave IN-PLACE (omit `isolation`) — or verify the base',
|
|
1157
|
+
' before trusting the agent\'s result:',
|
|
1158
|
+
' git worktree list --porcelain | awk -v h="$(git rev-parse HEAD)" '
|
|
1159
|
+
+ '\'/^worktree /{w=$2} /^HEAD /{if (w ~ /\\.claude\\/worktrees\\/agent-/ && $2 != h) '
|
|
1160
|
+
+ 'print "STALE " substr($2,1,12) " " w}\'',
|
|
1161
|
+
].join('\n');
|
|
1162
|
+
}
|
|
1163
|
+
|
|
912
1164
|
/**
|
|
913
1165
|
* Clip a path for the deny reason without losing the discriminating tail.
|
|
914
1166
|
*
|
|
@@ -980,6 +1232,69 @@ export function bumpSignalCounter(ledger, waveKey, status) {
|
|
|
980
1232
|
return next;
|
|
981
1233
|
}
|
|
982
1234
|
|
|
1235
|
+
/**
|
|
1236
|
+
* The digest half of the per-dispatch record (#1092) — the field set that makes
|
|
1237
|
+
* the SEND side joinable to the RECEIVE side.
|
|
1238
|
+
*
|
|
1239
|
+
* Four facts, all derived from the PROMPT alone, none of them a path:
|
|
1240
|
+
*
|
|
1241
|
+
* `scope_digest` — `scopeDigest()` over the paths extracted FROM THE
|
|
1242
|
+
* PROMPT. This is the join key `scope-echo --verify`
|
|
1243
|
+
* uses; `agent_id` cannot be one, because the two
|
|
1244
|
+
* halves spell it differently (measured 2026-09-16:
|
|
1245
|
+
* 609 send-side vs 51 receive-side records, agent-id
|
|
1246
|
+
* overlap ZERO — send writes
|
|
1247
|
+
* `"i-3 #1353 … (session-orchestrator:code-implementer)"`,
|
|
1248
|
+
* receive writes `"i-3"`).
|
|
1249
|
+
* `echo_instruction_present`— the coordinator appended the echo line at all.
|
|
1250
|
+
* `instructed_digest` — the digest THAT LINE names.
|
|
1251
|
+
* `digest_consistent` — the two agree. `false` is agent A's fenced block
|
|
1252
|
+
* beside agent B's echo line, caught at dispatch
|
|
1253
|
+
* time with no filesystem read.
|
|
1254
|
+
*
|
|
1255
|
+
* ABSENT IS NOT ZERO, in both directions (`docs/events-schema.md`):
|
|
1256
|
+
* `scope_digest` is OMITTED for an empty scope — never the digest of the empty
|
|
1257
|
+
* string, which is a real 8-hex value and would join marker-absent Discovery
|
|
1258
|
+
* dispatches to each other. `instructed_digest` is omitted when no line was
|
|
1259
|
+
* found, and `digest_consistent` unless BOTH are present.
|
|
1260
|
+
*
|
|
1261
|
+
* TOTAL by construction: every branch is wrapped, because this runs on the
|
|
1262
|
+
* decision path of a deny-capable hook. A throwing digest function must cost the
|
|
1263
|
+
* FIELD, never the verdict — same discipline as the awaited-and-caught emit.
|
|
1264
|
+
*
|
|
1265
|
+
* @param {string[]} files paths extracted from the prompt
|
|
1266
|
+
* @param {unknown} prompt the dispatch prompt
|
|
1267
|
+
* @param {((paths: string[]) => string)} [digestFn] injectable for tests; defaults
|
|
1268
|
+
* to the late-bound `scopeDigest` (undefined when `bootstrap()` has not run,
|
|
1269
|
+
* which this function treats exactly like a throwing one — the field is omitted)
|
|
1270
|
+
* @returns {{scope_digest?: string, echo_instruction_present: boolean,
|
|
1271
|
+
* instructed_digest?: string, digest_consistent?: boolean}}
|
|
1272
|
+
*/
|
|
1273
|
+
export function scopeDigestFields(files, prompt, digestFn) {
|
|
1274
|
+
/** @type {Record<string, unknown>} */
|
|
1275
|
+
const out = {};
|
|
1276
|
+
try {
|
|
1277
|
+
const fn = typeof digestFn === 'function' ? digestFn : scopeDigest;
|
|
1278
|
+
if (typeof fn === 'function' && Array.isArray(files) && files.length > 0) {
|
|
1279
|
+
const digest = fn(files);
|
|
1280
|
+
if (typeof digest === 'string' && DIGEST_RE.test(digest)) out.scope_digest = digest;
|
|
1281
|
+
}
|
|
1282
|
+
} catch { /* a broken digest costs the field, never the decision */ }
|
|
1283
|
+
|
|
1284
|
+
let instructed = null;
|
|
1285
|
+
try {
|
|
1286
|
+
const match = typeof prompt === 'string' ? ECHO_INSTRUCTION_RE.exec(prompt) : null;
|
|
1287
|
+
if (match !== null) instructed = match[1].toLowerCase();
|
|
1288
|
+
} catch { instructed = null; }
|
|
1289
|
+
|
|
1290
|
+
out.echo_instruction_present = instructed !== null;
|
|
1291
|
+
if (instructed !== null) out.instructed_digest = instructed;
|
|
1292
|
+
if (typeof out.scope_digest === 'string' && instructed !== null) {
|
|
1293
|
+
out.digest_consistent = out.scope_digest === instructed;
|
|
1294
|
+
}
|
|
1295
|
+
return /** @type {any} */ (out);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
983
1298
|
/**
|
|
984
1299
|
* @typedef {{action: 'allow'|'deny'|'warn', reason?: string, suggestion?: string,
|
|
985
1300
|
* ledger?: object|null, note?: string, telemetry?: object}} Verdict
|
|
@@ -997,9 +1312,11 @@ export function bumpSignalCounter(ledger, waveKey, status) {
|
|
|
997
1312
|
* @param {Function} params.collide `findScopeCollisions` (injected for testability)
|
|
998
1313
|
* @param {(entry: object) => boolean} [params.isFinished] liveness probe (§ Liveness)
|
|
999
1314
|
* @param {string} [params.nowIso] dispatch timestamp recorded on the entry
|
|
1315
|
+
* @param {(paths: string[]) => string} [params.digestFn] scope-digest function
|
|
1316
|
+
* (injected for testability; defaults to the late-bound `scopeDigest`)
|
|
1000
1317
|
* @returns {Verdict}
|
|
1001
1318
|
*/
|
|
1002
|
-
export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, collide, isFinished, nowIso }) {
|
|
1319
|
+
export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, collide, isFinished, nowIso, digestFn }) {
|
|
1003
1320
|
const toolName = input?.tool_name;
|
|
1004
1321
|
// Row 4: not our tool.
|
|
1005
1322
|
if (toolName !== DISPATCH_TOOL) return { action: 'allow' };
|
|
@@ -1023,13 +1340,19 @@ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, coll
|
|
|
1023
1340
|
// agent id, never a path, never a byte of the prompt (issue #1092 acceptance
|
|
1024
1341
|
// criterion 3). `wave` is omitted rather than zeroed when unknown.
|
|
1025
1342
|
const wave = waveNumberOf(waveKey);
|
|
1343
|
+
const digestFields = scopeDigestFields(files, toolInput.prompt, digestFn);
|
|
1026
1344
|
const telemetryFor = (ledgerResult, collisionCount = 0) => ({
|
|
1027
1345
|
...(wave === null ? {} : { wave }),
|
|
1028
1346
|
agent_id: id.slice(0, MAX_AGENT_ID_CHARS),
|
|
1029
1347
|
declared_path_count: files.length,
|
|
1030
1348
|
injected: files.length > 0,
|
|
1349
|
+
// `marker_found` is NOT a second spelling of `injected`: a fenced block whose
|
|
1350
|
+
// lines are prose is `marker_found: true, injected: false` (matrix row 6),
|
|
1351
|
+
// which is the row-5-vs-row-6 split expressed as a boolean a query can group by.
|
|
1352
|
+
marker_found: signal.status !== SIGNAL_MARKER_ABSENT,
|
|
1031
1353
|
shape: signal.shape,
|
|
1032
1354
|
signal: signal.status,
|
|
1355
|
+
...digestFields,
|
|
1033
1356
|
ledger_result: ledgerResult,
|
|
1034
1357
|
collision_count: collisionCount,
|
|
1035
1358
|
});
|
|
@@ -1297,8 +1620,59 @@ async function main() {
|
|
|
1297
1620
|
} catch { /* observability is best-effort — it never blocks the decision */ }
|
|
1298
1621
|
}
|
|
1299
1622
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1623
|
+
// #1413 — the stale-worktree-base warning, also awaited BEFORE the terminal
|
|
1624
|
+
// emit for the same process.exit() reason as the block above. It reads
|
|
1625
|
+
// `verdict` not at all: it can never turn an allow into a deny, and a git or
|
|
1626
|
+
// STATE.md failure resolves to silence rather than to a wrong accusation.
|
|
1627
|
+
//
|
|
1628
|
+
// ROUTED TO THE OPERATOR, not to stderr alone (w3-5): under the exit-0
|
|
1629
|
+
// PreToolUse protocol stderr goes nowhere — `io.mjs:545` calls it the "Debug
|
|
1630
|
+
// channel … Invisible under exit 0" — so the first cut of this check announced
|
|
1631
|
+
// the s18 incident class to a log and to nobody. The visible channel is the
|
|
1632
|
+
// top-level `systemMessage`, i.e. `emitWarn`, which carries NO
|
|
1633
|
+
// `permissionDecision` and therefore still means ALLOW (io.mjs § "which is
|
|
1634
|
+
// precisely what keeps warn non-blocking").
|
|
1635
|
+
//
|
|
1636
|
+
// The note is CARRIED to the single terminal emit below rather than emitted
|
|
1637
|
+
// here, because `emitWarn` calls `process.exit(0)` and never returns — warning
|
|
1638
|
+
// inline would terminate the process before a collision DENY could be emitted,
|
|
1639
|
+
// flipping a block into an allow (§ stdout discipline, the same reason
|
|
1640
|
+
// `decide()` is pure).
|
|
1641
|
+
//
|
|
1642
|
+
// `null` here means "not a worktree dispatch" and stays silent; ANY other
|
|
1643
|
+
// shape is emitted, including the `{stale: null, skipped: …}` non-measurements
|
|
1644
|
+
// (#1424). The warning still fires on `stale === true` alone — a skipped probe
|
|
1645
|
+
// accuses nobody.
|
|
1646
|
+
const worktreeBase = await worktreeBaseFacts(input, projectDir, sessionId);
|
|
1647
|
+
let staleNote = null;
|
|
1648
|
+
if (worktreeBase !== null) {
|
|
1649
|
+
if (worktreeBase.stale === true) staleNote = staleWorktreeWarning(worktreeBase);
|
|
1650
|
+
try {
|
|
1651
|
+
const { emitEvent, sessionAttribution } = await import(
|
|
1652
|
+
pathToFileURL(path.join(PLUGIN_ROOT, 'scripts', 'lib', 'events.mjs')).href
|
|
1653
|
+
);
|
|
1654
|
+
await emitEvent(
|
|
1655
|
+
WORKTREE_BASE_EVENT,
|
|
1656
|
+
{ hook: HOOK_NAME, ...worktreeBase, ...sessionAttribution(projectDir) },
|
|
1657
|
+
{ repoRoot: projectDir }
|
|
1658
|
+
);
|
|
1659
|
+
} catch { /* observability is best-effort — it never blocks the decision */ }
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// ONE terminal emit, with the #1413 note folded in where a visible channel is
|
|
1663
|
+
// free. On DENY the dispatch does not happen and `systemMessage` already
|
|
1664
|
+
// carries the ⛔ headline, so the note stays on the debug channel — a stale
|
|
1665
|
+
// base is moot for a dispatch that was just blocked.
|
|
1666
|
+
if (verdict.action === 'deny') {
|
|
1667
|
+
if (staleNote !== null) {
|
|
1668
|
+
try { console.error(`⚠ ${staleNote}`); } catch { /* stderr may be closed */ }
|
|
1669
|
+
}
|
|
1670
|
+
return emitDeny(verdict.reason, verdict.suggestion);
|
|
1671
|
+
}
|
|
1672
|
+
if (verdict.action === 'warn') {
|
|
1673
|
+
return emitWarn(staleNote === null ? verdict.note : `${verdict.note}\n\n${staleNote}`);
|
|
1674
|
+
}
|
|
1675
|
+
if (staleNote !== null) return emitWarn(staleNote);
|
|
1302
1676
|
return emitAllow();
|
|
1303
1677
|
}
|
|
1304
1678
|
|
|
@@ -1309,6 +1683,12 @@ async function main() {
|
|
|
1309
1683
|
// symlinked plugin install) while `import.meta.url` is realpath-resolved by
|
|
1310
1684
|
// node's default loader, so BOTH sides are realpath'd — the same comparison
|
|
1311
1685
|
// `hooks/post-bash-write-verify.mjs` documents (#938 MED-2).
|
|
1686
|
+
//
|
|
1687
|
+
// NAMED CEILING (BV-004): kept inline rather than imported — hooks avoid
|
|
1688
|
+
// importing `scripts/lib` on the hot path, where every added module is paid on
|
|
1689
|
+
// every dispatch. The canonical predicate is `scripts/lib/is-main-module.mjs`;
|
|
1690
|
+
// revisit if this hook ever imports that tree for another reason, at which
|
|
1691
|
+
// point the copy costs more than the import saves.
|
|
1312
1692
|
// ---------------------------------------------------------------------------
|
|
1313
1693
|
function invokedAsScript() {
|
|
1314
1694
|
const entry = process.argv[1];
|
|
@@ -37,6 +37,7 @@ import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
|
37
37
|
import { shouldDailyFlush } from '../scripts/lib/telemetry/sync.mjs';
|
|
38
38
|
import { resolveConsent, readTelemetryState } from '../scripts/lib/telemetry/consent.mjs';
|
|
39
39
|
import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
|
|
40
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
40
41
|
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
42
43
|
// Constants
|
|
@@ -209,7 +210,7 @@ async function main() {
|
|
|
209
210
|
// Self-execution guard — run only when invoked directly (not when imported).
|
|
210
211
|
// ---------------------------------------------------------------------------
|
|
211
212
|
|
|
212
|
-
const isMain =
|
|
213
|
+
const isMain =isMainModule(import.meta.url);
|
|
213
214
|
if (isMain) {
|
|
214
215
|
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
215
216
|
if (!shouldRunHook('skill-invocation-telemetry')) process.exit(0);
|
|
@@ -105,14 +105,14 @@
|
|
|
105
105
|
*/
|
|
106
106
|
|
|
107
107
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
108
|
-
|
|
109
|
-
if (!shouldRunHook('subagent-telemetry')) process.exit(0);
|
|
108
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
110
109
|
|
|
111
110
|
import fs from 'node:fs';
|
|
112
111
|
import path from 'node:path';
|
|
113
112
|
import { appendSubagent } from '../scripts/lib/subagents-schema.mjs';
|
|
114
113
|
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
115
114
|
import { resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
|
|
115
|
+
import { readTailWindow } from '../scripts/lib/tail-window.mjs';
|
|
116
116
|
|
|
117
117
|
// ---------------------------------------------------------------------------
|
|
118
118
|
// Constants
|
|
@@ -593,25 +593,18 @@ function resolveSubagentTranscriptPath(parentTranscriptPath, agentId) {
|
|
|
593
593
|
* @returns {number|null} epoch-ms of the matching start, or null
|
|
594
594
|
*/
|
|
595
595
|
function findStartTimestampMs(filePath, agentId) {
|
|
596
|
-
let fd;
|
|
597
596
|
try {
|
|
598
597
|
if (typeof agentId !== 'string' || !agentId.trim()) return null;
|
|
599
598
|
if (!fs.existsSync(filePath)) return null;
|
|
600
599
|
|
|
601
|
-
const
|
|
602
|
-
if (size === 0) return null;
|
|
600
|
+
const window = readTailWindow(filePath, START_JOIN_TAIL_BYTES);
|
|
601
|
+
if (window.size === 0) return null;
|
|
603
602
|
|
|
604
|
-
const
|
|
605
|
-
const from = size - readLen;
|
|
606
|
-
const buf = Buffer.allocUnsafe(readLen);
|
|
607
|
-
fd = fs.openSync(filePath, 'r');
|
|
608
|
-
fs.readSync(fd, buf, 0, readLen, from);
|
|
609
|
-
|
|
610
|
-
const lines = buf.toString('utf8').split('\n');
|
|
603
|
+
const lines = window.text.split('\n');
|
|
611
604
|
// When the window does not cover the whole file, the first element is a
|
|
612
605
|
// record sliced mid-line (possibly mid-UTF-8-sequence). Drop it rather than
|
|
613
606
|
// feed a corrupt fragment to JSON.parse.
|
|
614
|
-
if (
|
|
607
|
+
if (window.cut) lines.shift();
|
|
615
608
|
|
|
616
609
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
617
610
|
const line = lines[i].trim();
|
|
@@ -631,10 +624,6 @@ function findStartTimestampMs(filePath, agentId) {
|
|
|
631
624
|
return null;
|
|
632
625
|
} catch {
|
|
633
626
|
return null;
|
|
634
|
-
} finally {
|
|
635
|
-
if (fd !== undefined) {
|
|
636
|
-
try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
637
|
-
}
|
|
638
627
|
}
|
|
639
628
|
}
|
|
640
629
|
|
|
@@ -815,5 +804,12 @@ async function main() {
|
|
|
815
804
|
await appendSubagent(jsonlPath(), record);
|
|
816
805
|
}
|
|
817
806
|
|
|
818
|
-
//
|
|
819
|
-
|
|
807
|
+
// Entry guard (#1393): run only as the node script the harness execs — a bare
|
|
808
|
+
// `import()` must run no handler and must not exit the importing process.
|
|
809
|
+
if (isMainModule(import.meta.url)) {
|
|
810
|
+
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
811
|
+
if (!shouldRunHook('subagent-telemetry')) process.exit(0);
|
|
812
|
+
|
|
813
|
+
// Exit 0 always — informational hook must never block Claude.
|
|
814
|
+
main().catch(() => {}).finally(() => process.exit(0));
|
|
815
|
+
}
|