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
package/docs/events-schema.md
CHANGED
|
@@ -89,22 +89,24 @@ namespace we own. The validator + regex live in `scripts/lib/events-schema.mjs`
|
|
|
89
89
|
|---|---|---|
|
|
90
90
|
| `orchestrator.session.started` | `hooks/on-session-start.mjs` | SessionStart. **Optional, additive:** `peers_superseded` (number, GH#67) — the count of mechanically-detected peers (`mechanicalPeers`) whose `lockSuperseded === true` (a LIVE lock at this root is held by a different raw session_id than that registry-only peer — a HINT, not a verdict; see the GH#67 discussion above). Computed as `mechanicalPeersSuperseded` and rendered inline per peer via `supersessionMarker()` in the banner text. `peer_count` is deliberately left unchanged by this addition, so the supersession rate is measurable (`peers_superseded` / `peer_count`) instead of only inferred from banner prose (HR-105). **Optional, additive (#1091, 2026-09-09):** `native_source` (string) — the harness-supplied SessionStart `source` (`startup` \| `resume` \| `clear` \| `compact` on Claude Code; other harnesses' enums pass through verbatim), OMITTED when stdin carries no `source`, never `null`/`""`. Measurement only: it makes "does the same raw `session_id` repeat under `source: resume`?" answerable from this ledger; no continuity logic reads it. The Claude Code matcher in `hooks/hooks.json` was widened to `startup\|resume\|clear\|compact` in the same change — before it, a native resume never invoked this hook at all. **Consequently, since this matcher widening (2026-09-09), this row fires once per SessionStart SOURCE, not once per logical session** — a session that resumes N times contributes N rows, so count DISTINCT `session_id` values to count sessions, and read `native_source` to tell which source produced each row. Companion field `resume_linkage` (`raw-id` \| `semantic` \| `none`, omitted together with `native_source`): `raw-id` = the stdin `session_id` equals the one in `current-session.json`, so the prior semantic id is REUSED and the wave high-water marks (`last_wave`, `last_batch`, `wave_start_sha`) are preserved; `semantic` = the pre-existing semantic-match branch; `none` = unverified restart, fresh id, no preservation (the #1091 contract: never guess continuity) |
|
|
91
91
|
| `orchestrator.session.ended` | `hooks/on-session-end.mjs` | SessionEnd. **Payload:** `reason` (always), plus `session_id` / `semantic_session_id` / `duration_ms` — **each OMITTED, never fabricated, when it could not be measured** (#1068 AC1; `duration_ms` since the W5 F1 sweep). `duration_ms` is written ONLY when the ending session IS the one `.orchestrator/current-session.json` records (the `isRecordedSession` predicate, decided on the RAW stdin UUID) AND that file's start timestamp parsed. Until that fix it fell back to a hard `0`: **1082 of 1498** fleet records (72,2 % — 415 nonzero, 1 key absent, measured 2026-09-02) carried a zero that reads as a MEASURED zero-length session and is indistinguishable from one. An ABSENT `duration_ms` means NOT MEASURED, never "instant" — same omit-never-fabricate contract as `session.stopped` / `agent.stopped` above |
|
|
92
|
+
| `orchestrator.telemetry.flush` | `hooks/on-session-end.mjs` (`flushTelemetry()`; result shaped by `classifyFlush()`) | SessionEnd, exactly once per teardown and LAST (#1138) — after backfill, lock release and deregistration are durable, because it is the only step that may touch the network. Emitted whatever the outcome. **Payload:** exactly `outcome`, `reason` and `reason_class` — no queue payload, no anon id — plus the `session_id` / `semantic_session_id` / `wave` / `schema_version` envelope `emitEvent()` stamps. `outcome`: `sent`, `queued`, `gated` or `skipped`. `reason`: `flush()`'s own reason IN FULL, bounded to `FLUSH_REASON_MAX_CHARS` (200) — so a sandbox refusal is written as `sandbox:temp-root`, not `sandbox` — or `persistence-disabled` (Session Config `persistence: false`; `flush()` is never called), or `error` (`flush()` threw). `reason_class`: the head token before the first `:` (`sandbox`, `gated`, `build-error`, `persistence-disabled`, `error`), for aggregation. **Until #1392 (2026-09-20) `classifyFlush()` CUT `reason` at its first `:`, so the health banner's `startsWith('sandbox:')` predicate could never match a record from this emitter** — the wire is now proven by `tests/hooks/on-session-end.test.mjs` § "telemetry-flush breadcrumb → flush-health banner (wiring, #1392)", which drives a real `flush()` refusal through the real writer into the real banner. Readers: `scripts/lib/telemetry-flush-health-banner.mjs` via its `session-start-probes.mjs` registry entry (newest record; warns on a `reason` starting with `sandbox:`), and `scripts/lib/sessions-staleness-banner.mjs`, which SKIPS it as a closing diagnostic |
|
|
92
93
|
| `orchestrator.turn.stopped` | `hooks/on-stop.mjs` (`handleStop`) | Stop, once per ASSISTANT TURN. **The canonical name for this event since 2026-09-06 (GitLab #1234); payload identical to the deprecated `orchestrator.session.stopped` row below, minus its `deprecated` marker.** **Why the rename:** the old name says *session*, the emitter fires per *turn*. Measured 2026-09-06 over the 90-day fleet window: **15.538 records against 2.016 distinct `orchestrator.session.started` ids = 7,7 per session**, with **184 for a single id**. Six consumers read it as a session-lifecycle signal and were therefore wrong by that factor — any "sessions stopped" count derived from it is a turn count. **A turn is not a session, and the count is not a rate:** to count sessions, count `session.started` ids; to count closes, count `sessions.jsonl` records with `status: completed`. **Migration:** both names carry the same payload for one generation; the legacy name additionally carries `deprecated: true`. Removal of `orchestrator.session.stopped`: **2027-03-06**. Readers should switch the name they match on and change nothing else. **NOT affected:** the SubagentStop branch keeps emitting `orchestrator.agent.stopped` — a different event whose per-agent cardinality is correct |
|
|
93
94
|
| `orchestrator.session.stopped` | `hooks/on-stop.mjs` (`handleStop`) | Stop. **Payload:** `session_id`, `semantic_session_id`, `wave`, optional `branch` / `commit`, plus the pair `duration_ms` + `duration_source` (`stdin-start-ms` | `session-lock`). **The pair is written TOGETHER or omitted together — never `0`.** Until this change `duration_ms` was a hard `0` in **8.127 of 8.127** fleet records (measured 2026-09-02): the expression fell back to `0` because the harness never sends `start_ms`, and a fabricated zero reads as a MEASURED zero-length turn, indistinguishable from one. The span is now derived from `.orchestrator/session.lock` `started_at` — **ownership-gated on the RAW stdin `session_id`**, because a lock in this working copy routinely names a live PEER session (the resolved id is deliberately not used: it falls back to `current-session.json`, which is the foreign-identity inheritance the guard refuses). No owned, readable, parseable lock ⇒ both keys absent, which means NOT MEASURED and never "instant". **`duration_source` says WHICH span the number is, and the two are not the same quantity:** `session-lock` is SESSION-elapsed measured at this turn's end — Stop fires per TURN while `started_at` is stamped once per SESSION, so it GROWS MONOTONICALLY across a session's turns (the last Stop of a 3-hour session reports ~3 hours, not its final turn); `stdin-start-ms` is TURN-elapsed, the only first-party measurement of the turn itself, and the harness has never sent it. Do not sum `session-lock` spans over a session — that double-counts. **DEPRECATED since 2026-09-06 (GitLab #1234), removal 2027-03-06** — superseded by `orchestrator.turn.stopped` above, which carries the identical payload under the name that matches what the emitter actually measures. Every record emitted under this name since the rename additionally carries `deprecated: true`, so a reader can tell at a glance that it matched the legacy name; a record WITHOUT that key predates 2026-09-06. Both names are emitted from the same payload object, so they can never disagree |
|
|
94
|
-
| `orchestrator.session.backfill_completed` | `hooks/on-session-end.mjs` (`emitBackfillOutcome`) · `scripts/backfill-abandoned-sessions.mjs` (`emitBackfillCompleted`, the startup/CLI path, #1167) | SessionEnd, once per backfill call (#1068 AC2 — the backfill outcome is canonically queryable, the side-log is no longer the only result source) — and separately, once per record the startup/CLI path itself writes, which until #1167 wrote SILENTLY: the SessionEnd hook was the only emitter, so nothing distinguished "the backfill never ran" from "it ran at startup". Both producers mirror the same payload shape so one filter queries either. Payload: `kind` (`abandoned`\|`state-md-completed`), `action` (the backfill result action, e.g. `appended`\|`superseded`\|`skipped-already-recorded`\|`unknown`), plus `session_id`, `semantic_session_id`, `record_id`, `supersedes`, `reason` — each OMITTED when unknown, never `null`. Best-effort on both paths: emission is wrapped in try/catch so a telemetry failure never blocks the backfill it describes |
|
|
95
|
+
| `orchestrator.session.backfill_completed` | `hooks/on-session-end.mjs` (`emitBackfillOutcome`) · `scripts/backfill-abandoned-sessions.mjs` (`emitBackfillCompleted`, the startup/CLI path, #1167) | SessionEnd, once per backfill call (#1068 AC2 — the backfill outcome is canonically queryable, the side-log is no longer the only result source) — and separately, once per record the startup/CLI path itself writes, which until #1167 wrote SILENTLY: the SessionEnd hook was the only emitter, so nothing distinguished "the backfill never ran" from "it ran at startup". Both producers mirror the same payload shape so one filter queries either. Payload: `kind` (`abandoned`\|`state-md-completed`), `action` (the backfill result action, e.g. `appended`\|`superseded`\|`skipped-already-recorded`\|`skipped-key-occupied`\|`unknown`; `skipped-key-occupied` (#1388 P8) is the key-occupancy guard — the closing session's own identity was classified absent, yet a canonical non-stub record of a DIFFERENT session already holds `record_id`, so appending would have displaced it — distinct from `skipped-already-recorded`, which means this very session is already on file), plus `session_id`, `semantic_session_id`, `record_id`, `supersedes`, `reason` — each OMITTED when unknown, never `null`. Best-effort on both paths: emission is wrapped in try/catch so a telemetry failure never blocks the backfill it describes |
|
|
95
96
|
| `orchestrator.session.root_left` | `scripts/lib/session-transition.mjs` (`leaveSourceRoot`) | a session left a repo root for good — the process-boundary teardown of Worktree-Auto-Promotion (#1069), emitted into the OLD root's stream after `deregisterSelf()` + `release()`, whether or not either found anything. NOT emitted when the teardown ABORTED (invalid args, a lock owned by another session, an unparseable lock): the event asserts a root was left, and on those branches none was. Payload: `session_id`, `semantic_session_id` (OMITTED when unknown, never `null`), `from_root_hash` (`repoPathHash()` of the abandoned root — the SAME hash the session registry keys its entries by, so a departure joins to the entry it removed), `from_root_basename` (matching the registry's `repo_name`), `reason` (e.g. `worktree-promotion`). **Never the absolute root:** this payload also travels over the optional Clank webhook with no redaction, and an absolute root on a developer host is `/Users/<operator>/…` — same rule as `board_written` / `mirror_completed` and `relativeWorktreePath` in `worktree-pipeline.mjs`. Its absence beside a live-looking registry entry is the phantom-peer signature the event exists to make visible |
|
|
96
97
|
| `orchestrator.session.lock.acquired` | `hooks/_lib/lock-bootstrap.mjs` | SessionStart |
|
|
97
|
-
| `orchestrator.session.lock.released` | `hooks/on-session-end.mjs` · `scripts/lib/autopilot/worktree-pipeline.mjs` (`teardownWorktree`) · `scripts/lib/session-transition.mjs` (`leaveSourceRoot`) | after a `release()` that matched ownership (#952). Payload: `session_id`, `caller` (`on-session-end`\|`worktree-pipeline`\|`session-transition`), `outcome` (`deleted`\|`already-gone`), `verified`; hook-side additionally `lock_session_id`, `semantic_session_id`, `end_reason` (the SessionEnd reason — deliberately NOT `reason`, which the sibling `release_failed` uses for the failure reason); pipeline-side additionally `worktree_path`, `issue_iid`. The `session-transition` caller emits only this event and no `release_failed` sibling — a failed release there is reported to its caller as `{ ok: false, reason: 'lock-<reason>' }`, which the promotion prose must WARN on, so the stream is not the only witness. `outcome: 'already-gone'` means the lock had ALREADY vanished between `readLock()` and `release()` — the forensically interesting case, since a successful release previously left no trace at all and a missing lock was therefore indistinguishable from a lock someone else deleted (#914 residual 3) |
|
|
98
|
+
| `orchestrator.session.lock.released` | `hooks/on-session-end.mjs` · `scripts/lib/autopilot/worktree-pipeline.mjs` (`teardownWorktree`) · `scripts/lib/session-transition.mjs` (`leaveSourceRoot`) · `scripts/release-session-lock.mjs` (#1395, the verifying CLI that session-end Phase 3.8 invokes; `caller: 'session-end-phase-3-8'`, both ids passed EXPLICITLY because the lock is gone before the emit, and NO event on `outcome: 'absent'`) | after a `release()` that matched ownership (#952). **The fourth producer exists because Phase 3.8 was PROSE:** the coordinator deleted the lock by hand, so the SessionEnd hook later found `status: 'absent'` and emitted nothing — measured 2026-09-19 in this repo, 5 `lock.acquired` against 0 `lock.released`, and fleet-wide 110 terminal events against 702 acquisitions (15,7 %). Payload: `session_id`, `caller` (`on-session-end`\|`worktree-pipeline`\|`session-transition`\|`session-end-phase-3-8`), `outcome` (`deleted`\|`already-gone`), `verified`; hook-side additionally `lock_session_id`, `semantic_session_id`, `end_reason` (the SessionEnd reason — deliberately NOT `reason`, which the sibling `release_failed` uses for the failure reason); pipeline-side additionally `worktree_path`, `issue_iid`. The `session-transition` caller emits only this event and no `release_failed` sibling — a failed release there is reported to its caller as `{ ok: false, reason: 'lock-<reason>' }`, which the promotion prose must WARN on, so the stream is not the only witness. `outcome: 'already-gone'` means the lock had ALREADY vanished between `readLock()` and `release()` — the forensically interesting case, since a successful release previously left no trace at all and a missing lock was therefore indistinguishable from a lock someone else deleted (#914 residual 3) |
|
|
98
99
|
| `orchestrator.session.lock.release_failed` | `hooks/on-session-end.mjs` · `scripts/lib/autopilot/worktree-pipeline.mjs` (`teardownWorktree`) | ownership matched but `release()` did NOT delete the lock (#724). Payload: `session_id`, `reason` (`fs-error`\|`session-mismatch`\|`not-deleted`\|`threw`), `caller`; pipeline-side additionally `worktree_path`, `issue_iid` |
|
|
100
|
+
| `orchestrator.session.lock.read_anomaly` | `hooks/on-session-end.mjs` (teardown step (b), via `readLockDetailed()` from `scripts/lib/session-lock.mjs`) | SessionEnd, at most once, when `.orchestrator/session.lock` EXISTS but cannot be used: the read failed with anything but ENOENT (`unreadable` — e.g. EISDIR, EACCES), or the content does not parse as a lock (`corrupt`). Neither release nor reconciliation runs then; before this event an unusable lock was indistinguishable from "no lock". **Payload:** `session_id` (the ending session's id as `resolveSession()` resolves it — the raw stdin id, falling back to the one `.orchestrator/current-session.json` records; `null` when neither names one), `status` (`'unreadable'` or `'corrupt'`), and `error` for `unreadable` only (the raw `err.message` — for an open failure it contains the absolute lock path). Because the payload supplies `session_id`, `emitEvent()` stamps NO `semantic_session_id` (a caller key suppresses the whole correlation envelope). An error path, so 0 records is a correct reading (this repo's `events.jsonl{.1,}`, 2026-09-19: 0). `scripts/lib/session-transition.mjs` deliberately does NOT emit it — it returns `lock-unreadable` / `lock-corrupt` to a caller that must WARN. Read only by `scripts/lib/sessions-staleness-banner.mjs`, which SKIPS it as a closing diagnostic |
|
|
99
101
|
| `orchestrator.session.lock.reconcile_attempted` | `hooks/_lib/lock-reconcile.mjs` | SessionEnd, when NEITHER ownership check matched the recorded lock (#748). Payload: `session_id`, `action` (`reaped`\|`skipped`\|`unknown`), `reason` (e.g. `own-host-pid-alive`) |
|
|
100
102
|
| `orchestrator.session.lock.reaped` | `scripts/lib/lock-reaper.mjs` | a dead lease was reaped. Payload: `session_id`, `semantic_session_id`, `host`, `pid`, `age_hours`, `reap_mode`, `current_session` |
|
|
101
103
|
| `orchestrator.agent.stopped` | `hooks/on-stop.mjs` (`handleSubagentStop`) | SubagentStop, once per stopping subagent — the fleet's most frequent event (103.763 records / 19 repos, measured 2026-09-02). **Payload (#1190) — every field is OPTIONAL and the KEY IS OMITTED when the measurement could not be made; never `null`, never `'unknown'`, never a stand-in `0`/`false`:** `agent` (the stdin `agent_type`/`subagent_type`, trimmed) — **omitted when empty**, which is the #1190 fix itself: the previous `input?.agent_type ?? 'unknown'` never fired on the EMPTY STRING the harness actually sends, so 89.991 of the 103.763 historical records (86,7%) carry `agent: ""`. A consumer must read a MISSING `agent` as "the harness did not name the type", not as a broken emitter. `agent_id` (stdin `agent_id`\|`subagent_id`; opaque id, charset-guarded with `/^[A-Za-z0-9_-]{1,64}$/` before it is interpolated into any path). The remaining fields are derived from the agent's sidecar pair `<transcript-dir>/<parent-basename>/subagents/agent-<agent_id>.{jsonl,meta.json}` and are all omitted when that derivation is not possible (no `agent_id`, no `transcript_path`, or a rejected id): `transcript_found` (boolean — `false` here is a MEASURED absence, the probe ran; the key is ABSENT when it could not run), `tool_use_id` + `agent_type_meta` (from the `.meta.json` keys `toolUseId` / `agentType`; `agent_type_meta` is a SECOND witness for the type and is deliberately NOT merged into `agent`, so the empty-`agent_type` rate stays measurable), `duration_ms` + `duration_source` (`meta-birthtime` — the sidecar carries no spawn timestamp, so its birthtime IS the spawn moment; both keys omitted together when the stat fails, never a fabricated `0`), and `status` (`done`\|`partial`\|`blocked`\|`failed`\|`no-tests-needed`, from the last LINE-ANCHORED `STATUS:` marker in the final 64 KiB of the agent transcript — the anchoring rationale is `scripts/lib/wave-transcript-tail.mjs:105-112`, since a free-floating match fires on any agent that merely QUOTES the token). **`status` coverage is partial by measurement, not by accident: 61,7 %** — 71 `done` / 3 `partial` / 46 absent over **120 COMPLETED sidecars**, population: one operator's `~/.claude/projects/<this-repo>` directory, sidecars idle ≥ 30 min (`find … -path "*/subagents/agent-*.jsonl" -mmin +30 | head -120`), each read through `readStatusFromTranscriptTail`, measured 2026-09-02. Re-cut it with that command; the earlier "roughly a quarter to a half" came from an n=4 IN-FLIGHT sample and undercounted, because a running agent has not written its STATUS line yet. **An absent `status` means NOT FOUND, never success.** **Never the transcript text, the meta `description`, or an absolute path:** this payload also travels over the optional Clank webhook with no redaction — same rule as `board_written` / `session.root_left`. `session_id` / `semantic_session_id` / `schema_version` are stamped by `emitEvent()` |
|
|
102
|
-
| `orchestrator.memory.propose_invoked` | `hooks/pre-bash-memory-propose-audit.mjs` | PreToolUse(Bash) |
|
|
103
|
-
| `orchestrator.wave.
|
|
104
|
+
| `orchestrator.memory.propose_invoked` | `hooks/pre-bash-memory-propose-audit.mjs` | PreToolUse(Bash), once per `node … memory-propose.mjs …` invocation the G3 regex matches — observe-only, the hook never denies. **Payload:** `session_id` (or `null`), `wave`, `cwd`, `exit_code` (always `null` — a PreToolUse hook runs BEFORE the command), plus the #1415 argv summary: `command_hash` (**sha256 of the RAW command, truncated to 16 hex — same recipe as `enforce-commands` after `8f15f77b`; never the command text**), `flags_present` (string[], the subset of `memory-propose.mjs`'s OWN flag NAMES — `type`, `subject`, `insight`, `evidence`, `confidence`, `dry-run`, `file-paths`, `help` — present in the command, in that fixed order; built by intersecting a CLOSED list, never by scraping `--\S+`, so a flag-shaped VALUE can never be reported as a flag) and `argv_length` (number, the raw command's character count). Until 2026-09-21 this row carried `argv_truncated`: 512 characters of command TEXT, redacted only per-VALUE for five flags, whose own caveat named its gaps (`--insight=$VAR`, `--insight=$(cat secret)`, a secret written anywhere else on the line). It went into the TRACKED `events.jsonl` and, with the Clank webhook configured, onto the network. **Measured 2026-09-21** (`rg argv_truncated` over `scripts/ hooks/ skills/ docs/`): **zero production readers** — the field's only consumers were this hook's own tests. Pinned in `tests/hooks/pre-bash-memory-propose-audit.test.mjs` in BOTH directions (summary fields present AND `argv_truncated`/`command` undefined), plus a negative test that string-searches the serialized record for every sentinel value — so nobody puts the raw field back beside the hash |
|
|
105
|
+
| `orchestrator.wave.completed` | `hooks/post-tool-batch-wave-signal.mjs` · `hooks/on-session-end.mjs` (`emitFinalWaveCompleted`) | PostToolBatch — fires live via `.claude/wave-scope.json` `.wave` increase (mechanical fallback, #612), closing wave N at the N→N+1 transition; an explicit injected `wave_signal: 'wave-complete'` still takes precedence. **The former `started` sibling was REMOVED 2026-09-19** (operator decision, #1202 §11): it carried no measurement, its only reader was `scripts/lib/convergence-monitor.mjs`, and there it cost the one signal it could not feed — the batch hook wrote it in the same millisecond as completed{N}, so both shared a tail tick and it masked the (N-1, N) `shrinking_diff` pair. An injected `wave_signal: 'wave-start'` is still accepted and emits nothing; historical records stay in the ledgers and nothing reads them. **Re-emission ceiling (open):** a session that does not own `.orchestrator/current-session.json` never advances the marks below, so each of its batches re-emits completed{lastWave} until its own SessionStart takes the file over — measured 2026-09-19 as up to 579 emissions for one (session_id, wave_number) group. **Second `.completed` emitter (#1193):** the batch hook closes wave N-1 only at an N-1→N transition, so the LAST wave of every session never received a completion — measured fleet-wide 2026-09-02 as **296 gaps over 296 wave runs** (1018 started vs 722 completed), exactly one missing final completion per run. SessionEnd now emits it. **Payload of that record:** `wave_number` (the `current-session.json` `last_wave`), `reason: 'session-end'`, `emitted_by: 'on-session-end'`, plus `session_id` / `semantic_session_id` — **omitted when unattested**, never fabricated. **Idempotent** via the `last_wave_completed` high-water mark in `.orchestrator/current-session.json`, written by BOTH emitters and preserved across `/clear`+compact by `on-session-start.mjs`; `last_wave` absent or `0` emits nothing (an Express-Path or coordinator-direct session never batched, and zero waves is the correct reading, not a gap). **Two gates on the SessionEnd emitter, both load-bearing:** (a) OWNERSHIP — `current-session.json` is a single repo-global file describing whichever session most recently ran SessionStart, routinely a different still-live session in a shared working copy, so the emit reuses `resolveSession()`'s `isRecordedSession` predicate; when false it emits nothing AND writes nothing, since writing the marker into a peer's file would silence the peer's own SessionEnd and preserve this very gap on the wrong session. (b) REASON — `reason === 'clear'` **and `reason === 'resume'`** are SKIPPED: the SessionEnd matcher is empty, so `/clear` fires the hook mid-wave while the LOGICAL session continues, and `on-session-start.mjs` preserves `last_wave` / `last_wave_completed` across a resume of the SAME logical session exactly as it does across a clear — so both end the HARNESS session, not the logical one. Closing the live wave on either is premature, and the preserved marker would then suppress the real completion later. Resume is the MORE common of the two (fleet n = 1498 `session.ended`, 2026-09-02: 12 `resume` vs 9 `clear`). Deliberately SessionEnd-only (`on-stop.mjs` is not mirrored) so the closed-vs-abandoned split stays measurable. **Diff-size keys on the BATCH-HOOK emitter only (#980):** `files_changed` (integer) + `files_changed_source: 'worktree-vs-wave-start-sha'` (the only value emitted today; present iff `files_changed` is). Measurement: the DEDUPED union of `git diff --name-only <wave_start_sha>` and `git ls-files --others --exclude-standard`, run in the project dir at the N→N+1 transition, where `wave_start_sha` is the `git rev-parse HEAD` the batch hook persisted into `.orchestrator/current-session.json` when wave N was OPENED (same ownership gate as the wave keys; written as `null` when git is unreadable, so a previous wave's sha can never inflate the next count). Worktree-vs-sha rather than `<sha>..HEAD` because the coordinator commits at session close, not per wave — a commit-only diff reads 0 for every wave of a normal session. **Both keys are OPTIONAL and absent-is-not-zero:** any git failure, a 1.5 s timeout, or a missing `wave_start_sha` omits them, and `scripts/lib/convergence-monitor.mjs` reads an absent key as `null`, so the `shrinking_diff` signal simply does not fire (it never reads a fabricated 0). The `on-session-end.mjs` final-wave `.completed` carries NEITHER key by design — no wave-open transition runs there, so it has no start sha to measure against |
|
|
104
106
|
| `orchestrator.wave.final_refused` | `hooks/on-session-end.mjs` (`emitFinalWaveCompleted` → `emitFinalRefused`, via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`, wrapped in its OWN try/catch — independent of the caller's outer catch — so a telemetry failure on one refusal can never surface as a teardown failure) | the SIBLING event to `orchestrator.wave.completed` above (#1201 Part B / Discovery D8) — fires exactly once per SessionEnd in which the final `.completed` was NOT emitted, one row per refusal. Deliberately a SEPARATE event name rather than `.completed` carrying `emitted:false`: existing consumers of `.completed` treat every row as a finished wave, and overloading it would silently corrupt that count. **Payload:** `reason` (always present, closed enum — verified against `emitFinalWaveCompleted`'s own call sites) `not-recorded` \| `clear` \| `resume` \| `unreadable` \| `session-id-mismatch` \| `no-wave` \| `already-completed` \| `exception`, `emitted_by: 'on-session-end'` (always), plus `session_id` / `semantic_session_id` — omitted when `null`, never fabricated. **Optional, absent-is-not-zero:** `wave_number` — present ONLY on the `already-completed` reason, the sole call site that passes a resolved `last_wave` through to `emitFinalRefused`; every other reason, INCLUDING `no-wave`, omits it (that path never resolved a wave number at all — the function's own JSDoc pins this: "only when `last_wave` was resolved to a positive number before the refusal (currently only `already-completed`)"). Six of the eight reasons were previously SILENT refusal paths with no trace anywhere (`.claude/rules/host-resources.md` § HR-105: "a refusal that writes nothing is unfalsifiable") |
|
|
105
107
|
| `orchestrator.quality_gate.passed` / `.failed` | `scripts/run-quality-gate.mjs` (the gate CLI — live between waves) · `scripts/lib/quality-gate.mjs` (`emitGateEvent` inside `runQualityGateWithRetry`, reached ONLY under `verification-auto-fix.enabled: true` — default `false`, and `false` in this repo) | CLI: once per gate-CLI run. Library: once per `runQualityGateWithRetry` **call**, never per retry attempt (`attempts` carries that detail). The two paths never nest, so one run passes through exactly one emitter. **Payload (both):** `variant`, `exit_code`, plus `session_id` / `semantic_session_id` when `sessionAttribution()` finds a session lock (both omitted when it does not). CLI `variant` is the `--variant` value (`baseline`\|`incremental`\|`full-gate`\|`per-file`); the library pins `variant: 'auto-fix-loop'`. **CLI only:** `wave_number`. **Library only:** `attempts` (1…`maxRetries+1`) and `gate` (`lint`\|`typecheck`\|`test` — the fail-fast gate of the last attempt; omitted on the passing path). **Optional on both:** `counts: {passed, failed, total}`. **Absent is not zero — for both optional fields.** `counts` is admitted by the ONE shared policy `admitSuiteCounts()` (`scripts/lib/gates/gate-helpers.mjs`, #967 item 2), which returns `null` — never a zero triple — for an unmeasured or inconsistent input (test gate skipped/stubbed, fail-fast on lint or typecheck before the test step, no parseable `<N> passed` marker, or `passed + failed !== total`); both callers spread `...(counts ? { counts } : {})`, so the KEY is missing in those runs. A present `counts.failed: 0` therefore means "measured, zero failures", while an absent `counts` means "not measured" — reading a missing field as `0` mis-analyses the ledger in both directions. Same contract for `wave_number` (CLI, #966 step 1): resolved from the `.{pi,cursor,codex,claude}/wave-scope.json` sidecar, **omitted** — never `0` — when there is no sidecar or its `wave` is non-numeric/non-positive; a human running `npm run quality-gate` from a `git push` has no wave at all, so an invented wave 0 would have to be special-cased by every consumer. Note `total` is `passed + failed` and EXCLUDES skipped/todo (see `extractTestCounts`) |
|
|
106
108
|
| `orchestrator.scope.coordinator_carveout_allowed` | `hooks/enforce-scope.mjs` (coordinator carveout, #245 / #1361) | exactly once per ALLOWED write BY THE COORDINATOR into one of the harness-owned in-repo files the carveout covers — `.claude/STATE.md` and its `.codex/` / `.cursor/` / `.pi/` siblings, plus the exact relative path of the live `wave-scope.json` the hook itself just read. Since #1361 a payload carrying `agent_id` is a dispatched subagent: it gets no carveout and falls through to Gate 7, where a manifest that does not grant the path is a DENY (a subagent permitted to write `wave-scope.json` could rewrite its own file scope and disarm every later gate of the wave; STATE.md is coordinator-owned per `skills/_shared/state-ownership.md`). **Payload:** `hook`, `manifest` (the manifest path), `wave`, `file_path` (the project-RELATIVE, forward-slash-normalized path — the carveout set is in-repo by construction, so no host-local absolute path enters the ledger) and `discriminator` (`'coordinator' \| 'malformed' \| 'absent'` — same enum and same meanings as `orchestrator.scope.memory_dir_allowed`: `'coordinator'` = `agent_type` present without `agent_id`; `'malformed'` = an `agent_id` key present but unusable (number, object, array, blank string), a fail-open that must stay rare; `'absent'` = no `agent_id` key at all, the harness's documented main-thread shape. `'subagent'` never appears here BECAUSE a subagent gets no carveout and so emits no allow) — exactly the five keys the `emitEvent` call passes, with `{ repoRoot: projectRoot }` as options. **Fail-safe:** awaited BEFORE `emitAllow()` (which calls `process.exit()` and would discard a pending append) and wrapped in its own `try {} catch {}`, so a telemetry failure can never flip the decision. Before #1361 this branch was a bare `emitAllow()` with no log and no event, so whether it ever fired was unfalsifiable after the fact (HR-105) | <!-- path-check: example -->
|
|
107
|
-
| `orchestrator.scope.foreign_session_ignored` | `hooks/enforce-scope.mjs` (Gate 3b, #1123) · `hooks/enforce-commands.mjs` · `hooks/post-bash-write-verify.mjs` (both Gate 3b, #1153 P1) | exactly once per gated tool call while a FOREIGN-session `wave-scope.json` is live: the manifest's `session_id`/`semantic_session_id` provably name another session (legacy `session`/`semantic_session` still read, #1153 P2), so the hook stands down instead of enforcing. **Payload:** `hook`, `manifest` (path), `manifest_session` (string[]), `own_session` (string[]), `wave`; additionally `file_path` from `enforce-scope` (PreToolUse Edit/Write) and `
|
|
109
|
+
| `orchestrator.scope.foreign_session_ignored` | `hooks/enforce-scope.mjs` (Gate 3b, #1123) · `hooks/enforce-commands.mjs` · `hooks/post-bash-write-verify.mjs` (both Gate 3b, #1153 P1) | exactly once per gated tool call while a FOREIGN-session `wave-scope.json` is live: the manifest's `session_id`/`semantic_session_id` provably name another session (legacy `session`/`semantic_session` still read, #1153 P2), so the hook stands down instead of enforcing. **Payload:** `hook`, `manifest` (path), `manifest_session` (string[]), `own_session` (string[]), `wave`; additionally `file_path` from `enforce-scope` (PreToolUse Edit/Write) and `command_hash` from `enforce-commands` (PreToolUse Bash) — **sha256 auf 16 Hex gekuerzt, NIE das rohe Kommando.** Bis 2026-09-19 stand hier `command` im Klartext, und zwar als dokumentierter Vertrag, nicht als Versehen. Gemessen in EventDrop.at (#1140): **3.816 Zeilen** der GETRACKTEN `.orchestrator/metrics/events.jsonl` trugen ein rohes Kommando, darin **24 distinkte echte Produktions-Share-Codes** aus **17 fremden Kundenkonten** plus ein protokollierter `select access_pin_hash` — bei 23 dieser Events ist der Share-Code die VOLLSTAENDIGE Capability (`/event/<code>` oeffnet das Album ohne Anmeldung), und das Journal geht bei jedem Klon mit. Der Geschwisterhook `pre-bash-destructive-guard.mjs` fuehrte von Anfang an `command_hash` und sagt den Grund im eigenen Kopf; diese Zeile war die Ausnahme. Der Hash haelt das Ereignis zaehlbar und gruppierbar — genau die Eigenschaft, fuer die es laut `docs/scope-collision-guard.md` existiert; das Rohfeld hatte keinen Leser. Gepinnt in `tests/hooks/enforce-commands.test.mjs` in BEIDE Richtungen (Hash vorhanden UND `command` undefined), damit niemand das Rohfeld neben den Hash zuruecklegt. **Der zweite rohe Schreiber derselben Klasse — `hooks/pre-bash-staging-fence.mjs` (512 Zeichen rohes Kommando je Eintrag) — ist seit #1404 (2026-09-20) geschlossen:** der Fence speichert jetzt die Pfad-Operanden plus `command_hash`, nie den Kommandotext. Belegt mit einem Test-Secret im Kommando: `grep -c` auf die Fence-Datei findet es 0-mal, ebenso 0 Vorkommen eines `command`-Feldes. Der einzige Leser (`hooks/wave-scope-commit-guard.mjs`) vergleicht seitdem Pfadlisten statt den Rohtext per Regex zu durchsuchen — was nebenbei `git add -A` abdeckt, das der Regex-Leser nie erkannte. **Die DRITTE und letzte Stelle derselben Klasse — `hooks/pre-bash-memory-propose-audit.mjs` (`argv_truncated`: 512 Zeichen nur flag-redigiertes Kommando in `orchestrator.memory.propose_invoked`, also in die GETRACKTE `events.jsonl` und optional an den Webhook) — ist seit #1415 (2026-09-21) geschlossen, analog `8f15f77b` und #1404:** der Hook schreibt jetzt `command_hash` + `flags_present` + `argv_length`, nie den Kommandotext. Gemessen vor dem Fix: 0 Produktionsleser des Rohfelds. Damit ist die Klasse zu — kein Hook dieses Repos legt mehr rohen Kommandotext in ein Event oder eine Kontrolldatei. `post-bash-write-verify` (PostToolUse Bash) carries neither — it reports on the working tree, not on one tool input. Legacy manifests without a session-binding field never emit this — they stay enforced |
|
|
108
110
|
| `orchestrator.scope.memory_dir_allowed` | `hooks/enforce-scope.mjs` (Gate 5c, #1295) | exactly once per ALLOWED write BY THE COORDINATOR (#1352 — a payload carrying `agent_id` is a dispatched subagent, gets no carve-out and falls through to the normal gates) into THIS repo's harness auto-memory directory `~/.claude/projects/<encodeProjectDir(repoRoot)>/memory/` — the single out-of-repo carveout, evaluated only on the out-of-root branch (Gate 6) and only when a `wave-scope.json` manifest is live, so the in-repo gates are untouched. **Payload:** `hook`, `manifest` (the manifest path), `wave`, `file_path` (the REALPATH-resolved candidate) and, since #1352, `discriminator` (`'coordinator' \| 'malformed' \| 'absent'` — `'coordinator'` = `agent_type` present without `agent_id`; `'malformed'` = an `agent_id` key present but unusable (number, object, array, blank string), a fail-open that must stay rare; `'absent'` = no `agent_id` key at all, the harness's documented main-thread shape. `'subagent'` never appears here BECAUSE a subagent gets no carve-out and so emits no allow) — exactly the five keys the `emitEvent` call passes, with `{ repoRoot: projectRoot }` as options. **Fail-safe:** awaited BEFORE `emitAllow()` (which calls `process.exit()` and would discard a pending append) and wrapped in its own `try {} catch {}`, so a telemetry failure can never flip the decision. This row is the audit trail for the carveout: it is the only place an out-of-repo ALLOW becomes measurable after the fact |
|
|
109
111
|
| `orchestrator.scope.unbound_manifest` | `scripts/wave-scope-binding.mjs` (#1153 P4) | wave-executor § Scope Manifest, when the binding step resolves to `{}` — `attributionForRecord()` found no `.orchestrator/session.lock`, or the lock's `session_id` did not match this process's own identity, so the manifest about to be written names NOBODY. **Payload:** `wave` (number\|string\|null), `role` (string\|null), `reason` (currently only `no-confirmed-session-attribution`). Exactly one per invocation, and only on the unbound path — a bound binding emits nothing. An unbound manifest is the FAIL-CLOSED direction (it enforces against every session in the checkout) and is therefore otherwise silent; this event is what makes it countable instead of indistinguishable from a coordinator who skipped the step |
|
|
110
112
|
| `orchestrator.grounding.injected` | `scripts/compute-grounding-injection.sh` (via `scripts/emit-event.mjs`) | grounding injection, when `PERSISTENCE=true` |
|
|
@@ -118,18 +120,25 @@ namespace we own. The validator + regex live in `scripts/lib/events-schema.mjs`
|
|
|
118
120
|
| `orchestrator.express_path.evaluated` | `scripts/lib/express-path.mjs` (`evaluateExpressPath`, emit in `_emitEvaluated`; name const `EXPRESS_PATH_EVENT`) | once per Phase-8.5 evaluation — **on refusal as well as activation**. Until #1119 this was unrecordable twice over: `scripts/lib/config.mjs` discarded the `express-path` key **even when the block was present** (synthetic probe: 88 keys emitted, none of them this one), and the decision lived only in `skills/session-start/phase-8-5-express-path.md` prose, so it fired only when a coordinator read that prose. Ledger evidence, measured 2026-08-23 @ `34321bc` (a count, so read it as history, not as state): **0** express events at that point, against 22 of the last 30 sessions running with no wave at all — every one of them `housekeeping`, the exact population the path targets. **Payload:** `activated` (always, boolean), `reasons` (always — the BLOCKING codes on refusal, the satisfied ones on activation; nothing short-circuits, so a refusal names every blocker and a reader can tell whether trimming the issue list alone would have helped). **Optional, absent-is-not-zero:** `enabled`, `session_type`, `task_count`, `parallel_agents_required`, plus `session_id`/`semantic_session_id` via `sessionAttribution`. An unmeasured `sessionType` or `taskCount` fails CLOSED (`reasons: ['session-type-unknown','task-count-unknown']`) — defaulting unknown scope to 0 would activate a gate-skipping path on data nobody supplied. **Four inputs, not three:** activation condition 3 carries two clauses (`≤ 3 issues` AND no parallel agents), which both condition matrices list as a non-activating row. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling through to `SO_PROJECT_DIR` — that is the wave-1 incident of this session (a probe with an unexported var wrote a synthetic record into the real fleet ledger) made structurally impossible; a regression test reproduces it. `events.mjs` is imported lazily so `config.mjs`'s 48-file import graph does not gain `platform.mjs`, which runs filesystem walk-ups at module load |
|
|
119
121
|
| `orchestrator.foreign_dispatch.completed` | `scripts/lib/wave-executor/foreign-dispatch.mjs` (`dispatchForeign`, via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`) | once per foreign-model dispatch (#1150) — the replacement for `SubagentStop` telemetry, which cannot fire for a Bash-spawned `cursor-agent` child (no hook in the chain sees it). **Payload:** `model`, `role`, `ok`, `exit_code`, `timed_out`, `duration_s`, `changed_files` (count, tracked-modified ∪ untracked-new — `git diff` alone is blind to new files), `reason` (present on every refusal — `never-foreign-role`, `empty-diff`, `channel-unavailable`, `unsafe-*` — and on the failure classes of a completed run, so no failure class is reasonless), `hook_tampering` (tri-state: `true` = the child repointed/rewrote the shared `.git` hooks path, invalidates the run regardless of `ok`; `false` = fingerprint matched; absent/`null` = not measured, never read as clean), plus `session_id`/`semantic_session_id` via `sessionAttribution` (omitted, never fabricated, without a readable `session.lock`). Emitted on refusals too (`ok:false`), so a blocked dispatch is a record, not a silence |
|
|
120
122
|
| `orchestrator.remote_dispatch.completed` | `scripts/lib/wave-executor/remote-dispatch.mjs` (`dispatchRemote`, via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`; name const `REMOTE_DISPATCH_EVENT`) | once per REMOTE-host dispatch over the `offload` CLI (#1160) — the sibling of `foreign_dispatch.completed` on the other channel: that one sends a task to a foreign MODEL on this machine, this one sends a task to Claude on ANOTHER machine. Same reason for existing — a Bash-spawned `offload` child fires no `SubagentStop` hook, so this is the only ledger record a remote dispatch produces. **Payload:** `host` (the `offload` alias, never a hostname or an IP), `role`, `run_id`, `ok`, `exit_code`, `duration_ms`, `patch_files` (COUNT of paths parsed from the returned patch — `+++ b/` plus the `diff --git` header, because a DELETED file's `+++` is `/dev/null`), `patch_bytes`, `reason` (present on every refusal — `never-foreign-role`, `unsafe-run-id`, `unsafe-host`, `unsafe-patch-path` — and on every failure class of a completed run: `usage-config`, `host-unreachable`, `remote-command-failed`, `sync-failed`, `timeout`, `empty-diff`, `rate-limited`, `write-lock-busy`, `channel-unavailable`; absent means success, so no failure class is reasonless), plus `session_id`/`semantic_session_id` via `sessionAttribution` (omitted, never fabricated, without a readable `session.lock`). **Emitted on refusals too** (`ok:false`, `exit_code: null`, `duration_ms: 0`, `patch_files: 0`) — a blocked dispatch is a record, not a silence, and the null exit code is what keeps "refused" distinguishable from "attempted and measured empty". **Deliberately EXCLUDED, pinned by a test:** the prompt text, the patch BODY, and `patch_path` — this payload also travels over the optional Clank webhook with no redaction, and a tmp patch path names the run id and the operator's host |
|
|
121
|
-
| `orchestrator.wave_dispatch.scope_checked` | `hooks/pre-task-scope-disjoint.mjs` (name const `SCOPE_EVENT`; built by `decide()` as `verdict.telemetry`, emitted in `main()` via `emitEvent(..., {repoRoot: projectDir})` + `sessionAttribution(projectDir)`) | PreToolUse `Agent` — **once per dispatch DECISION** (#1092), awaited BEFORE the terminal `emitAllow`/`emitDeny`/`emitWarn`, all of which `process.exit()` and would discard a pending append. **Payload:** `hook`, `agent_id` (the coordinator's `description` + `subagent_type`, clamped to 120 chars), `declared_path_count`, `injected` (a `FILE-SCOPE` declaration was found AND at least one path survived parsing), `shape` (`fenced` \| `inline` \| `none` — which PARSER won, deliberately not a second spelling of `signal`: a fenced block whose lines are prose is `signal: 'unparseable', shape: 'none'`), `signal` (`marker-absent` \| `unparseable` \| `extracted` — the row-5-vs-row-6 distinction of the hook's error-class matrix), `ledger_result` (`no-scope` \| `allow` \| `allow-finished` \| `deny` \| `warn-ledger-corrupt` \| `warn-not-evaluable`), `collision_count` (collisions involving THIS dispatch, live or already-finished — `ledger_result` says which). **Optional, absent-is-not-zero:** `wave` (the number out of `waveKeyOf()`'s `w<N>` segment — **omitted, never `0`**, under the `<session>|w?|?` fallback, same contract as `quality_gate`'s `wave_number`), plus `session_id` / `semantic_session_id` via `sessionAttribution` — omitted, never fabricated, without a readable `session.lock`. **What it proves and what it does not:** that the hook SAW (or did not see) a declaration in the prompt the coordinator handed to the dispatch tool, and what the guard decided — the SEND side. It proves nothing about the block reaching the agent's context or the agent reading it; that receive-side half of #1092 stays open for want of a platform prompt-assembly boundary (`docs/scope-collision-guard.md` § 4.2). **No prompt body and no declared path is in the payload** (issue #1092 acceptance criterion 3) — counts and closed enums only, because this record also travels over the optional Clank webhook with no redaction. Rows 1–4 of the matrix emit nothing (no decision was made) and neither do the two crash rows 2/12 — a hook that fell over cannot describe itself, which is what the `GUARD INACTIVE` stderr banner is for |
|
|
123
|
+
| `orchestrator.wave_dispatch.scope_checked` | `hooks/pre-task-scope-disjoint.mjs` (name const `SCOPE_EVENT`; built by `decide()` as `verdict.telemetry`, emitted in `main()` via `emitEvent(..., {repoRoot: projectDir})` + `sessionAttribution(projectDir)`) | PreToolUse `Agent` — **once per dispatch DECISION** (#1092), awaited BEFORE the terminal `emitAllow`/`emitDeny`/`emitWarn`, all of which `process.exit()` and would discard a pending append. **Payload:** `hook`, `agent_id` (the coordinator's `description` + `subagent_type`, clamped to 120 chars), `declared_path_count`, `injected` (a `FILE-SCOPE` declaration was found AND at least one path survived parsing), `shape` (`fenced` \| `inline` \| `none` — which PARSER won, deliberately not a second spelling of `signal`: a fenced block whose lines are prose is `signal: 'unparseable', shape: 'none'`), `signal` (`marker-absent` \| `unparseable` \| `extracted` — the row-5-vs-row-6 distinction of the hook's error-class matrix), `ledger_result` (`no-scope` \| `allow` \| `allow-finished` \| `deny` \| `warn-ledger-corrupt` \| `warn-not-evaluable`), `collision_count` (collisions involving THIS dispatch, live or already-finished — `ledger_result` says which), `marker_found` (a declaration of any recognised shape was seen — `signal !== 'marker-absent'`; NOT a second spelling of `injected`, which additionally requires a path to have survived), `echo_instruction_present` (an `End your final report with the line: SCOPE-DIGEST: <8hex>` line was found in the SAME prompt). **Optional, absent-is-not-zero — the digest trio (#1092):** `scope_digest` (8-hex `scopeDigest()` over the paths extracted FROM THE PROMPT — the join key `scope-echo --verify` uses; **OMITTED for an empty scope**, never the digest of the empty string, which is a real 8-hex value that would join every marker-absent Discovery dispatch to every other), `instructed_digest` (the 8-hex the echo line names — omitted when no line was found), `digest_consistent` (`scope_digest === instructed_digest` — omitted unless BOTH are present; `false` is agent A's fenced block beside agent B's echo line, caught at dispatch time with no filesystem read). All three are computed inside `scopeDigestFields()`, which is TOTAL by construction: a throwing digest function costs the FIELD, never the verdict. **Why a digest and not `agent_id`:** measured 2026-09-16 over this host's ledger — 609 `scope_checked` against 51 `scope_echo_checked`, agent-id set overlap **zero** (send writes `description` + `subagent_type`, receive writes the coordinator's short handle), so the two halves were unjoinable. **Optional, absent-is-not-zero:** `wave` (the number out of `waveKeyOf()`'s `w<N>` segment — **omitted, never `0`**, under the `<session>|w?|?` fallback, same contract as `quality_gate`'s `wave_number`), plus `session_id` / `semantic_session_id` via `sessionAttribution` — omitted, never fabricated, without a readable `session.lock`. **What it proves and what it does not:** that the hook SAW (or did not see) a declaration in the prompt the coordinator handed to the dispatch tool, and what the guard decided — the SEND side. It proves nothing about the block reaching the agent's context or the agent reading it; that receive-side half of #1092 stays open for want of a platform prompt-assembly boundary (`docs/scope-collision-guard.md` § 4.2). **No prompt body and no declared path is in the payload** (issue #1092 acceptance criterion 3) — counts and closed enums only, because this record also travels over the optional Clank webhook with no redaction. Rows 1–4 of the matrix emit nothing (no decision was made) and neither do the two crash rows 2/12 — a hook that fell over cannot describe itself, which is what the `GUARD INACTIVE` stderr banner is for |
|
|
122
124
|
| `orchestrator.wave_dispatch.scope_echo_checked` | `scripts/lib/scope-echo.mjs` (name const `SCOPE_ECHO_EVENT`; verdict built by `checkScopeEcho()`, payload by `scopeEchoPayload()`, emitted from the CLI's `--emit` path via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`) | **coordinator-invoked, post-wave — once per agent** that reported in a wave, at `skills/wave-executor/references/wave-loop-review.md` step 3d-bis, after Edit-Persistence Verify. Never emitted at dispatch time and never by a hook. **Payload:** `agent_id` (the coordinator's agent id, clamped to 120 chars — omitted when not passed), `applicable` (boolean; `false` when the agent's DECLARED file-scope was EMPTY — nothing to echo a digest against — paired with `reason: 'scope-empty'`, so consumers filter this never-instructed population out of the echo rate before computing it), `echoed` (the report carried a well-formed `SCOPE-DIGEST: <8 hex>` marker), `match` (the echoed digest equals the digest of that agent's `<state-dir>/filescopes/wave-<N>/<agent-id>.json`), `expected_digest` / `actual_digest` (8-hex or `null`), `reason` (`echo-absent` \| `digest-mismatch` \| `scope-file-unreadable` \| `scope-empty` — present whenever `match` or `applicable` is false). **Optional, absent-is-not-zero:** `wave` (**omitted, never `0`**, when the caller passes no wave number — same contract as `scope_checked`'s `wave`), plus `session_id` / `semantic_session_id` via `sessionAttribution`. **What it proves and what it does not:** that the agent's final report carried the digest the coordinator injected beside the `FILE-SCOPE` block — the **receive** side of #1092, i.e. the line survived the round trip into the agent's context and back. It does NOT prove the model read, understood or obeyed the scope: the digest stands in the prompt and can be copied without ever reading the paths (the named BV-004 ceiling, `docs/scope-collision-guard.md` § 4.2). It is INFORMATIONAL — `match:false` or `echoed:false` blocks nothing and triggers no re-dispatch. **No path and no prompt body is in the payload** (issue #1092 acceptance criterion 3), same reason as the row above: this record also travels over the optional Clank webhook with no redaction |
|
|
125
|
+
| `orchestrator.wave_dispatch.scope_materialized` | `scripts/materialize-wave-scope.mjs` (name const `SCOPE_MATERIALIZED_EVENT`, defined in `scripts/lib/scope-echo.mjs`; emitted from `main()` after stdout, fire-and-forget with a `.catch`) | **once per `materialize-wave-scope` CLI run**, i.e. once per wave manifest (`wave-loop-scope-manifest.md` § 3.2). Never emitted by the exported `materializeWaveScope()` function — the seam is the CLI, so a library caller writes no record. **Payload:** `wave`, `agent_count` (records that got a per-agent file, i.e. excluding `peer-session-*`), `digest_count` (DISTINCT `scopeDigest()` values among those records' non-empty file arrays — lower than `agent_count` means two agents were handed the identical scope), `transport_observable`, plus `session_id` / `semantic_session_id` via `sessionAttribution`. **`transport_observable` is the DEGRADATION half of #1092:** true iff a `PreToolUse` entry with matcher `Agent` is registered in the plugin's active `hooks/hooks.json`. On Codex / Cursor / Pi it is false BY DESIGN (no `Agent` dispatch tool — the asymmetry is registered in `DOCUMENTED_ASYMMETRIES`), and there a missing `scope_checked` record is **not** evidence of a missing injection; `scope-echo --verify` reads this field and degrades every verdict to `echo-only`. **Fails CLOSED** on any unreadable hooks file: observability we cannot prove would produce false `injection-missing` accusations. `repoRoot` is the state directory's PARENT, never `process.cwd()`, so a run from a subdirectory cannot write into another repo's ledger. Silent on failure — the corpus pins byte-empty stderr on this command's success path |
|
|
126
|
+
| `orchestrator.wave_dispatch.scope_verified` | `scripts/lib/scope-echo.mjs` (name const `SCOPE_VERIFIED_EVENT`; report by `verifyWaveScope()`, payload by `scopeVerifiedPayload()`, emitted from the `--verify --emit` path) | **coordinator-invoked, post-wave — exactly ONCE per wave** (`wave-loop-review.md` step 3d-bis, after the per-agent `--emit` calls). This is the JOIN of the three halves — `scope_checked`, the `<state-dir>/filescopes/wave-<N>/*.json` artefacts, and `scope_echo_checked` — **keyed on the digest, never on `agent_id`** (see the `scope_checked` row for the zero-overlap measurement that forces it). **Payload:** `wave`, `transport_observable`, `dispatches`, `injected`, `echoed`, `malformed_lines`, `malformed_scope_files`, `by_verdict` (a count per verdict), `digests` (the 8-hex keys the row covers). `malformed_lines` counts the ledger lines the join could not parse (a writer killed mid-append leaves a truncated line — a measured shape here) and is **ALWAYS present, including as `0`**, like the three counters beside it: it is the honesty check on the denominator, because a join that silently dropped half the ledger otherwise writes a record byte-identical to a clean wave (HR-105). `malformed_lines > 0` means every count and verdict in the row is a FLOOR, not a census; the human table says so beside them. `malformed_scope_files` (#1379 P2) is the same honesty check on the FILE side — the per-agent `<state-dir>/filescopes/wave-<N>/*.json` files the join could not read or parse, also **ALWAYS present, including as `0`**; before it existed a corrupt scope file was skipped silently, so `digest-unknown` / `injection-missing` could not be told apart from a genuinely missing injection. A file that parses but holds an empty/non-array scope is NOT counted — an empty scope is legitimate. **Deliberately NO `agent_id` and no path** — unlike its two halves, this record carries a LIST, and an agent id is a free-form coordinator string that has carried private project slugs; the per-agent verdicts stay on stdout, where they never reach the webhook. **Verdict enum — SIX members** (precedence order, one row per digest, all kebab-case): `duplicate-claim` (≥2 distinct agent ids claimed one digest — agent A's scope reported for agent B) · `echoed-not-injected` (an echo names a digest no dispatch claimed) · `digest-unknown` (no scope file on disk carries it) · `matched` · `injected-not-echoed` (the normal state during a wave, before reports land) · `injection-missing` (a scope file no dispatch claimed and no agent echoed — the omitted-injection case, #1092 AC-2). **Degraded value, NOT a seventh member:** `echo-only` — what EVERY verdict collapses to when `transport_observable` is false; the precedence chain never produces it, so it is absent from `SCOPE_VERDICTS`. **Spelling migration:** `injection-missing` was `injection_missing` (the one snake_case member) until 2026-09-16 — records written before that date may carry the old key in `by_verdict`, and there is no dual-emit, so a consumer reading history must accept both. **Exit 0 for every verdict** — the tool reports; the wave-executor turns `injection-missing` / `duplicate-claim` into a STATE.md deviation, never a block |
|
|
127
|
+
| `orchestrator.wave_dispatch.worktree_base_checked` | `hooks/pre-task-scope-disjoint.mjs` (name const `WORKTREE_BASE_EVENT`; facts by `worktreeBaseFacts()`, emitted in `main()` via `emitEvent(..., {repoRoot: projectDir})` + `sessionAttribution(projectDir)`, awaited BEFORE the terminal emit) | PreToolUse `Agent` — once per dispatch whose `tool_input.isolation === "worktree"` (#1413, #1424). **Payload (measured):** `hook`, `head` (full sha from `git rev-parse HEAD`), `session_start_ref` (STATE.md frontmatter, full sha), `stale` (`head !== session_start_ref`), optional `subagent_type`, plus the `session_id` / `semantic_session_id` envelope. **Payload (non-measurement):** `hook`, `stale: null`, `skipped` ∈ `identity-mismatch` \| `no-state-md` \| `no-start-ref` \| `git-error` \| `no-own-id` \| `probe-error`, plus the envelope — no `head`, no `session_start_ref` (a skip accuses nobody). **Written for EVERY `worktree` dispatch — both `stale` outcomes AND every skip** — so the firing rate is `stale:true / all records of this name`; a numerator-only stream cannot tell "rare" from "broken" (HR-105), and the rate was otherwise underivable: per-wave `isolation` exists in 2 of 455 `sessions.jsonl` records and a per-wave commit marker in 0 of 455 (measured 2026-09-20). **Emits NOTHING only when `isolation` is absent** (HR-101: that is most dispatches, and a record there would put the denominator on the whole hot path). A peer's STATE.md, a missing STATE.md, a missing `session-start-ref` or a git failure are recorded as `stale: null` + `skipped` since #1424 — until then each of them emitted nothing, which reproduced exactly the zero-records ambiguity this event exists to close; the candidate loop now `continue`s past a foreign STATE.md instead of `break`ing on the first parseable one. Warns on stderr when `stale` is true; **never affects the decision** — the hook stays fail-open. **Why it exists:** the agent worktree's base commit belongs to the HARNESS (the `Agent` tool has no base-ref field), and it is the SESSION-START commit, not HEAD — measured 2026-09-19 (s18), worktrees created 25 minutes after commit `240efda6` still stood on its parent `8f15f77b`, so a fix agent silently repaired old code and its test run was structurally red on top (`check-guard-requires-parity.mjs` compares against `git show HEAD:`, and `validate-plugin` is vitest's globalSetup) |
|
|
123
128
|
| `orchestrator.hook.import_probe_failed` | `hooks/post-edit-import-probe.mjs` | PostToolUse(Edit\|Write\|MultiEdit), after a module listed in `hooks/_lib/hook-import-set.json` (the committed hook-reachable allowlist) fails the probe. **Payload:** `file` (repo-relative), `check` (`eslint` \| `import` — which of the two checks caught it), `error` (the first offending message: a `no-undef`/fatal ESLint message, or the import diagnostic line), `reachable_from` (the hook entry basenames that import this module — the blast radius, since a throwing helper turns every tool call into "Internal hook error — request blocked" host-wide, #1224), `duration_ms`. Emitted ONLY on failure; a clean edit produces no record, so the event count IS the incident count |
|
|
124
|
-
| `orchestrator.reconcile.completed` | `scripts/lib/reconcile/engine.mjs` (`emitReconcileCompleted`, called from the thin `runReconcile` wrapper; name const `RECONCILE_EVENT`) | one record per `runReconcile` call (#1192) — from the WRAPPER, so all three return points are covered: the empty short-circuit, the normal tail, and the never-throws catch. An inline emit would have missed two of them, including the empty corpus and the error path — the two runs an operator most needs recorded (`.claude/rules/host-resources.md` § HR-105). Same shape as `narrative_mirrored`'s wrapper, and the emit is try/catch-wrapped because `emitEvent` THROWS `EventValidationError`, which would otherwise break `runReconcile`'s never-throws contract. **Payload, all ALWAYS present including as `0`** (each was measured over the whole run, like `mirror_run_completed`'s counters): `trigger` (`skill` | `session-end` | `phase-skip` | `unknown` — written always, so the per-trigger denominator is complete; the two markdown callers depend on a coordinator passing it, hence the honest `unknown` default), `dry_run`, `learnings_total`, `eligible`, `proposals`, `rejected`, `capped`, `already_materialized` (`summary.alreadyMaterialized` — the REAL idempotent-skip count, #484), `candidate_store_merged` (boolean — the `reconcile-candidates.jsonl` idempotency-sidecar merge, derived from `summary.written` at `scripts/lib/reconcile/engine.mjs:791`, which is the engine's ONLY disk write. It is NOT a rule-write signal: the engine never touches `.claude/rules/`, and this event is emitted BEFORE the operator-approval AUQ. For "a rule reached `.claude/rules/`" read `orchestrator.reconcile.rules_written` (row below) — its `rules_written` count is the only field that carries that claim. **Renamed from `written` in #1315** because the old name read as "rule files were written" and was misread on exactly that basis (#1307). The legacy key `written` is STILL emitted, carrying the identical boolean from the same expression so the two can never disagree — deprecated, removal **2027-03-13**, same one-generation dual-emit convention as the `orchestrator.session.stopped` → `orchestrator.turn.stopped` rename (#1234). `schema_version` is NOT bumped: it versions the record envelope, and the payload contract here stays additive), `duration_ms`. **Optional, absent-is-not-zero:** `targets` (the caller's effective target list; absent ⇒ none asserted), `store_records_dropped` (`summary.skipped` — absent ⇒ the candidate store was never INSPECTED: under `dryRun`, on the empty short-circuit, on the error path; a `0` there would be a false all-clear), and `aborted: 'engine-error'` + `reason` (clamped to 300 chars) — present ONLY when the never-throws guard fired; their absence means "ran to the end", never "unknown". **`dry_run` is the discriminator, not the event's absence:** the `phase-skip` caller runs dry on EVERY close and is the highest-volume trigger, so consumers filter `dry_run: false` for real runs. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling back to `SO_PROJECT_DIR` — most engine tests pass none, and the fallback would append synthetic records to the real fleet ledger on every `npm test` (#1119, same contract as `express_path.evaluated` and `narrative_mirrored`); a regression test pins it. **Engine identities — the counters are NOT a flat partition, and a live payload reads as inconsistent without them** (source: `scripts/lib/reconcile/engine.mjs:64-83`): `learnings_total === proposals + rejected`, and `capped` + `already_materialized` are DIAGNOSTIC SUB-COUNTS *inside* `rejected`, not siblings of it — each capped or already-materialized learning is also counted as rejected. Within the eligible set: `eligible − proposals − capped === already_materialized`. Worked against a live record: `learnings_total 164 = proposals 10 + rejected 154`, and `eligible 102 − proposals 10 − capped 72 = already_materialized 20` — with `capped 72` and `already_materialized 20` both sitting inside those 154. `already_materialized` is computed BEFORE the volume brake, so a terminal learning never consumes a new learning's quota. `session_id` / `semantic_session_id` / `wave` / `schema_version` are stamped by `emitEvent()` |
|
|
129
|
+
| `orchestrator.reconcile.completed` | `scripts/lib/reconcile/engine.mjs` (`emitReconcileCompleted`, called from the thin `runReconcile` wrapper; name const `RECONCILE_EVENT`) | one record per `runReconcile` call (#1192) — from the WRAPPER, so all three return points are covered: the empty short-circuit, the normal tail, and the never-throws catch. An inline emit would have missed two of them, including the empty corpus and the error path — the two runs an operator most needs recorded (`.claude/rules/host-resources.md` § HR-105). Same shape as `narrative_mirrored`'s wrapper, and the emit is try/catch-wrapped because `emitEvent` THROWS `EventValidationError`, which would otherwise break `runReconcile`'s never-throws contract. **Payload, all ALWAYS present including as `0`** (each was measured over the whole run, like `mirror_run_completed`'s counters): `trigger` (`skill` | `session-end` | `phase-skip` | `unknown` — written always, so the per-trigger denominator is complete; the two markdown callers depend on a coordinator passing it, hence the honest `unknown` default), `dry_run`, `learnings_total`, `eligible`, `proposals`, `rejected`, `capped`, `already_materialized` (`summary.alreadyMaterialized` — the REAL idempotent-skip count, #484), `candidate_store_merged` (boolean — the `reconcile-candidates.jsonl` idempotency-sidecar merge, derived from `summary.written` at `scripts/lib/reconcile/engine.mjs:791`, which is the engine's ONLY disk write. It is NOT a rule-write signal: the engine never touches `.claude/rules/`, and this event is emitted BEFORE the operator-approval AUQ. For "a rule reached `.claude/rules/`" read `orchestrator.reconcile.rules_written` (row below) — its `rules_written` count is the only field that carries that claim. **Renamed from `written` in #1315** because the old name read as "rule files were written" and was misread on exactly that basis (#1307). The legacy key `written` is STILL emitted, carrying the identical boolean from the same expression so the two can never disagree — deprecated, removal **2027-03-13**, same one-generation dual-emit convention as the `orchestrator.session.stopped` → `orchestrator.turn.stopped` rename (#1234). `schema_version` is NOT bumped: it versions the record envelope, and the payload contract here stays additive), `duration_ms`. **Optional, absent-is-not-zero:** `targets` (the caller's effective target list; absent ⇒ none asserted), `store_records_dropped` (`summary.skipped` — absent ⇒ the candidate store was never INSPECTED: under `dryRun`, on the empty short-circuit, on the error path; a `0` there would be a false all-clear), and `aborted: 'engine-error'` + `reason` (path-redacted, THEN clamped to 300 chars: `redactLocalPaths` in `scripts/lib/reconcile/engine.mjs` replaces the absolute repo root with `<repo>` and the home directory with `~` before the clamp, so an engine error message never writes a host-local path into the ledger) — present ONLY when the never-throws guard fired; their absence means "ran to the end", never "unknown". **`dry_run` is the discriminator, not the event's absence:** the `phase-skip` caller runs dry on EVERY close and is the highest-volume trigger, so consumers filter `dry_run: false` for real runs. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling back to `SO_PROJECT_DIR` — most engine tests pass none, and the fallback would append synthetic records to the real fleet ledger on every `npm test` (#1119, same contract as `express_path.evaluated` and `narrative_mirrored`); a regression test pins it. **Engine identities — the counters are NOT a flat partition, and a live payload reads as inconsistent without them** (source: `scripts/lib/reconcile/engine.mjs:64-83`): `learnings_total === proposals + rejected`, and `capped` + `already_materialized` are DIAGNOSTIC SUB-COUNTS *inside* `rejected`, not siblings of it — each capped or already-materialized learning is also counted as rejected. Within the eligible set: `eligible − proposals − capped === already_materialized`. Worked against a live record: `learnings_total 164 = proposals 10 + rejected 154`, and `eligible 102 − proposals 10 − capped 72 = already_materialized 20` — with `capped 72` and `already_materialized 20` both sitting inside those 154. `already_materialized` is computed BEFORE the volume brake, so a terminal learning never consumes a new learning's quota. `session_id` / `semantic_session_id` / `wave` / `schema_version` are stamped by `emitEvent()` |
|
|
125
130
|
| `orchestrator.reconcile.rules_written` | `scripts/lib/reconcile/writer.mjs` (`emitRulesWritten`, called from the tail of `writeApprovedRules`; name const `RULES_WRITTEN_EVENT`) | one record per rule-WRITE pass (#1307) — the companion `orchestrator.reconcile.completed` is emitted by the `runReconcile` wrapper, which runs BEFORE the operator-approval AUQ and before this module is reached at all, so a `dry_run: false` record there proves the engine ran and merged the candidate store, NEVER that a rule reached `.claude/rules/`: an operator who declines every proposal emits a byte-identical record to one who approves five. This event is the one that proves the write. Emitted from the FUNCTION TAIL, so both return points are covered — the normal pass and the lock-acquisition failure (which is a zero-write pass carrying `write_errors: 1`). **Payload, all ALWAYS present including as `0`:** `rules_written` (FILE count, not proposal count — one approved proposal written to two targets counts twice), `approved_proposals` (how many the operator approved), `rejected_archived` (records appended to `.orchestrator/reconcile.rejected.log`), `write_errors` (`result.errors.length`). **Optional, absent-is-not-zero:** `targets` (allowlisted to the CLOSED `TARGET_DIRS` key set — `repo-local` \| `baseline` — because the list originates in operator-authored Session Config and an unknown value would be a verbatim echo of untrusted text; absent ⇒ none in effect). **A ZERO-WRITE PASS IS EMITTED, and the discriminator is a FIELD, never the event's absence** (same convention this table states for `dry_run` one row up, and `.claude/rules/host-resources.md` § HR-105): `rules_written: 0` with `approved_proposals: 0` is *the operator declined everything*, while `rules_written: 0` with a non-zero `approved_proposals` and `write_errors` is *every write was refused by a guard* — outcomes a success-only emitter would collapse into one silence, together with *the writer was never reached*. The ONE case that emits nothing is the caller's true no-op (neither an approved nor a rejected item), which returns before the lock is taken. **The emit is not a write:** the #693 FA2/FA3 brandmauer is unchanged — `writeApprovedRules` is still the only module that writes rule files and still writes only operator-approved items. **A missing `repoRoot` SKIPS the emit silently** (same #1119 contract as the row above), and the whole emit is try/catch-wrapped to stderr because `emitEvent` THROWS `EventValidationError`, which would otherwise break `writeApprovedRules`'s never-throws contract. `session_id` / `semantic_session_id` / `wave` / `schema_version` are stamped by `emitEvent()` |
|
|
126
131
|
| `orchestrator.evolve.completed` | `scripts/lib/learnings/evolve-telemetry.mjs` (`emitEvolveCompleted`), called from `scripts/sweep-expired-learnings.mjs`'s `--prune --apply` exit path (#1206) for the success form, and from `skills/evolve/SKILL.md` Phase 1 (persistence/no-session-data aborts, skill-prose via `scripts/emit-event.mjs` — no mechanical pipeline call site precedes either gate) for the two abort forms | once per `/evolve analyze` run (default mode; #1200, mechanized #1206) — the success form is now the SAME command that performs the Step 3.5(5) store write, so the event can no longer be forgotten independently of the write it reports on (previously a separate `emit-event.mjs` call in skill prose, one edit away from drifting out of sync). Until #1200 `/evolve` reported completion in prose only — the whole class of `orchestrator.evolve.*` / `orchestrator.dialectic.*` events was **0 records across 164k fleet events** despite every run reporting success. **Payload (success), all FOUR counters ALWAYS present including as `0`** (same contract as `mirror_run_completed`'s counters): `appended` (new learnings written, Step 3.5(4)), `boosted` (existing learnings reinforced, Step 3.5(2)), `pruned` (`$PRUNE.archived` — this SAME call's own returned `archived` total, across every `_archive_reason`), `promoted` (always `0` from THIS call site — promotion to `public` scope is a separate CLI, `npm run share:hw-learnings -- --promote`, never invoked by `/evolve analyze` itself), `duration_ms`. **Optional, absent-is-not-zero:** `skipped` (HR-105 — an array of optional-step slugs, e.g. `skill-evolution-off` \| `vault-mirror-off`, that RAN but were themselves skipped this run; present only when non-empty, and distinct from the `aborted` form below — "ran, a step inside it skipped" is not "did not run at all"). **Payload (abort):** `aborted` (`persistence-disabled` \| `no-session-data`), `reason` (the abort message shown to the user, clamped to 300 chars), `duration_ms`. A `--prune --dry-run` preview run emits NOTHING — a preview never wrote anything, so it must not report a completed run either; `emitEvolveCompleted()` also refuses to emit (stderr WARN, never a throw) without an explicit `repoRoot`, same #1119 fail-closed contract as `emitReconcileCompleted` |
|
|
127
|
-
| `orchestrator.dialectic.completed` | `scripts/lib/learnings/evolve-telemetry.mjs` (`recordDialecticRun`), called from `scripts/dialectic-deriver.mjs`'s `runDialecticDeriver()` for its FOUR return
|
|
132
|
+
| `orchestrator.dialectic.completed` | `scripts/lib/learnings/evolve-telemetry.mjs` (`recordDialecticRun`), called from `scripts/dialectic-deriver.mjs`'s `runDialecticDeriver()` for SIX outcomes — its FOUR return values (`empty-input`, `budget-exceeded`, `would-empty-card`, and the dry-run `ok` success form), each recorded at its return point, plus the two THROWN aborts (`unknown-model` from `validateModel()`, `subagent-crash` from a failed `dispatchAgent` call), each recorded at the throw point by `recordThrownAbort()` before the original error is rethrown unchanged (#1221) — and from `skills/evolve/references/evolve-dialectic-mode.md` Step 6.4's apply branch for the ONE outcome the pipeline cannot see: apply-mode success, which needs the post-merge `mergePeerCard()` stats `runDialecticDeriver()` does not have. Step 6.5 records nothing (a second record there would double-count the run) | once per `/evolve --dialectic` run (#1200, mechanized #1206). Same fleet-zero gap as `evolve.completed` above, now closed the same way: the pipeline function records every outcome it can see — all five abort slugs, the two throw-based ones included since #1221 — and only apply-mode's merge-dependent success remains a skill-prose call site. **Payload (success):** `mode` (`dry-run` \| `apply`), `user_deltas`, `agent_deltas` — the two modes measure DIFFERENT quantities, so never compare a dry-run delta with an apply delta. Dry-run: `countManagedSections()` on the PROPOSED diff text — non-string or empty → 0; if the body carries `<!-- BEGIN MANAGED: … -->` sentinels → their count; otherwise the count of `## ` headings, fence-aware (headings inside ```` ``` ```` / `~~~` blocks do not count); a non-empty body with no headings → 1. Apply: `mergePeerCard()`'s own `stats.replaced + stats.appended` per target — managed (sentinel) sections of the card actually replaced or newly appended by the merge; hand-authored sections are `preserved` and never counted, `tokens_in`, `tokens_out`, `duration_ms`. **Payload (abort):** `aborted` (`unknown-model` \| `budget-exceeded` \| `would-empty-card` \| `empty-input` \| `subagent-crash`), `duration_ms`. Same #1119 refusal as `emitEvolveCompleted` — `recordDialecticRun()` skips the emit (stderr WARN) without an explicit `repoRoot` |
|
|
128
133
|
| `orchestrator.dialectic.nudge_decided` | **HISTORICAL — no emission since 2026-09-09.** The producer wrapper (`decideAndRecordAutoDialectic` in `scripts/lib/auto-dialectic.mjs`, #1200 part c) was REMOVED in #1288 after its only caller — session-end Phase 3.6.7 — was retired; the session-start `maintenance-due` probe (`scripts/lib/maintenance-due-banner.mjs`) reads the side-effect-free `shouldDispatchAutoDialectic()` instead. | never — no emitter exists. Row kept so records written before 2026-09-09 stay readable. **Payload (historical):** `decided` (boolean, mirrored `trigger`), `reason`, `cadence`, `sessions_since`, `learnings_since`, plus `session_id`/`semantic_session_id` via `sessionAttribution`. |
|
|
129
134
|
| `orchestrator.learnings.sweep_applied` | `scripts/lib/session-end/tail-runner.mjs` (`runExpiredSweep`, emit in `emitSweepApplied`; name const `SWEEP_EVENT`) | once per APPLIED session-end Phase 3.6.4 Expired-Learnings Sweep — emitted only on the write path, never on a plan-skip, a no-plan call, or the never-throws error branch, so a record's presence is proof the active store was actually rewritten. This event exists because the apply path did not: until #723-B4 was wired here, `sweepExpiredLearnings` had no session-end caller at all (census 2026-09-09 — definition, the `dryRun: true` probe in `phase-skip.mjs`, the standalone CLI, tests), so 0 sweeps were ever applied across three consumer repos while 628 learnings stayed resident. The sweep CLI (`scripts/sweep-expired-learnings.mjs:210`) deliberately emits nothing, and `orchestrator.evolve.completed` covers only the `--prune --apply` sibling — this is the ONLY record of a time-driven sweep. **Payload:** `scanned` (entries read from `learnings.jsonl`), `archived` (entries moved to `learnings-archive.jsonl`; a measured `0` is emitted, since the planner can legitimately RUN on a fail-open probe-error), `source` (always `session-end-3.6.4`, separating this producer from any future one), plus `session_id` / `semantic_session_id` via `sessionAttribution(repoRoot)` — the SAME root the record is pinned to via `emitEvent(..., {repoRoot})`, both keys OMITTED rather than fabricated when no `session.lock` is readable. Emission is best-effort and wrapped in its own catch: a telemetry failure never changes the sweep's return value, and — like the sweep itself — can never block a session close. |
|
|
135
|
+
| `orchestrator.rules.expiry_sweep_applied` | `scripts/sweep-expired-rules.mjs` (emit at the tail of `main`, after the writes; name const `RULE_EXPIRY_SWEEP_EVENT` in `scripts/lib/reconcile/rule-expiry-sweep.mjs`) | once per APPLIED generated-rule expiry sweep (#1377) — the `.claude/rules/*.md` counterpart of `orchestrator.learnings.sweep_applied` one corpus over: that one archives expired LEARNINGS, this one removes the expired ENTRIES the reconcile engine generated from them. Emitted on the `--apply` path ONLY and AFTER the rewrites/deletes, so a record's presence is proof that tracked rule files actually changed; a `--dry-run` (the DEFAULT) emits nothing, which is why the discriminator here is the event's presence and not a `dry_run` field. This event exists because the removal half did not: `rule-loader.mjs` stopped INJECTING an expired generated rule at read time and nothing ever removed one from disk, so an expired file stayed tracked and kept counting against `generated-byte-ceiling` while shipping to no wave. **Payload, all ALWAYS present including as `0`:** `rewritten` (files whose expired prose blocks were removed — their `## Provenance` pairs are KEPT as `markers only`, because `/reconcile` dedupes on those markers and dropping one re-proposes the learning), `deleted` (files whose every substantive entry expired; each one's pairs are stamped terminal via `markCandidateProcessed` BEFORE the unlink), `stamped` (candidate records stamped for those deletes — `0` whenever `deleted` is `0`), `write_errors` (per-file failures; a non-zero value also makes the CLI exit 2), `expired_entries` (entries the plan judged expired across all files), `files_scanned` (machine-generated rule files enumerated), `source` (always `sweep-expired-rules-cli`, separating this producer from any future session-end caller). A ZERO-WRITE APPLY IS EMITTED: `rewritten: 0` + `deleted: 0` is *nothing was expired*, while a non-zero `write_errors` beside them is *every write was refused* — outcomes a success-only emitter would collapse into one silence (`.claude/rules/host-resources.md` § HR-105). **Not in the payload, deliberately:** the per-file plans, including the `no-1to1-mapping` skips and the unresolvable `learning-id`s. Those carry learning subjects and rule slugs, and this record travels verbatim over the optional Clank webhook with no redaction — the full plan stays on stdout (`--json`). The emit is try/catch-wrapped because `emitEvent` THROWS `EventValidationError`, which must never turn a completed sweep into a failed one; `session_id` / `semantic_session_id` / `wave` / `schema_version` are stamped by `emitEvent()` |
|
|
130
136
|
| `orchestrator.session.shape_resolved` | `scripts/lib/session-shape.mjs` (`resolveAndRecordSessionShape`, emit in `_emitShapeResolved`; name const `SESSION_SHAPE_EVENT`), reachable as an entrypoint via `scripts/session-shape.mjs` | once per session-shape resolution, at the moment the coordinator turns the confirmed mode into an execution plan. Until this event existed the shape lived in PROSE at 27 sites contradicting each other in 8 answers (measured 2026-09-09), so "how many waves did this session actually run" was unanswerable from the ledger — which is why 6 consumer-repo `housekeeping` sessions ran the full 5-wave deep shape unnoticed. **Payload:** `session_type` (always), `total_waves`, `waves_config_honored` (false exactly when the ultradeep profile ignored the Session Config `waves` value — the fixed 7-wave shape of `skills/session-plan/SKILL.md` § Role-to-Wave Mapping; PRD AC-9's `waves < 7` rejection was dropped 2026-09-09), `discovery`, `agent_caps` (one clamped cap per wave, in wave order — `min(tier raw, agents-per-wave)`, `0` on a coordinator-direct wave), `coordinator_direct_waves` (the 1-based `n` of every coordinator-direct wave; `[]` is a MEASURED empty list, not an omission — a housekeeping shape must read `[1]` and a plain deep shape `[]`), `shape_version` (the `SESSION_SHAPE_VERSION` contract the record was produced under). **Optional, absent-is-not-zero:** `session_profile` (OMITTED, never `null`/`''`, when the session has no profile — a written null would read as "measured, no profile"; value set is the closed `VALID_SESSION_PROFILES`), `task_count`, plus `session_id`/`semantic_session_id` via `sessionAttribution(repoRoot)` — the SAME root the record is pinned to, omitted rather than fabricated without a readable `session.lock`. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling through to `SO_PROJECT_DIR` (#941), and `--no-event` skips it entirely so a planning dry-run cannot record a session that never ran. `events.mjs` is imported lazily so a pure-resolver consumer does not gain `platform.mjs` and its module-load filesystem walk-ups |
|
|
131
137
|
| `orchestrator.issue_budget.reconciled` | `scripts/lib/issue-budget-reconcile.mjs` (`reconcileIssueBudget`, emit in `emitIssueBudgetReconciled`; name const `ISSUE_BUDGET_RECONCILED_EVENT`) | once per session close — the cross-check between what the session RECORDED as created (`record.issues_created.length`) and what the issue-budget ledger CHARGED. It exists because `readBudgetState` returns a ZEROED state for a MISSING counter file, so "the hook never ran for a single create" and "the session created nothing" are byte-identical in its return value; measured 2026-09-09 on a real session record with **26** recorded creations, **0** charged and no counter file under either accounting key. **Payload:** `verdict` (always — `match` \| `escaped` \| `no-ledger` \| `stale-record`; `no-ledger` is the absent-ledger case above, `stale-record` the inverse), `recorded`, `charged`, `exempt`, `overflow`, `escaped` (= `max(0, recorded − charged − exempt)`), and `ledgers` — one record per accounting key looked up (`key`: `semantic`\|`raw`, `path` — **repo-RELATIVE** `.orchestrator/runtime/issue-budget/<hash>.json`, never absolute: this payload also travels over the optional Clank webhook with no redaction, and an absolute ledger path names the operator's home directory and the private repo slug; the absolute form stays in the local WARN text only, `found`, `charged`, `exempt`); a fifth verdict `corrupt-ledger` marks a file that exists but has a non-integer `count`. **BOTH keys are read and SUMMED**, because the accounting key is semantic only when `current-session.json` verified the raw id (`resolveIssueBudgetSessionId`) — measured in one consumer repo: 25 of 36 counter files keyed semantic, 11 keyed raw, so reading one key reports a phantom escape for every session that used the other. `found` is measured with `existsSync` BEFORE the read and is the only thing that separates `no-ledger` from a real zero — read an absent `found` as "not measured", never as "no spend". Plus `session_id`/`semantic_session_id` via `sessionAttribution`. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling through to `SO_PROJECT_DIR` (#941); `events.mjs` is imported lazily so no consumer of the reconcile module gains `platform.mjs`'s module-load filesystem walk-ups |
|
|
138
|
+
| `orchestrator.events.rotated` | `scripts/lib/events-rotation.mjs` (`maybeRotate()`, called once per SessionStart) | **The first line of every new active `events.jsonl`, written synchronously between the rename and any other append.** It is the ledger's record of its own break: before #1401 a rotation wrote nothing durable — only a `console.error` whose stderr the harness discards — so a rotation and a DELETED archive were byte-identical from outside, and a 53.896-line archive destroyed on 2026-09-19 left no trace anywhere. **Payload:** `archived_as` (absolute path of the archive — **PROVENANCE ONLY since #1411, not a pointer to resolve**: `readEventsWithRotations()` in `events.mjs` resolves the tombstone by BASENAME against the active file's own sibling `_archive/`, and deliberately ignores an absolute hit outside it, because that hit can only be a FOREIGN checkout — validating this ledger against another repo's archive was a silent false negative. Since #1423 EXISTENCE is not reading: the sibling counts only when it was an actual SOURCE of the read (a name matching `ARCHIVE_NAME_RE`); a sibling that EXISTS but was never read yields the gap kind `unindexed-archive` (with `path`), not silence. The reader's return contract is three-state: `complete` is `true` (measured whole), `false` (measured with a gap — kinds `missing-archive` \| `unindexed-archive` \| `ring-hole` \| `unreadable-source`) or `null` (not measured: no readable source at all); `notices` is a separate array (`unindexed-archive-file` for a hand-placed `_archive/` file with no tombstone) that never moves `complete`. The stored absolute path breaks the moment the checkout moves or a sibling git worktree reads it, which is routine here; archives are never renamed after creation, which is why the `.1`..`.N` ring was retired), `size_before` (bytes of the rotated file), `lines` (records + unreadable lines), `first_ts` / `last_ts` (earliest/latest parseable timestamp in the archive — `null`, never absent, when the archive carried none: "range unknown" must be distinguishable from "field not written by this version"), `malformed_lines` (lines that were not readable JSON objects; `0` is a measured zero). **Optional, additive:** `pruned` (array of archive paths deleted to honour `events-rotation.max-backups`) — present only when something was pruned, so the deletion is itself in the ledger. Emitted by a raw `appendFileSync`, not `emitEvent()`: it must be the first line, and the correlation envelope describes a session rather than a file operation. It still passes through `stampEventSchemaVersion()` + `validateEventRecord()` |
|
|
139
|
+
| `discovery_validator_violation` | `hooks/post-subagent-discovery-validator.mjs` | SubagentStop, when `discovery-validator.enabled: true` (default `false`). One record per distinct normalized claim per session; log + warn only, the hook never blocks and always exits 0. Legacy bare name, like `stagnation_detected`. Field contract in § `discovery_validator_violation` below |
|
|
132
140
|
| `orchestrator.issue_budget.refunded` | `hooks/post-bash-issue-budget-refund.mjs` (`emitRefundDecision`; name const `ISSUE_BUDGET_REFUNDED_EVENT`) | once per refund DECISION — one record per `PostToolUseFailure` delivery whose command contains at least one `gh`/`glab issue create` statement, emitted on the no-op branches too so a census over N sessions has a denominator and not only a numerator. Before #1353 a refund wrote a stderr line only, which under exit 0 reaches the debug log alone: the refund path was unfalsifiable in the sense of `.claude/rules/host-resources.md` HR-105. **Payload:** `reason` (always, CLOSED enum — `refunded` \| `not-charged` \| `chain-not-attributable` \| `counter-at-zero` \| `no-signal`), `unit` (`count` \| `exempt` \| `null` — which counter was given back, read off the honoured charge records; `exempt` only when EVERY refund landed on the exempt counter, `null` whenever nothing was refunded), `statement_count` (issue-create statements `findIssueCreateStatements` found), plus `session_id`/`semantic_session_id` via `sessionAttribution`. Branch mapping: `no-signal` = G2b (a failure event carrying no failure FIELD), `chain-not-attributable` = G3b (the create is not the whole command, so the exit code judges neither), `refunded` = G5 with at least one honoured charge record, `not-charged` = G5 with none (parked at the cap, a re-delivered failure, or an identity-less call `refundBooking` answers `no-session` for). **`counter-at-zero` has no branch in the hook today**: a matched record whose counter is already 0 is absorbed by `refundBooking`'s never-below-zero guard and returns `refunded` like any other match, so the case is not observable without a new field on the shared core's verdict (`scripts/lib/issue-budget.mjs`) — the enum value is reserved, not dead. `mode: off` emits NOTHING (G4 returns before it, and there was no charge to give back). **No command text, issue title or path is in the payload** — it travels verbatim over the optional Clank webhook with no redaction, same rule as `orchestrator.issue_budget.reconciled`. `events.mjs` is imported lazily, awaited AND caught: a throwing emit must never change the hook's exit code or output |
|
|
141
|
+
| `orchestrator.reaper.scan_completed` | `scripts/lib/orphan-reaper.mjs` (`runOrphanScan`, emitted through `deps.emitEvent`; name const `REAPER_SCAN_EVENT`) | at most ONE record per orphan scan (Epic #1425 B5/B6), and **only when the scan found something** — `candidates + reported + killed > 0`, or `instrument_suspect` is true. A scan that found nothing emits nothing: the reaper is wired to a `PostToolBatch`-class hook, and a per-fire record would be exactly the always-on signal `.claude/rules/host-resources.md` HR-101 calls a broken instrument. **Payload:** `scanned` (rows in the `ps` snapshot), `candidates`, `reported`, `rejected`, `killed` (COUNTS, not the arrays the function returns), `unattributed` (of the `reported`, how many carried NO `sessionId` on their ledger record — broken out because it is a PRODUCER defect, not a decision: the foreign-session guard was inert across 377 of 377 live records and the generic `reported` count could not show it), `peer_liveness` (`measured` — `detectPeers()` answered, possibly with an empty list — or `unmeasured` — the probe could not answer at all; the two are opposite facts and an unmeasured probe must never read as “no peers are alive”), `survived_sigkill` (killed entries still alive after the ladder — never booked as success, PRD B6), `dry_run`, `duration_ms`, `instrument_suspect` (the false-alarm rate over the last 50 audit decisions exceeds the 10 % HR-101 ceiling — REPORTED, never acted on: the answer to a suspect instrument is re-aiming it, not re-thresholding it). **Optional:** `false_alarm_rate` (0..1) — **OMITTED, never 0**, below the 10-decision floor `falseAlarmRate()` requires; an absent rate means "no population yet", and a fabricated zero would read as a measured-clean instrument. Correlation keys come from `emitEvent()`'s own attribution against the passed `repoRoot`. Per-decision detail (trigger, threshold, ist-value, pid/pgid, command signature, outcome) lives in the separate B5 audit `.orchestrator/metrics/reaper-audit.jsonl`, not in this record |
|
|
133
142
|
|
|
134
143
|
Non-orchestrator names still present in the stream: `tmux-layout.{invoked,completed,degraded}`
|
|
135
144
|
(tmux-layout skill) and `stagnation_detected`. The latter keeps its legacy bare name
|
|
@@ -138,6 +147,46 @@ that wired its first producer would have stacked a breaking stream change on top
|
|
|
138
147
|
change (#1114). (`grounding_injected` WAS migrated, to the dotted
|
|
139
148
|
`orchestrator.grounding.injected` in #611 — see the catalog above.)
|
|
140
149
|
|
|
150
|
+
### `discovery_validator_violation`
|
|
151
|
+
|
|
152
|
+
Written by the `SubagentStop` hook `hooks/post-subagent-discovery-validator.mjs` when
|
|
153
|
+
`discovery-validator.enabled: true` (default `false`). One record per distinct normalized
|
|
154
|
+
claim per session. Log + warn only — the hook never blocks and always exits 0.
|
|
155
|
+
|
|
156
|
+
| Field | Type | Always | Meaning |
|
|
157
|
+
|---|---|---|---|
|
|
158
|
+
| `event` | string | yes | `discovery_validator_violation` |
|
|
159
|
+
| `timestamp` | ISO string | yes | write time |
|
|
160
|
+
| `agent` | string | yes | agent type, or `unknown` when unresolvable |
|
|
161
|
+
| `agent_source` | `payload`\|`meta`\|`none` | yes | where `agent` came from |
|
|
162
|
+
| `agent_description` | string | no | sidecar description, ≤120 chars |
|
|
163
|
+
| `payload_keys` | string[] | no | sorted stdin keys; only when `agent_source: none` |
|
|
164
|
+
| `agent_id` | string | no | harness per-process agent id |
|
|
165
|
+
| `session_id` | string | no | `parent_session_id`, else `session_id` |
|
|
166
|
+
| `claim_text` | string | yes | the offending line, clamped to 200 chars |
|
|
167
|
+
| `occurrences` | number | yes | repeats of this claim inside the scanned tail |
|
|
168
|
+
| `kind` | `distributional`\|`gate-verdict`\|`claim-mismatch` | yes | claim class; **absent on pre-w4-1 records, which are `distributional` by construction** |
|
|
169
|
+
|
|
170
|
+
**`claim-mismatch` only** (#1385 R1) — a claimed test count that no observed vitest run
|
|
171
|
+
carries. The comparison reads the `tool_result` side of the same transcript window, so the
|
|
172
|
+
claim cannot serve as its own evidence. That inversion was the defect: `RUN_RECEIPT_RE` is
|
|
173
|
+
report-wide and matches `\d+\s+passed`, so before this class a self-asserted number WAS its
|
|
174
|
+
own receipt (`STATUS: done / Tests pass: 5129 passed / 0 failed.` → no violation, while an
|
|
175
|
+
honest `alles grün` without a number → two).
|
|
176
|
+
|
|
177
|
+
| Field | Type | Meaning |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| `mismatch` | `count` | sub-kind |
|
|
180
|
+
| `claimed` | `{passed: number, failed: number\|null}` | the asserted counts; `failed` is `null` when the line names none |
|
|
181
|
+
| `observed` | `{passed, failed, total}[]` | the **last 3** vitest summaries found in the window |
|
|
182
|
+
| `observed_n` | number | how many were found in total |
|
|
183
|
+
|
|
184
|
+
No command text is ever recorded — only counts (precedent: `8f15f77b`, `command_hash` instead
|
|
185
|
+
of the raw command). No observation in the window means no `claim-mismatch` record: evidence
|
|
186
|
+
absence is the `gate-verdict` class's job. Measured firing rate before landing: **4 of 1044**
|
|
187
|
+
real subagent transcripts (0,38 %; 0,64 % of the 622 with at least one vitest observation),
|
|
188
|
+
reached by fixing the parser twice — never by loosening the threshold.
|
|
189
|
+
|
|
141
190
|
### `stagnation_detected` — two producers, one schema (#1114)
|
|
142
191
|
|
|
143
192
|
| Producer | `source` | Trigger |
|
|
@@ -197,7 +246,8 @@ they join to no `sessions.jsonl` row and will not appear in a per-session roll-u
|
|
|
197
246
|
## Consumers
|
|
198
247
|
|
|
199
248
|
- `scripts/lib/convergence-monitor.mjs` — tails events.jsonl; reads `event_type ?? event`.
|
|
200
|
-
- `scripts/lib/
|
|
249
|
+
- `scripts/lib/telemetry/sync.mjs` — `deriveSessionFromEvents` reads across rotations; carries `ledger_complete` (`true|false|null`) / `ledger_gaps` and writes ONE stderr line naming the gap kinds when the verdict is `false` (#1423). `scripts/lib/maintenance-due-banner.mjs` and `scripts/backfill-abandoned-sessions.mjs` walk the same source sequence newest-first via `scanEventsBackwards` / `listEventSourcesNewestFirst` (#1414) instead of the active file alone.
|
|
250
|
+
- `scripts/lib/tmux-layout/telemetry-stats.mjs` — filters `event.startsWith('tmux-layout.')`; since #1407 it reads ACROSS rotations (`readEventsWithRotations`) and surfaces an incomplete ledger on stderr plus `ledgerComplete` (`true` \| `false` \| `null` = no source at all, #1423) / `ledgerGaps` / `notices` in its JSON — a `null` ledger gets its own "UNMEASURED, not zero" WARN — because "all-time" over the active file alone means "since the last rotation".
|
|
201
251
|
- `scripts/lib/events-rotation.mjs` — size-based archival.
|
|
202
252
|
- `skills/session-end/metrics-collection.md` — jq roll-ups (incl. `orchestrator.grounding.injected` and `stagnation_detected` → `stagnation_events`).
|
|
203
253
|
- `scripts/compute-grounding-injection.sh` — filters `stagnation_detected` on `error_class` + `file` to build each agent's edit-friction history; ignores records carrying neither (which is every `psa007-git-write` / `status-partial` record).
|
package/docs/install.md
CHANGED
|
@@ -29,6 +29,22 @@ uninstall.
|
|
|
29
29
|
| **Cursor IDE** | `git clone https://github.com/Kanevry/session-orchestrator.git ~/Projects/session-orchestrator && cd ~/Projects/session-orchestrator && npm install && node scripts/cursor-install.mjs /path/to/your/project` |
|
|
30
30
|
| **Pi** | `pi install npm:session-orchestrator` ; dev fallback: `git clone https://github.com/Kanevry/session-orchestrator.git ~/Projects/session-orchestrator && cd ~/Projects/session-orchestrator && npm install && node scripts/pi-install.mjs /path/to/your/project --settings-only` |
|
|
31
31
|
|
|
32
|
+
### Headless Claude Code (`claude -p`): two commands need the namespaced form
|
|
33
|
+
|
|
34
|
+
`session` and `plan` are reserved terminal-only built-in names in non-interactive
|
|
35
|
+
sessions. Under `claude -p` the bare form answers `"/session isn't available in
|
|
36
|
+
this environment."` — that is the harness, not the plugin, and no frontmatter or
|
|
37
|
+
manifest field overrides it (claude 2.1.273, measured 2026-09-16). Use the
|
|
38
|
+
namespaced form:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
claude -p "/session-orchestrator:session deep"
|
|
42
|
+
claude -p "/session-orchestrator:plan feature"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Every other command keeps its bare form (`/go`, `/close`, `/test`, …), and
|
|
46
|
+
interactive sessions are unaffected.
|
|
47
|
+
|
|
32
48
|
### Claude Code: install the Node dependencies once
|
|
33
49
|
|
|
34
50
|
For Claude Code, also install the package's Node dependencies **once** and
|
package/docs/persona-panel.md
CHANGED
|
@@ -422,7 +422,7 @@ or commit them if you want an auditable history.
|
|
|
422
422
|
|
|
423
423
|
## See Also
|
|
424
424
|
|
|
425
|
-
- `
|
|
425
|
+
- `skills/persona-panel/SKILL.md` — command reference: argument syntax, flag validation, examples
|
|
426
426
|
- `skills/persona-panel/SKILL.md` — full skill spec: 6 phases, catalog format, dispatch
|
|
427
427
|
mechanics, consolidation logic, sidecar schema
|
|
428
428
|
- `skills/persona-panel/persona-format.md` — persona file format specification: frontmatter
|
package/docs/pi-setup.md
CHANGED
|
@@ -76,7 +76,7 @@ The package manifest in `package.json` exposes:
|
|
|
76
76
|
|
|
77
77
|
- `pi.extensions`: `./pi/extensions/session-orchestrator.ts`
|
|
78
78
|
- `pi.skills`: `./skills`
|
|
79
|
-
- `pi.prompts`: `./pi/prompts/*.md` generated from `commands/*.md`
|
|
79
|
+
- `pi.prompts`: `./pi/prompts/*.md` generated from `commands/*.md` plus every skill with explicit `user-invocable: true`
|
|
80
80
|
|
|
81
81
|
The extension uses `hooks/hooks-pi.json` and `scripts/lib/pi-hook-bridge.mjs` to translate Pi events into the existing hook stdin contract.
|
|
82
82
|
|
package/docs/rule-authoring.md
CHANGED
|
@@ -106,13 +106,15 @@ globs:
|
|
|
106
106
|
|
|
107
107
|
`rule-loader.mjs`'s frontmatter parser (`parseGlobsFrontmatter`) tolerates a leading run of blank lines and/or single-line HTML comments before the opening `---`, so a vendored rule's provenance header does not defeat its `globs:` scoping — the header line is skipped, then frontmatter parses exactly as it would without the header. This tolerance is header-agnostic (it accepts any single-line HTML comment, not only the plugin's own), so a hand-authored rule that happens to start with a one-line comment is unaffected.
|
|
108
108
|
|
|
109
|
+
This convention binds only files that are actual sync SOURCES — the entries `syncRules()` resolves from `rules/_index.md` (`join(pluginRoot, 'rules', '_index.md')`, the manifest it reads before writing anything into a consumer's `.claude/rules/`). `rules/README.md` and `rules/_index.md` itself are never entries in that manifest, so they are never sync targets and carry no provenance header by construction — not an oversight to fix.
|
|
110
|
+
|
|
109
111
|
### Vendoring validation (issue #722)
|
|
110
112
|
|
|
111
113
|
Before `syncRules()` writes a source file into a consumer repo's `.claude/rules/`, it runs a pre-write gate via `validateRuleContent()` (`scripts/lib/validate-vendored-rules.mjs`). Five probes:
|
|
112
114
|
|
|
113
115
|
| Probe | Severity | Rejects / flags |
|
|
114
116
|
|-------|----------|------------------|
|
|
115
|
-
| `paths-frontmatter` | error | A top-level `paths:` frontmatter key
|
|
117
|
+
| `paths-frontmatter` | error | A top-level `paths:` frontmatter key **in a `rules/` library source**. Since #795 `rule-loader.mjs` accepts `paths:` as an alias for `globs:`, so such a rule IS glob-scoped — this is a vendoring-CONVENTION gate (`globs:` is the canonical form for vendored rules, #742), not a loader-compatibility gate. Its population is what `syncRules()` reads, i.e. `<pluginRoot>/rules/**` as listed by `rules/_index.md`; the consolidated files under `.claude/rules/` are `paths:`-canonical (see § Consolidated rules point 3) and are never its input. |
|
|
116
118
|
| `provenance-header` | error (opt-in via `requireProvenance`, default `true` in `syncRules()`) | Missing provenance header on a library source — without it, `rules-sync.mjs` mis-detects the file as a local override on the next re-sync and can never update it again. |
|
|
117
119
|
| `placeholder` | error | Unfilled placeholder tokens: `{{PROJECT_NAME}}`-style handlebars, a `## TODO: Customize` heading, or a `<!-- TODO:` comment — skeleton content, not a finished rule. |
|
|
118
120
|
| `zero-match-globs` | warn | A `globs:` pattern matching 0 files in the target repo's tracked file list (`git ls-files`, falling back to a directory walk). Legitimately possible in a freshly-scaffolded repo. |
|
|
@@ -285,19 +287,67 @@ them silently loses a learning or regenerates it:
|
|
|
285
287
|
TTL past what its type registry granted it.) State the rule in the file
|
|
286
288
|
itself, so the next editor does not "fix" it upward.
|
|
287
289
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
290
|
+
Since #1387 an expired file also stops DEDUPING: the `/reconcile` provenance
|
|
291
|
+
reader (`scripts/lib/reconcile/backlog.mjs`
|
|
292
|
+
`defaultReadMaterializedProvenance`) skips an expired file whole —
|
|
293
|
+
frontmatter key and body bullets alike — through the same `isRuleExpired`
|
|
294
|
+
predicate the loader's injection gate uses, fail-open on an absent or
|
|
295
|
+
unparseable date. Two KNOWN LIMITS, both measured 2026-09-18 and both left
|
|
296
|
+
unsolved on purpose:
|
|
297
|
+
|
|
298
|
+
- **Only the on-disk half.** `partitionMaterialized` marks a learning
|
|
299
|
+
materialized on `sidecarTerminal || onDisk`; this covers `onDisk` only. On
|
|
300
|
+
this repo 79 of 92 provenance keys are ALSO terminal in
|
|
301
|
+
`.orchestrator/runtime/reconcile-candidates.jsonl`, so only 13 keys
|
|
302
|
+
actually become re-proposable. The sidecar half stays untouched because it
|
|
303
|
+
carries operator DECLINES — expiring a rule must not re-ask a question the
|
|
304
|
+
operator already answered.
|
|
305
|
+
- **A file the sweep cannot split re-proposes forever.** A file the sweep
|
|
306
|
+
skips (`no-1to1-mapping`, `no-provenance-block`, `unreadable`,
|
|
307
|
+
`no-counter-sentence`) that then
|
|
308
|
+
expires legitimately becomes re-proposable on EVERY run, with no
|
|
309
|
+
mechanical exit: the sweep will not rewrite it, so nothing retires its
|
|
310
|
+
markers. The live tree is currently clear of this —
|
|
311
|
+
`node scripts/sweep-expired-rules.mjs --dry-run --json` reported
|
|
312
|
+
`skipped: []` over 7 generated files (2026-09-18) — but the class has no
|
|
313
|
+
guard, so a future unsplittable file lands in it silently.
|
|
314
|
+
|
|
315
|
+
The expiry sweep (`node scripts/sweep-expired-rules.mjs`) maintains the
|
|
316
|
+
earliest-date rule in
|
|
317
|
+
ONE direction: a header sitting EARLIER than the earliest absorbed date is
|
|
318
|
+
RAISED to it — the raise is its own rewrite trigger (`action: "rewrite"`,
|
|
319
|
+
`reason: "header-raise"`, visible in `--json` before `--apply`), and it
|
|
320
|
+
touches nothing but the frontmatter line and the sentence above. Without it
|
|
321
|
+
the file passes its header date with nothing expired, `rule-loader.mjs`
|
|
322
|
+
stops injecting it, and the surviving provenance markers keep `/reconcile`
|
|
323
|
+
treating its learnings as materialized — the substance goes dark and is
|
|
324
|
+
never re-proposed. The sweep never LOWERS a header: a header that outlives
|
|
325
|
+
its content is reported as an `advisory` only, because lowering on every run
|
|
326
|
+
would cut a healthy entry's TTL short.
|
|
327
|
+
|
|
328
|
+
3. **`paths:` is the canonical scope key, and it carries the UNION of the
|
|
329
|
+
parts.** The merged file loads for any path any of its parts covered, so its
|
|
330
|
+
list is the union of theirs. `paths:` is the key Claude Code's OWN native
|
|
331
|
+
rule loader reads, and it treats a rule lacking it as unconditional,
|
|
332
|
+
always-on (`check-rules.mjs` check #1108) — exactly the instruction-budget
|
|
333
|
+
failure consolidation exists to prevent. `globs:` is an accepted ALIAS, not a
|
|
334
|
+
second required mirror: `rule-loader.mjs` resolves either key
|
|
335
|
+
(`parseGlobsFrontmatter`, issue #795) and `instruction-budget-guard.mjs`
|
|
336
|
+
(`:960`) goes through that same parser, so a `paths:`-only file is
|
|
337
|
+
glob-scoped for every reader in this repo and still counts under
|
|
338
|
+
`bySurface.pathScoped`. Measured 2026-09-18 over this repo's own
|
|
339
|
+
`.claude/rules/` (25 files; NOT the vendored `rules/` library, and not the
|
|
340
|
+
private baseline's `rules/` population `docs/baseline.md` counts separately)
|
|
341
|
+
(`for f in .claude/rules/*.md; do awk '/^---$/{n++;next} n==1 && /^(paths|globs):/{print FILENAME": "$1}' "$f"; done`):
|
|
342
|
+
10 path-scoped rule files, ALL 10 `paths:`-only — 0 carry `globs:`, 0 carry
|
|
343
|
+
both (`grep -rn '^globs:' .claude/rules/` → no match, exit 1). Until this
|
|
344
|
+
session `cli-design.md` carried both; the duplicate `globs:` was removed
|
|
345
|
+
here. `globs:` is canonical only for rules VENDORED OUT through the `rules/`
|
|
346
|
+
fleet library, where `validate-vendored-rules.mjs`'s `paths-frontmatter`
|
|
347
|
+
probe enforces it (issue #742); that probe judges `rules/` sources only and
|
|
348
|
+
never sees a consolidated file under `.claude/rules/`. Carrying both keys is
|
|
349
|
+
allowed, but NEVER with different values: `globs:` wins SILENTLY when both
|
|
350
|
+
are present (#795), and `check-rules.mjs` fails a divergent pair outright.
|
|
301
351
|
|
|
302
352
|
4. **Substance in, boilerplate out.** Each absorbed learning becomes an `###`
|
|
303
353
|
heading carrying its original rule sentence, plus its evidence line. What is
|
|
@@ -330,6 +380,77 @@ This writes `.orchestrator/runtime/reconcile-candidates.jsonl` (creating it if
|
|
|
330
380
|
absent). Verify with a dry run: `alreadyMaterialized` must equal
|
|
331
381
|
absorbed + dropped, not absorbed alone.
|
|
332
382
|
|
|
383
|
+
#### The expiry sweep (`scripts/sweep-expired-rules.mjs`, #1377)
|
|
384
|
+
|
|
385
|
+
`rule-loader.mjs` stops INJECTING a generated rule once its `expires-at` has
|
|
386
|
+
passed; nothing removed one from disk, so an expired consolidated file stayed
|
|
387
|
+
tracked, kept costing bytes against `generated-byte-ceiling`, and kept reading
|
|
388
|
+
as live corpus to every human and every grep. The sweep
|
|
389
|
+
(`scripts/lib/reconcile/rule-expiry-sweep.mjs`, CLI
|
|
390
|
+
`node scripts/sweep-expired-rules.mjs`, `--dry-run` default) closes that gap
|
|
391
|
+
under four contracts, all of them consequences of the four merge rules above:
|
|
392
|
+
|
|
393
|
+
1. **Prose goes, the pair STAYS.** An expired entry's `###` block is deleted;
|
|
394
|
+
its provenance pair is converted to the `markers only` shape already present
|
|
395
|
+
in the corpus — a same-line HTML comment appended to the `- learning-id:`
|
|
396
|
+
bullet, leaving the backticked value regex-visible to
|
|
397
|
+
`BODY_LEARNING_ID_RE`. Per point 1, deleting the pair would make the
|
|
398
|
+
learning look unmaterialized and `/reconcile` would regenerate it. **A pair
|
|
399
|
+
is never deleted while its file survives.**
|
|
400
|
+
|
|
401
|
+
2. **Three fail-open cases, all reported rather than guessed.** An entry carries
|
|
402
|
+
no date of its own; its date is recoverable only via `learning-id` →
|
|
403
|
+
`.orchestrator/metrics/learnings.jsonl` `expires_at` (measured 2026-09-17 @
|
|
404
|
+
`9e8146b4`: 87 of 92 unique ids resolve, 5 do not). An **unresolvable id**
|
|
405
|
+
keeps its entry and blocks the file delete. An **ambiguous file** — where the
|
|
406
|
+
`###` headings do not map 1:1 onto the non-`markers only` pairs, because
|
|
407
|
+
several learnings were merged into one prose entry — gets `action: 'keep'`
|
|
408
|
+
plus a `skipped` record with reason `no-1to1-mapping`. Measured the same day,
|
|
409
|
+
the 1:1 mapping held in 3 of the 7 live files
|
|
410
|
+
(`measurement-discipline` 12/12, `process-contracts` 6/6,
|
|
411
|
+
`toolchain-and-build` 10/10) and failed in the other 4. The third case is an
|
|
412
|
+
**unrecognised counter sentence** (GH#70, 2026-09-20): when the body sentence
|
|
413
|
+
restating `expires-at` is in no spelling `COUNTER_FORMS` knows, the file gets
|
|
414
|
+
`action: 'keep'` plus a `skipped` record with reason `no-counter-sentence`
|
|
415
|
+
and NO write of any kind — not a rewrite, not a raise, and not a delete
|
|
416
|
+
either, because a shape the sweep cannot read is the last shape whose most
|
|
417
|
+
destructive action should run. Moving only the header would ship a
|
|
418
|
+
frontmatter date contradicting a body sentence that forbids exactly that
|
|
419
|
+
correction. Malformed `learnings.jsonl` lines are COUNTED
|
|
420
|
+
(`malformedLines`), never skipped.
|
|
421
|
+
|
|
422
|
+
3. **Deleting a whole file obeys the stamp-before-delete rule above.** A file is
|
|
423
|
+
deleted only when it has zero kept AND zero unresolved pairs, and EVERY pair
|
|
424
|
+
on it — markers-only ones included, since those are dedupe markers too — is
|
|
425
|
+
stamped via `markCandidateProcessed` BEFORE the `unlink`. Stamping afterwards
|
|
426
|
+
leaves a window in which neither the file nor a terminal verdict exists.
|
|
427
|
+
|
|
428
|
+
4. **The header is recomputed only for a file the sweep actually rewrites.**
|
|
429
|
+
Point 2's earliest-date rule is re-applied over the pairs that remain and
|
|
430
|
+
are not expired, in the frontmatter and in the body sentence
|
|
431
|
+
``**`expires-at` <D> = the EARLIEST of the <N> absorbed dates**``. A file
|
|
432
|
+
with nothing expired is left BYTE-IDENTICAL, so a recompute-on-every-run
|
|
433
|
+
cannot silently shorten a healthy file's TTL. The discrepancy is reported as
|
|
434
|
+
the plan's `advisory` field instead — computed over every RESOLVABLE pair on
|
|
435
|
+
the file and emitted BEFORE the `no-1to1-mapping` skip, so an ambiguous file
|
|
436
|
+
still gets one. Measured 2026-09-17
|
|
437
|
+
(`node scripts/sweep-expired-rules.mjs --json`, 7 files scanned, 0 expired),
|
|
438
|
+
**6 of the 7 files carry a discrepancy**: `identity-and-locks` 2026-10-01 vs
|
|
439
|
+
2026-10-02, `measurement-discipline` 2026-10-04 vs 2026-10-02,
|
|
440
|
+
`process-contracts` 2026-10-04 vs 2026-10-27,
|
|
441
|
+
`review-and-adapter-contracts` 2026-10-04 vs 2026-10-02, `test-hygiene`
|
|
442
|
+
2026-10-20 vs 2026-10-07, `toolchain-and-build` 2026-10-01 vs 2026-10-16 —
|
|
443
|
+
only `guard-design` agrees with its content. Three are the harmful direction,
|
|
444
|
+
a header OUTLIVING its content (`measurement-discipline`,
|
|
445
|
+
`review-and-adapter-contracts`, `test-hygiene`); the other three expire
|
|
446
|
+
earlier than they need to, which costs injection and loses nothing. `N`
|
|
447
|
+
counts the pairs remaining in the file, which is the total pair count — an
|
|
448
|
+
absorbed date stays absorbed after its prose is gone, and all 7 live
|
|
449
|
+
sentences carry that number.
|
|
450
|
+
|
|
451
|
+
`--apply` emits `orchestrator.rules.expiry_sweep_applied` after the writes
|
|
452
|
+
succeed (see `docs/events-schema.md`); a dry run emits nothing.
|
|
453
|
+
|
|
333
454
|
## Authoring Examples
|
|
334
455
|
|
|
335
456
|
### (a) Hand-authored always-on rule (no frontmatter)
|
|
@@ -157,6 +157,8 @@ Why it exists: the in-ledger `scopeSignals` counter (§ 4.1, rows 5/6) is a **wa
|
|
|
157
157
|
|
|
158
158
|
A cryptographic proof still needs that platform boundary: no mechanism inside this repo can distinguish a copied digest from a read one.
|
|
159
159
|
|
|
160
|
+
**The digest now JOINS the two halves (#1092, 2026-09-16).** Everything above stays true — what changed is that the halves became joinable at all. `scope_checked` carries `scope_digest` (`scopeDigest()` over the paths extracted from the prompt, omitted for an empty scope rather than defaulted to the empty-string digest), `echo_instruction_present`, `instructed_digest` and `digest_consistent`; `scope_echo_checked` already carried `expected_digest` / `actual_digest`; and `materialize-wave-scope` writes one `scope_materialized` record per wave carrying `transport_observable`. `node scripts/lib/scope-echo.mjs --verify --wave <N> --state-dir <dir>` joins all three **on the digest, never on `agent_id`** — measured 2026-09-16 over this host's ledger, 609 send-side against 51 receive-side records whose agent-id sets overlap in ZERO elements (send records the dispatch `description` + `subagent_type`, receive the coordinator's short handle), so before the digest the two halves could not be paired even in principle. Its closed verdict enum — `matched` · `injected-not-echoed` · `echoed-not-injected` · `injection-missing` · `digest-unknown` · `duplicate-claim`, all degrading to `echo-only` when the transport is unobservable — is what makes the issue's AC-2 (an omitted injection turning an end-to-end probe red) a measurable state rather than an unfalsifiable absence. `digest_consistent: false` additionally catches agent A's fenced block beside agent B's echo line **at dispatch time**, from the prompt alone. None of this raises the ceiling above: a joined digest still proves transport of a LINE, never obedience.
|
|
161
|
+
|
|
160
162
|
**Revisit-Trigger** (verbatim from issue #1092, for the transport half this section deliberately does NOT close):
|
|
161
163
|
|
|
162
164
|
> Implement when the platform exposes a stable prompt-assembly hook or when a coordinator-owned digest event can be proven against the real dispatched transcript.
|