session-orchestrator 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/navigator/SKILL.md +22 -0
- package/.agents/skills/navigator/agents/openai.yaml +5 -0
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/navigator/SKILL.md +22 -0
- package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/navigator.md +15 -0
- package/.cursor/commands/session.md +1 -1
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/rules/060-evolve.mdc +27 -12
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/navigator/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +9 -30
- package/CHANGELOG.md +206 -0
- package/README.md +13 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +22 -7
- package/docs/ci-setup.md +57 -4
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +12 -10
- package/docs/events-schema.md +65 -15
- package/docs/persona-panel.md +5 -4
- package/docs/rule-authoring.md +84 -17
- package/docs/scope-collision-guard.md +26 -4
- package/docs/session-config-reference.md +154 -12
- package/docs/session-config-template.md +41 -3
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +87 -6
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +101 -11
- package/hooks/on-stop.mjs +170 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +202 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
- package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +792 -74
- package/hooks/subagent-telemetry.mjs +111 -38
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +3 -3
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/navigator.md +12 -0
- package/pi/prompts/session.md +1 -1
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +7 -2
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +6 -2
- package/rules/opt-in-stack/backend-data.md +10 -2
- package/rules/opt-in-stack/backend.md +10 -4
- package/rules/opt-in-stack/frontend.md +7 -2
- package/rules/opt-in-stack/security-web.md +10 -4
- package/rules/opt-in-stack/swift.md +6 -2
- package/scripts/apply-session-learnings.mjs +492 -0
- package/scripts/archive-closed-prds.mjs +2 -1
- package/scripts/autopilot.mjs +70 -53
- package/scripts/backfill-abandoned-sessions.mjs +118 -16
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/cursor-install.mjs +1 -4
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +371 -31
- package/scripts/eval-session.mjs +65 -3
- package/scripts/export-hw-learnings.mjs +22 -3
- package/scripts/fleet-checkin.mjs +147 -0
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +18 -2
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +19 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +163 -0
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +85 -10
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +545 -7
- package/scripts/lib/fleet-protocol.mjs +270 -0
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +40 -5
- package/scripts/lib/gates/gate-full.mjs +35 -5
- package/scripts/lib/gates/gate-helpers.mjs +336 -23
- package/scripts/lib/gates/gate-incremental.mjs +49 -23
- package/scripts/lib/gates/gate-per-file.mjs +42 -8
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +208 -8
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
- package/scripts/lib/learnings/io.mjs +154 -26
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +104 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
- package/scripts/lib/orphan-reaper/scan.mjs +506 -0
- package/scripts/lib/orphan-reaper.mjs +316 -0
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/process-group.mjs +950 -0
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +152 -33
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +141 -74
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +3 -4
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +27 -2
- package/scripts/lib/session-schema/constants.mjs +56 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/session-token-rollup.mjs +245 -52
- package/scripts/lib/sessions-canonical.mjs +118 -4
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/pricing.mjs +118 -71
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tests-src-ratio.mjs +140 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +210 -84
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
- package/scripts/lib/vault-status/board-writer.mjs +48 -11
- package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +306 -0
- package/scripts/release.mjs +40 -8
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
- package/scripts/run-quality-gate.mjs +357 -197
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +56 -5
- package/scripts/sweep-expired-rules.mjs +16 -4
- package/scripts/validate-plugin.mjs +19 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +81 -47
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +131 -0
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/_shared/state-ownership.md +5 -0
- package/skills/autopilot/SKILL.md +47 -18
- package/skills/bootstrap/SKILL.md +1 -1
- package/skills/bootstrap/_shared-template.md +1 -1
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +3 -3
- package/skills/evolve/references/evolve-analyze-mode.md +10 -6
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/navigator/SKILL.md +193 -0
- package/skills/navigator/references/account-check.sh +87 -0
- package/skills/navigator/references/fleet-decisions.mjs +231 -0
- package/skills/navigator/references/slot-check.sh +114 -0
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/learning-patterns.md +22 -12
- package/skills/session-end/metrics-collection.md +5 -6
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +32 -60
- package/skills/session-plan/SKILL.md +17 -8
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/SKILL.md +6 -0
- package/skills/session-start/references/operations-contract.md +4 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +9 -5
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
- package/skills/wave-executor/references/wave-loop-review.md +3 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,212 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.4.0] - 2026-10-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **One lock for every in-process `learnings.jsonl` read-modify-write** (`scripts/lib/learnings/io.mjs` `withLearningsLock()`, #1447 item 8). `appendLearning`, `rewriteLearnings`, the expiry sweep and prune, `apply-session-learnings --apply`, `export-hw-learnings --promote` and `run-migrate-v2-cross-repo --apply` (a locked repo is reported and left unwritten, exit 2) run their whole read-to-write inside `<store>.lock` (over `withFileLock`, reentrant per async call chain only). Before, an append landing between a rewriter's read and its rename was lost; on a 10 s lock timeout nothing is written now and the CLIs exit non-zero naming the lock file. The lock and rewrite temp files are gitignored in this repo. Not covered: the `/evolve --prune --entries` sidecar is built by the agent before the prune runs, so a record appended in between is still archived as `pruned` (#1486).
|
|
15
|
+
- **Per-model token breakdown for mixed-model subagents** (`hooks/subagent-telemetry.mjs`, `scripts/lib/subagents-schema.mjs`, #1470). A subagent record whose billable turns span two or more models carries `models_usage` (the four buckets per model); `rollupSessionTokens()` prices it part by part instead of leaving it unpriced.
|
|
16
|
+
- **`cost_records_priced` / `cost_records_total` reach `sessions.jsonl`** (#1475 variant B) as optional, validated fields, so the share of a session the cost covers is visible in the ledger.
|
|
17
|
+
- **`describeOriginForLog()`** (`scripts/lib/named-vault-resolver.mjs`, #1479 item 4): one formatter the three vault writers use for the origin in their refusal line — a local origin prints as `file://<local path>`, a remote one with credentials redacted.
|
|
18
|
+
- **Fleet protocol v1 — files instead of messages** (#1462, Epic #1461, PRD !53 S1). New reference `skills/_shared/fleet-protocol.md` (no rule, nothing under `.claude/rules/`) and the schema library `scripts/lib/fleet-protocol.mjs` — the only place that knows the `~/.config/navigator/` layout (override `NAVIGATOR_CONFIG_DIR`, the navigator-CLI's own variable), the file modes 0600/0700, the check-in fields, `CHECKIN_FALLBACK_MIN = 10` and lease validity: `readNavigatorLease()` returns exactly `active` \| `none` (missing or expired) \| `unreadable` (broken JSON, missing fields) and is fail-closed — an unreadable or expired lease is never "aktiv". `isSafeSessionId()` guards every path built from a session id (no `/`, `..`, NUL). Review hardening before merge: a lease whose `session_id`/`adresse` carries control bytes, whose `plattform` is unknown or whose timestamps lack the `Z` is `unreadable` (a `Z`-less value would be read in local time and could extend the lease west of UTC); `repo` must be a name, `kandidaten` issue numbers only — both travel in the event, which may leave the host over the optional webhook.
|
|
19
|
+
- **Fleet check-in: session-start Phase 7.6, operations-contract peer preflight, CLI and event** (#1462). `scripts/fleet-checkin.mjs` reads the check-in JSON from stdin, stamps `zeit` (`date -u +%FT%TZ` form), validates, writes `checkin/<session_id>.json` atomically with mode 0600 (directories 0700) and emits `orchestrator.fleet.checkin` (payload `session`, `repo`, `modus`, `kandidaten`, `navigator_state`; catalogued in `docs/events-schema.md`); exit 0 written, 2 invalid input (error list on stderr, no file), 1 write error. The new Phase 7.6 in `skills/session-start/SKILL.md` (+6 lines / +1 038 B against the +12 / +1 200 B ceiling) and one sentence in `references/operations-contract.md` (+4 lines / +318 B against +4 / +400 B) place the check-in before the first writing action; a hint to the navigator is optional and never awaited (CSM-004); after 10 min without `auflagen/<session_id>.json` the reference's Standard-Auflagen apply. Test isolation: `tests/setup/navigator-dir-guard.mjs` points `NAVIGATOR_CONFIG_DIR` at a tmp dir per vitest worker, so no test reads or writes the operator's real directory.
|
|
20
|
+
- **Registry field `role`** (#1462). `heartbeat(sessionId, { role })` in `scripts/lib/session-registry.mjs` accepts `'navigator'` or `null`, throws `TypeError` on anything else before touching the file, and the patch-less call `heartbeat(id)` (`hooks/on-stop.mjs`) preserves the field; `_validEntry` drops an entry whose `role` is neither `null` nor a string. Display only, never ownership — the navigator lease stays authoritative (`skills/_shared/state-ownership.md`). `registerSelf()` does not set it and `classifyMode` is untouched; this change ships no production writer (the `/navigator` skill is a later cut).
|
|
21
|
+
- **Navigator banner line** (#1462). `hooks/on-session-start.mjs` appends exactly one line after the peer lines, on every harness that runs the hook: `🧭 Navigator aktiv: <adresse ?? session_id>, Check-in nach ~/.config/navigator/checkin/`, `🧭 Navigator: keiner nachweisbar (keine gültige Lease)` or `🧭 Navigator: Lease unlesbar, Status nicht nachweisbar` — never more than 120 bytes UTF-8 (a long label is cut, measured with a 128-character `adresse`; a 200-character id is refused by the lease reader and renders the "unlesbar" line), never blocking. 🧭 joins the curated emoji set in `scripts/lib/validate/check-unicode-safety.mjs`.
|
|
22
|
+
- **Skill `navigator` — the fleet-coordinator persona as a user-invocable skill** (#1462 B5, Epic #1461). `skills/navigator/SKILL.md` (`/session-orchestrator:navigator ticker|status|start|fenster|handover`, `disable-model-invocation: true` because a coordinator role runs only on operator command; description 344 bytes against the ≤ 400 B ceiling of PRD 4.5) describes what the navigator measures, writes and refrains from: it grants no approvals („Freigaben erteile ich keine, maßgeblich ist dein Owner-Auftrag"), writes `auflagen/<session_id>.json` only through `auflagenPath()` after `isSafeSessionId()`, treats a merge "Zeichen" as its own measurement (MR opened + mergeable, head = pipeline = gate SHA, job list, main free, file overlap), starts headless runs only behind a slot gate and a busy gate, carries the verbatim foreground wait pattern, the offload path rule (worktree path, alias `m5-remote` only), the Codex rules, the account watch and the AI-budget rule, and sets `role: 'navigator'` via `heartbeat()` per ticker round — the first production writer of that registry field. Lease acquisition, renewal and handover stay S4 (agents/navigator#36): the skill writes no lease, so the session-start banner keeps reporting "keiner nachweisbar" until S4 lands. Schemas are not repeated; `skills/_shared/fleet-protocol.md` stays the single reference (its obsolete `path-check: planned` marker for the PRD is removed and it now links the skill). Generated mirrors under `.agents/`, `.codex-plugin/`, `.cursor/` and `pi/`; README and `docs/components.md` counts move to 52 skills / 28 slash commands / 26 user-invocable skills.
|
|
23
|
+
- **`skills/navigator/references/` — pure decision functions plus measurement-only shell wrappers** (#1462 B5). `fleet-decisions.mjs` exports `slotDecision` (free-memory bands 4 / 3 / 0 runs at ≥ 50 % / ≥ 35 % / below, load1 > 40 closes everything, an unreadable value yields cap 0), `busyDecision` (a Codex rollout for this checkout younger than 30 min or a `session.lock` younger than 6 h means busy; missing counts mean busy) and `chooseAccount` (usable = 5h < 97 % and 7d < 97 %, earliest 7-day reset within 3 days first, far accounts by consumption, 1 800 s tie tolerance, a row without data is never usable), all fail-closed, none reading the environment, with a stdin-JSON CLI (`slot|busy|account`, exit 0 allowed / 1 refused / 2 bad input). `slot-check.sh` and `account-check.sh` only measure (`memory_pressure`, `vm.loadavg`, `pgrep` anchored on the `timeout -k 60` wrapper because one Codex run spawns three matching processes, `find -mmin`, the operator's `NAVIGATOR_ACCOUNT_CMD`) and hand the numbers to the module; `account-check.sh` prints the decision only, never the account rows (SEC-008), and `--apply` merely shows the switch command because a switch affects every session on the host. Tests: `tests/skills/navigator/fleet-decisions.test.mjs` (24 cases, each naming its bug, every rule probed red before its fix, including that no account alias ever reaches the wrapper's output).
|
|
24
|
+
- **Session Config `offload-first` — placement before pressure** (#1465, navigator decision E4). New boolean key, default `false`; `false` or absent changes nothing. With `true`, `applyOffloadDecision()` in `scripts/lib/wave-resource-gate.mjs` fires on a `proceed` verdict too, not only on `reduce` / `coordinator-direct`: an offloadable role goes to the first declared `remote-hosts` entry whose `roles-allowed` carries it and that is witnessed ready, still under the HR-004 heavy-repo cap. The offloadable table (`OFFLOADABLE_WAVE_ROLES`: `quality`/`test` → `test`, `ui`, `perf`) is not extended — build, lint and audit ride on the Quality wave's `test` role. The gate still never probes the network, so the witness (`remoteReady` or `probeFn`) is mandatory for the switch to act. A wave that stays local names why: `offload-first: no ready host for '<role>' — staying local`, or `offload-first: true but no remote-hosts declared — staying local`; `validateSessionConfig()` adds the warn-level cross-field finding `offload-first-cross-field` for the second case and never flips `ok`. Named limits: `resource-awareness: false` stays a full opt-out (no probe, no cap, no placement); the `probe failed (ignored)` path does place, since placement needs no measurement; the key is committed-only — no env or `owner.yaml` precedence, because `resolveDispatcherAutonomy` is enum-specific and `owner.yaml` freezes its sections, so reuse would need a schema change.
|
|
25
|
+
|
|
26
|
+
- **`hooks/pre-edit-sessions-ledger-guard.mjs` — the sessions ledger is guarded on the Edit/Write lane too** (#1443 item 4). A PreToolUse hook on `Edit|Write|MultiEdit` (Claude Code; mirrored on Cursor and Pi, Codex recorded as a documented asymmetry in `check-hooks-symmetry.mjs`) denies a write whose `file_path` ends in `.orchestrator/metrics/sessions.jsonl` (or a rotated `sessions.jsonl.<N>`) and points at `scripts/emit-session.mjs --entry` / `scripts/check-sessions-integrity.mjs`. Every other input allows — malformed stdin, missing fields, unknown tool name, an internal error, a payload over 1 MB — so a broken hook can never block foreign edits; only the ledger path is fail-closed. Named limits in the header: the tracked fixture `tests/fixtures/harness-audit/clean-repo/.orchestrator/metrics/sessions.jsonl` is denied too (override `SO_DISABLED_HOOKS=pre-edit-sessions-ledger-guard`), the match is case-insensitive. Allow-path latency, median of 20 runs: 31.9 ms direct, 36.3 ms through `run-node.sh` (Node startup is 25.5 ms of it). **Live sessions pick the hook up only after the live checkout pulls this change.**
|
|
27
|
+
- **Skill `test-audit` — gate, audit and prune tests under written evidence** (`skills/test-audit/`). `/test-audit [gate|audit|campaign] [scope] [--goal "<goal>"]` applies one value bar at three depths: a four-question write gate before any new test is written, a focused audit of one scope, and a lane-based campaign (phases P1–P9 in `references/campaign.md`). Audit and campaign open with a stated goal — default: remove at least 20 % of the least useful tests measured in test lines, total coverage within 2 percentage points of M0, no production file below its own M0 coverage — that never licenses an unproven deletion: every test declaration in scope gets one ledger mark (R/F/C/D/N/B/O), and every consolidate, delete or repair needs eight written evidence fields plus a targeted production mutation the keeper catches, restored byte for byte. Test lines count only for files the runner actually includes. `references/mutate.sh` runs a mutation manifest after a green control run, counts a mutation as caught only when the run fails AND prints a red summary, aborts on a restore mismatch (exit 3) and logs `EXTERN:<lane>` rows as cross-lane references instead of running them. Coverage thresholds, runner config, CI, snapshot updates, new dependencies and product-bug fixes stay owner decisions. Inspired by openclaw/openclaw `.agents/skills/test-audit` (MIT), rewritten for this plugin.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **The orphan reaper is six modules behind a stable entry** (`scripts/lib/orphan-reaper/`, #1437). `scripts/lib/orphan-reaper.mjs` (1588 → 316 lines) re-exports the same 21 names and keeps the CLI; the hooks import only `scan-throttle.mjs`. The audit is read through a bounded, growing tail window and pruned past 1 MiB to its newest 512 KiB; `DEFAULT_KILL_GRACE_MS` now lives in `process-group.mjs` (still re-exported from `dispatch-common.mjs`).
|
|
32
|
+
- **tests:src ratio counts TypeScript and co-located tests** (`scripts/lib/tests-src-ratio.mjs`, #1451, envelope `tests-src-ratio/2`). `.ts/.tsx/.mts/.cts/.jsx` count, declaration files do not; the test side is `tests/` plus `*.test.*`/`*.spec.*` anywhere plus `__tests__/`. A TypeScript repo now gets a real number (navigator: 1.2847) instead of "not measurable". This repo moves 1.4006 → 1.4007 at the same tree.
|
|
33
|
+
- **B5 proves a clock seam only from a named clock key** (`scripts/lib/validate/check-test-value-bans.mjs`, #1478): `SEAM_CLOCK_ARG` for pass 1, the wide `CONTROL_CLOCK_ARG` for pass 2; seamed pairs 192 → 172, B5 findings unchanged at 0.
|
|
34
|
+
- **The offload threshold has one source** (`skills/_shared/fleet-protocol.md`, `skills/navigator/SKILL.md`, #1482): `navigator m5-ersatz --quiet` and its exit contract replace the prose numbers (< 12, `last_max`).
|
|
35
|
+
- **Dependency overrides:** `brace-expansion` ^5.0.12 (two high advisories in 5.0.9, #1481) and `fast-uri` ^3.1.8 (moderate GHSA-hrr3-gc8f-f4qj); `npm audit` reports 0 vulnerabilities.
|
|
36
|
+
- **`vault-mirror`'s refusal path has a subprocess test for its ledger record** (#1450 follow-up 4). `tests/unit/vault-mirror.test.mjs` spawns the CLI against a vault without a git origin and pins exit 2, the stderr refusal, exactly one `orchestrator.vault.mirror_run_completed` record with `aborted: 'vault-not-canonical'` and `total: 0`, and an untouched vault directory. The exit/stderr half was already covered by `tests/scripts/vault-mirror-entry-point.test.mjs`; the ledger record and the no-write assertion were not.
|
|
37
|
+
- **`board-writer.mjs` names the `vault-dirs:` cwd-match tier in its `hostPaths` JSDoc** (#1450 follow-up 2). The last precedence twin without the tier now matches `narrative-mirror.mjs`. Comment text only.
|
|
38
|
+
- **Vault-dir precedence comments name the `vault-dirs:` cwd-match tier** (#1450 item 2, remaining sites). `scripts/lib/vault-status/narrative-mirror.mjs` and `tests/unit/vault-mirror.test.mjs` no longer describe the chain as `SO_VAULT_DIR > owner.yaml paths.vault-dir > committed`; the three files the issue named already carried the tier. Comment text only.
|
|
39
|
+
- **The Codex entrypoints close-evidence handover is linked** (#1439 item 3). `docs/handover/2026-09-19-codex-session-entrypoints.md` now points at `2026-09-19-codex-session-entrypoints-close.md`, which no tracked file referenced before.
|
|
40
|
+
- **`OPTIONAL_FIELDS` declares the four session-record fields that were written but never listed** (`scripts/lib/session-schema/constants.mjs`, #1457 item 7). `total_tokens` and `matched_records` (the #1436 rollup, shape-checked by the validator), `raw_session_id` (`scripts/emit-session.mjs`, `scripts/lib/session-close-backfill.mjs`) and `_repair_source` (`scripts/lib/session-record-repair.mjs`) join the list; no runtime consumer reads it, so nothing changes in validation.
|
|
41
|
+
- **Test consolidation** (#1457 item 5, #1459 item 8). `tests/lib/autopilot/flags.test.mjs` drops the `--max-tokens` case that no mutation of `flags.mjs` could turn red (non-numeric input: default = min = 0) and the absent-flag case the empty-argv `toEqual` already pins, and asserts the bounds as literals so the behavioural cases catch a wrong constant on their own instead of only through the `FLAG_BOUNDS` pin; `tests/_helpers/markdown-fences.mjs` replaces the two private fenced-block parsers in the evolve-recipe and sweep-expired-learnings tests with one line-based, indentation-tolerant helper.
|
|
42
|
+
- **`wave-executor-state-init.md` names the third reader of `session-start-ref`** (#1457 item 11): `scripts/lib/scope-baseline.mjs` diffs `<ref>..HEAD` in the coordinator's checkout, so the worktree-base edge does not apply there and the init-time edge only under-counts.
|
|
43
|
+
- **`shellQuote` lives in one module** (`scripts/lib/sh-quote.mjs`, #1438). Five former copies — `scripts/cursor-install.mjs`, `scripts/lib/tmux-layout/layouts.mjs`, `scripts/lib/tmux-layout/tmux-shell.mjs`, `scripts/run-quality-gate.mjs` and an inline copy in `scripts/vault-integration-watcher.mjs` — now import it. New round-trip test `tests/lib/sh-quote.test.mjs` runs each quoted value through `sh -c 'printf %s …'` and expects it back byte-identical.
|
|
44
|
+
- **Validator messages name `claude-opus-5-5` as the full-model-ID example** (#1459 item 4). `scripts/lib/agent-frontmatter.mjs`, `scripts/lib/config/persona-gate-wave.mjs`, `scripts/lib/persona-panel/catalog-loader.mjs`, `scripts/lib/validate/check-agents.mjs` and `docs/session-config-reference.md` replace the `claude-opus-4-7` example; `claude-opus-4-7` stays a valid input.
|
|
45
|
+
- **`prompt-caching` is no longer vendored into `cli-tool` repos** (#1449). `rules/_index.md` drops the `cli-tool` archetype from `opt-in-domain/prompt-caching.md`: CLI repos without an Anthropic client received a 15k-character rule they can never apply; repos of the `nextjs-saas`, `express-service`, `docker-service` or `monorepo-oss` archetypes still get it; a `cli-tool` repo that does use the SDK no longer does, because a repo carries exactly one archetype (`bootstrap.lock`).
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **Each subagent counts once in the session token and cost totals** (`scripts/lib/session-token-rollup.mjs`). The telemetry hook records the RUNNING total of an agent's whole transcript at every stop, so a resumed agent (e.g. continued via SendMessage) was summed once per stop. The rollup now keeps one record per `agent_id`: the last stop that found its transcript decides pricing, the last token-bearing one the token sums (they differ only for a transcript over the hook's 50 MiB read limit, which stays an unpriced candidate). **Series break:** measured over the fleet's local ledgers, 209 of 1,533 sessions change, output tokens fall to a median 0.88× (min 0.27×), costs to a median 0.83×; sessions without a resumed agent are unchanged. Historical `sessions.jsonl` values are not rewritten.
|
|
50
|
+
- **A partial session cost is no longer persisted as if complete** (`scripts/lib/session-token-rollup.mjs`, #1475 variant A). `total_cost_usd` is omitted whenever `cost_records_priced < cost_records_total` — an unknown or missing model, a found transcript without tokens, or a started agent whose transcript was never found (unless the same agent was priced from another stop). Phantom stops without a start record still never count.
|
|
51
|
+
- **A killed quality-gate run is recorded as `failed` in every variant** (`scripts/run-quality-gate.mjs`, #1459). The reporting variants exit 0 by design, so a run whose command the per-command timeout killed used to emit `orchestrator.quality_gate.passed`.
|
|
52
|
+
- **The autopilot stops on a backfill-stub iteration instead of counting it as 0 tokens** (`scripts/autopilot.mjs`, `scripts/lib/sessions-canonical.mjs` `isBackfillStub`, #1457 F2), and `emit-session` no longer takes a compact/resume re-emit as the session start ref (#1457 item 3).
|
|
53
|
+
- **Ledger prunes rewrite atomically and refuse symlinks** (`pruneReaperAudit`, `pruneGateProcessLedger`, `touchScanMarker`): a tmp file renamed over the target behind an `lstat` regular-file check; a symlinked audit can no longer truncate the file it points to.
|
|
54
|
+
- **The release regeneration moves `site/sitemap.xml` `lastmod`** (`scripts/site-numbers.mjs`, #1484): `lastmod := max(lastmod, page counted-at)`, never backwards; `--check` reports a stale `lastmod`.
|
|
55
|
+
- **The vault writers' refusal lines print no local paths** (#1479 items 4–6): origin via `describeOriginForLog()`, no `vaultDir`/source path in the `vault-mirror` refusal and not-found lines (the per-note SKIP lines in `scripts/lib/vault-mirror/process.mjs` still do, #1487), one `owner.yaml` load order, `SO_CONFIG_HOME` pinned in the entry-point test.
|
|
56
|
+
- **Learnings schema** (#1447 items 2, 10b, 1, 3a): strict validation type-checks `file_paths` with one shared predicate (`isRepoRelativePathEntry`) that `memory-propose --file-paths` and `validateProposalRecord` now use too, so an approved proposal can no longer fail at write time; migration strips `./` and moves array-typed path scopes into `file_paths` (scope words such as `project`/`repo` stay scopes); a missing baseline template or rule source is reported as `invalid-contract` / `invalid-rule-projection` instead of `unsafe-source`.
|
|
57
|
+
- **The narrative mirror refuses a non-canonical vault** (#1450 follow-up 1). `scripts/lib/vault-status/narrative-mirror.mjs` now runs the shared `checkCanonicalVault()` after the vault dir is resolved and before any slug lookup or write — the same guard the board writer and `vault-mirror` apply. A vault whose git origin is missing or does not end in a canonical suffix yields the new action `skipped-vault-not-canonical` (catalogued in `docs/events-schema.md`), nothing is written and one stderr line names the expected suffixes; `VAULT_MIRROR_SKIP_CANONICAL_CHECK=1` still bypasses it. The host context is loaded once and shared between the Session Config parse and the guard.
|
|
58
|
+
- **The board writer loads `owner.yaml` once per write** (#1450 follow-up 3). `mirrorBoardInner` resolved the host context twice — once inside `parseSessionConfig`, once for the canonical guard. `loadOwnerConfig()` has no cache and its section-drop warnings are not rate-limited, so a broken `owner.yaml` section printed every warning twice per board write. One `hostCtx` now feeds both; a regression test counts the warning lines.
|
|
59
|
+
- **`VAULT_MIRROR_SKIP_CANONICAL_CHECK=1` no longer prints `vaults:` warnings** (#1450 follow-up 5). `checkCanonicalVault()` evaluates the skip switch before resolving canonical suffixes, so a skipped check neither parses `owner.yaml` `vaults:` nor emits its WARN lines; the skip result carries `expected: []`, which no caller reads on the `ok` path.
|
|
60
|
+
- **`emitEvent` never writes relative to the working directory** (#1468). `scripts/lib/events.mjs` refuses an `opts.repoRoot` that is not an absolute path (empty string included) with one stderr WARN and no write, no directory, no webhook — previously `path.join` plus `mkdir -p` created `undefined/repo/.orchestrator/metrics/events.jsonl` under the cwd when an unset variable was concatenated into the root. An explicit `opts.filePath` is untouched, and `eventsFilePath()` keeps its return shape for readers. The four CLI entry points that forwarded an unresolved `--repo-root` (`release-session-lock.mjs`, `backfill-abandoned-sessions.mjs`, `wave-scope-binding.mjs`, `sweep-expired-rules.mjs`) now `path.resolve()` it at the parse site. `scripts/sweep-expired-learnings.mjs --repo-root` and an explicit root passed to `resolveRepoRoot()` in `scripts/lib/quality-gate.mjs` are resolved the same way, so a relative value there is no longer a silent telemetry drop.
|
|
61
|
+
- **The vault-status board writer refuses a non-canonical vault** (`scripts/lib/vault-status/board-writer.mjs`, #1450 item 1). `mirrorBoard()`/`sweepBoard()` now run the same canonical-vault guard `scripts/vault-mirror.mjs` has had since #600: when the resolved `vault-dir`'s git origin does not end in a canonical Meta-Vault suffix (or has no origin at all), the call returns `skipped-vault-not-canonical`, writes nothing, takes no lock — dry-run included — and prints one stderr line. The check itself is now ONE shared helper, `checkCanonicalVault()` + `normalizeRemote()` in `scripts/lib/named-vault-resolver.mjs`, and `vault-mirror.mjs` calls that helper instead of its private copy, so the two writers cannot drift apart again. Tests inject the origin reader through the existing `hostPaths` seam (`readOriginUrl`); the existing `VAULT_MIRROR_SKIP_CANONICAL_CHECK=1` switch is honoured from the injected env. The third writer, `narrative-mirror.mjs`, is closed by follow-up 1 below.
|
|
62
|
+
- **vault-mirror counts a collapsed duplicate session line instead of skipping it silently** (`scripts/vault-mirror.mjs`, #1291). The `--kind session` dedup pass gives a losing same-`session_id` line its own action, `skipped-duplicate-session`, through the ordinary per-entry path (stdout line, `orchestrator.vault.mirror_completed` record with `record_id` and `line`, no `path` — no target file exists for it), so `created + updated + skipped + failed === total` holds for session runs too. Both schema rows in `docs/events-schema.md` describe the new action; the #1186c ceiling test pins the invariant for both kinds.
|
|
63
|
+
- **Only a harness-written carrier record can finish a running agent in the liveness index** (`hooks/pre-task-scope-disjoint.mjs`, #1459 item 2 rest, security LOW, pre-existing). `buildTranscriptIndex()` matched `task-notification` on the raw transcript line, so a complete forged block placed first in a `tool_result` (a `Read` of a fixture), in assistant text or in a typed prompt marked a still-running agent's tool-use id as finished and turned a scope-collision DENY into an ALLOW. `notificationCarrierText()` now accepts exactly the three record forms the harness writes (`user` with `origin.kind: 'task-notification'`, optionally behind the system-notification preamble; `queue-operation` enqueue/remove; `queued_command` with `commandMode: 'task-notification'`), and `finishedNotificationIds()` requires the text to begin with the opener and the head to follow the harness tag order. Measured over 11,005 local transcript files: 0 genuine carrier ids stop being finished; the 20 lines the old code additionally accepted were all quotes. A remaining false-DENY for multi-notification preamble carriers is #1467.
|
|
64
|
+
- **Guards that were silently wrong** (#1455, #1451 fail-closed part, #1458 a+b, #1453, #1456, #1457 items 1–2). `hooks/pre-task-scope-disjoint.mjs`: `buildTranscriptIndex()` now treats every TERMINAL task-notification status (`completed|failed|killed|stopped`, census over local transcripts 2026-09-25) as finishing its tool-use-id, not only `completed` — a 429'd agent that notified `failed` and was resumed via `SendMessage` completed under the SendMessage's NEW id, so its dispatch never finished and the next wave was denied permanently (#1455; the resumed run itself is still unindexed — a false-ALLOW window, follow-up). `scripts/lib/tests-src-ratio.mjs`: 0 counted test files next to tracked test-like files (`*.test.ts`, `__tests__/`, anything under `tests/`) now yields `ratio: null`, `withinCorridor: null`, `reason: 'no-tests-found'`, `--check` exits 3, and the session-start probe WARNs "nicht messbar" instead of reading a TypeScript repo as green; `CODE_EXTENSIONS` is unchanged — whether TS tests COUNT is an open owner decision (#1451). `scripts/check-package-manager.mjs`: new advisory `checkHuskyHooksPath()` warns once, with `npx husky`, when `.husky/` exists but `core.hooksPath` is unset — `.npmrc` `ignore-scripts=true` skips the `prepare` script in every fresh clone, so commitlint, the owner-leakage scan and the pre-push gate were silently inactive there; never mutates, never changes the exit code, silent under CI; README names the step (#1458; the CI commitlint job is parked). `skills/evolve/references/evolve-analyze-mode.md`: the `/evolve analyze` sidecar moved from `.orchestrator/metrics/.learnings-next.jsonl` to `.orchestrator/tmp/learnings-next.jsonl`, outside the `ledger-delete-protected` denylist; a new test feeds the recipe with `$NEXT` spelled out through the real destructive guard (the literal `"$NEXT"` form was never blocked — the guard does not expand variables — so the collision only bit when an agent wrote the path out) (#1453). `persona-panel` presets, `templates/personas/*`, the `persona-gate-wave.dispatch-model` default and their docs use the alias `opus` instead of the stale pin `claude-opus-4-7`; two ID-shape examples in error messages are kept (#1456). `gate-full` publishes each command's inner kill ladder and `run-quality-gate.mjs` OR-joins it with the outer one into `orchestrator.quality_gate.*` via `joinGateKillFields()` — when the inner side was not measured and the outer did not fire the three keys are now OMITTED rather than written as `false/0/[]` (baseline/incremental/per-file publish it since the entry below, #1459 item 3); `emit-session.mjs` WARNs about a missing `session_start_ref` also without an own UUID and names what STATE.md contributed (#1457 items 1–2). Every behaviour change carries a test that is red on `0efb3e97`.
|
|
65
|
+
|
|
66
|
+
- **The sessions ledger is honest again: reader, writer, repair and token rollup** (#1443, #1442, #1436, #1439 items 1–2, #1440 C1/C2/C4). `canonicalizeSessions` no longer lets a later backfill stub (0 waves, `_backfill_source`) hide a real record with the same `session_id`, merges a stub and a real record that share a `raw_session_id` onto the real one, and treats repair provenance (`repair-invalid-sessions/*`) as real (fleet check 30 ledgers / 3135 records: 7 winner changes, all stub→real). `orchestrator.session.started` carries `head_sha`; the STATE.md template gains `session-start-ref`; `emit-session.mjs` falls back to the own event's `head_sha`, drops a short ref with a WARN (53 fleet records carry 7/8-char refs, so the validator does NOT enforce the format), stamps `raw_session_id` and runs `rollupSessionTokens` mechanically (new `total_tokens` = input + output) for the own UUID only — the prose step in `session-metrics-write.md` is gone, `readTailSession()` returns `usage`, so the autopilot `token-budget-exceeded` kill-switch fires again (wiring test through the real path). `validateSession` type-checks `total_tokens` and `matched_records`. `repair-invalid-sessions`: new class `timestamp_precision` truncates sub-millisecond timestamps (deliberately NOT listed in `_backfill_incomplete_fields`, which would discard the value downstream); post-verification now means "no worsening and every repaired line valid" — an unrepairable remainder is kept byte-identical and reported, exit 4 distinguishes it from a rollback (exit 3); the Bash ledger guard's deny text for repair variants names the one allowed command and says an in-command `SO_DISABLED_HOOKS=` prefix has no effect. `orchestrator.memory.propose_invoked` writes `cwd` project-relative or as `sha256:<12 hex>` (also when the project dir is `/`, home or an ancestor of home — pseudonymisation, not anonymisation), never an absolute path. `orchestrator.quality_gate.{passed,failed}` carry `timed_out`, `survivors`, `kill_signals` on both emitters (`failed_files` documented). `run-quality-gate.mjs` sits behind an `isMainModule` guard. The `over_delivery_ratio` / `planned_files_count` prose chain (0 of 173 waves ever carried it, no code wrote or read it) is removed instead of implemented; the two tests pinning it are deleted. `release.mjs` polls the registry 12 × 15 s instead of 5 × 3 s. Review fixes before merge: the token enrichment validates the enriched record first and, on a validation error, WARNs and appends the record WITHOUT the token fields (enrichment is never the reason the ledger gains no line); `repair-invalid-sessions` keeps an existing `_backfill_source` origin and records the repair in the additive `_repair_source` field, so a repaired zero-work stub stays a stub; a missing `session_start_ref` is WARNed; the `session.started` fallback takes the FIRST own event (its `head_sha` or nothing). **Autopilot `--max-tokens` now defaults to `0` (off):** with `readTailSession()` reporting `usage`, the dormant 500 000 default would have stopped autopilot after about one deep session (measured 450 499 subagent output tokens for one session, HR-101); the unit is cumulative subagent OUTPUT tokens, coordinator tokens are not counted. The reaper PRD now says an over-budget hook latency warns, it does not skip the scan.
|
|
67
|
+
|
|
68
|
+
- **`vault-dir` gains a per-directory tier, and a host can switch `vault-integration` off locally** (`scripts/lib/config/host-paths.mjs`, `scripts/lib/config.mjs`, `scripts/lib/owner-yaml.mjs`, `scripts/lib/named-baseline-resolver.mjs`, agents/vault#319, #1448). A single host-wide `owner.yaml` `paths.vault-dir` silently overrode every repo on the host, so a repo whose own vault lives elsewhere wrote its session closes into the wrong vault. `vault-dir` now resolves `SO_VAULT_DIR` > `owner.yaml` `vault-dirs:` cwd path-prefix match > `paths.vault-dir` > committed, through one helper (`resolveVaultDir`) for both the `vault-integration` and the `vault-sync` block, and the winning tier is exposed as `vault-integration.vault-dir-source` (`env|match|owner|committed`). The match tier reuses the #819 resolver, now parameterised by `section`/`envKey` (`name` stays required for `baselines:` only). The host switch `resolveVaultIntegrationHost` reads env `SO_VAULT_INTEGRATION=off|warn|strict` > `owner.yaml` `vault-integration: { enabled: false | mode: … }` > committed and may only LOWER the committed level, never enable or raise it; `vault-integration.host-override` names the tier that lowered it (`env:SO_VAULT_INTEGRATION` / `owner.yaml`, else `null`). An invalid `owner.yaml` `vault-integration:` section is dropped on its own via `droppedSections`; `vault-dirs:` is a lenient list section like `baselines:`. Session-end step 5 and the `/evolve` mirror step print `vault-mirror: bewusst aus auf diesem Host (<tier>)` instead of hitting `vault-mirror.mjs`'s exit 2 on a host without a vault, and log `vault-dir=<path> (Quelle: <source>)` when they do mirror. `claude-md-drift-check` still reads the raw committed values. A leading `~` in the two owner.yaml tiers (`vault-dirs[].path`, `paths.vault-dir`) is now expanded, and `scripts/vault-mirror.mjs` expands its `--vault-dir` at entry: before, a `~/…` value reached `existsSync(resolve(…))` as `<cwd>/~/…` and the mirror aborted with exit 2 `missing-vault-dir`. The #1244 whole-file discard (an invalid REQUIRED owner.yaml section) now keeps `vault-dirs:` raw alongside `paths:`, so an unrelated `tone:` typo no longer sends a repo under a declared prefix back into the host-wide vault. The test setup clears `SO_VAULT_INTEGRATION` (`tests/setup/scrub-session-env.mjs`), so an operator's exported switch cannot flip fixtures that assert the committed mode.
|
|
69
|
+
- **A configured baseline without its exporter is reported as `producer-missing`, not `unsafe-source`** (`scripts/lib/baseline-archetypes.mjs`, #1445, GH#73). `baselineSourcePath()` maps a missing `scripts/archetype-manifest.mjs` to `producer-missing` only when `lstat` on that path itself throws `ENOENT`; symlinks (dangling or not), symlinked parent directories, paths escaping the baseline, wrong file types and every other error stay `unsafe-source`. Contract-error results gain an additive, path-free `message` field; for `producer-missing` it names the missing exporter and points to `docs/baseline.md` § How bootstrap finds it. Exit code stays 2, and `scripts/baseline-archetypes.mjs --help` names the reason. The bootstrap docs (`skills/bootstrap/public-fallback.md`, `SKILL.md`, `private-contract.md`, `references/bootstrap-retroactive-flow.md`, `docs/baseline.md`) describe the two ways forward — migrate the baseline or unset the configured path — as the user's choice, never a silent downgrade to public.
|
|
70
|
+
- **Session-end learnings writes go through a validating CLI, and the migration repairs broken consumer records** (#1446, GH#69). `migrateLegacyLearning()` (`scripts/lib/learnings/schema.mjs`) coerces a path-like `scope` into `file_paths` plus `scope: private`, `scope: project|repo` to `private`, and `schema_version: "1"` / `2` to `1` — on the migration path only; the read funnel stays pass-through. `scripts/run-migrate-v2-cross-repo.mjs` reports invalid-record counts per validation error class before and after migration (`errorClassesPre` / `errorClassesPost`). The new `scripts/apply-session-learnings.mjs` (dry-run by default, `--apply` to write; strict `validateLearning()`, one invalid record writes nothing; passive decay from `learning-decay-rate`; emits `orchestrator.learnings.session_write_applied` only when an `--apply` run actually wrote the store — never on a dry run, never when nothing was written) replaces the shell `>` rewrite in `skills/session-end/learning-patterns.md` and `skills/plan/mode-retro.md`. `.cursor/rules/060-evolve.mdc` is not routed to that CLI: it now writes through the `/evolve` prune helper `scripts/sweep-expired-learnings.mjs --prune`, passing `--repo-root "$(pwd)"` like the canonical `skills/evolve/references/evolve-analyze-mode.md` so the helper records `orchestrator.evolve.completed`. All three learning templates now carry `scope` and `file_paths`. The sweep CLI's `--prune --entries` path strictly validates every sidecar record whose `id` is not yet in the store and exits 1 without writing when one is invalid. New blocking validator `scripts/lib/validate/check-learnings-shell-writes.mjs` rejects instruction prose that writes `learnings.jsonl` with `>`, `>>` or `tee`; a backticked operator counts when the store name sits within ±3 lines, a prohibition exempts a line only when its keyword precedes the operator, and Markdown blockquotes are ignored. The dead reference `GitLab #386` in the schema module now reads `EventDrop #386`; fixture `tests/fixtures/learnings-invalid-golden.jsonl` added.
|
|
71
|
+
- **The `baseline`, `incremental` and `per-file` gate variants publish the per-command kill ladder** (`scripts/lib/gates/gate-{baseline,incremental,per-file}.mjs`, #1459 item 3, #1457 item 12). `typecheck` and `test` in their JSON envelope are now objects `{ status, timed_out, kill_signals, survivors }` like `gate-full`'s; a skipped command carries `{ status: 'skip' }` only. `joinGateKillFields()` can therefore join the inner ladder for these variants too, so their `orchestrator.quality_gate.*` events carry `timed_out`/`kill_signals`/`survivors` instead of omitting them. Envelope change: these keys were bare status strings before. `skills/quality-gates/SKILL.md` already documented the object form for baseline and per-file; the incremental schema is updated.
|
|
72
|
+
- **A quoted `<tool-use-id>` no longer finishes a running agent** (`hooks/pre-task-scope-disjoint.mjs`, #1459 item 2, security LOW). `buildTranscriptIndex()` read every id on a notification line, so an id merely QUOTED inside another notification's `<result>`/`<summary>` counted as finished. Now only the line's FIRST `<task-notification>` block is read, and only its head before `<summary>`/`<result>`: the first `<tool-use-id>` counts, only when the first `<status>` is terminal and `</task-notification>` follows (fail-closed: the deny stays). Named ceiling: several DIFFERENT real notifications in one line report only the first — measured 0 times in this repo's transcripts.
|
|
73
|
+
- **Three doc corrections** (#1451, #1457). `skills/session-start/references/phase-4-ssot-environment-check.md` describes the `checkTestsSrcRatio` "nicht messbar" warn (`reason: 'no-tests-found'`, `ratio: null`) and the `reason?` result field; the `skills/autopilot/SKILL.md` pseudocode checks `max-tokens > 0` before comparing, matching `kill-switches.mjs` (default `0` = off); `hooks/pre-bash-memory-propose-audit.mjs` and `docs/events-schema.md` no longer call `events.jsonl` tracked — it is gitignored here (`.gitignore:57`), consumer repos may track it.
|
|
74
|
+
- **Vendored rules load path-scoped again: frontmatter on line 1, `paths:` beside `globs:`** (#1449). Claude Code reads a rule's `paths:` only when the frontmatter opens on line 1. The 17 library rules under `rules/` began with the provenance comment before `---`, and the 7 path-scoped ones carried only `globs:`, so every consumer loaded them always-on on every turn. The 7 rules with frontmatter (`backend`, `backend-data`, `frontend`, `swift`, `security-web`, `prompt-caching`, `bash-harness-pitfalls`) now open with `---`, carry a `paths:` list identical to `globs:` and the provenance comment directly after the closing `---`; bodies are byte-identical, the 10 rules without frontmatter are unchanged. The tooling now measures what Claude Code loads: `scripts/lib/rule-loader.mjs` reads a block only from line 1 and reports a displaced block as `defect: 'frontmatter-not-at-top'` (export `FRONTMATTER_NOT_AT_TOP`) with `globs: null`; `scripts/lib/instruction-budget-guard.mjs` counts such rules always-on and returns `defects[]` plus a banner line naming the files; `claude-md-drift-check` Check 9 drops its own header tolerance and reports the loader signal as an error. `scripts/lib/validate-vendored-rules.mjs` replaces the `paths-frontmatter` probe with `frontmatter-not-at-top` and `globs-paths-mismatch` (both errors; list comparison order-insensitive) and accepts the provenance header on line 1 (rule without frontmatter) or on the first line after the closing `---`; `scripts/lib/rules-sync.mjs` detects plugin ownership via the new `isPluginOwnedContent()` at both positions, so old-format copies stay plugin-owned and are lifted to the new format on the next sync instead of being frozen as local overrides. `docs/rule-authoring.md` documents the contract.
|
|
75
|
+
- **`ci-audit-job` reports an `include:`-supplied audit as unverifiable, not missing** (`scripts/lib/project-hygiene.mjs`, #1460). When `.gitlab-ci.yml` has no local audit step but declares `include:` (component/local/project/remote/template), `checkCiConfig()` cannot see the included jobs; the finding is now `fixable: false` with "CI dependency-audit step could not be verified: N include(s) not evaluated" instead of a `fixable: true` missing-audit finding. Without `include:` nothing changes, and a local audit job still wins.
|
|
76
|
+
- **Autopilot loads Session Config from a plugin path with spaces or non-ASCII characters** (`scripts/autopilot.mjs`, #1440). `loadConfig()` built the `parse-config.mjs` path from `new URL(...).pathname`, which keeps percent-encoding, so under such a path the config load failed and autopilot ran on defaults; it now uses `fileURLToPath()`. The Bash ledger guard's deny text additionally names the read-only check `scripts/check-sessions-integrity.mjs --session-id` to run after the writer succeeds.
|
|
77
|
+
- **The `semgrep` CI job gets an empty entrypoint and a source directory** (`.gitlab-ci.yml`). The job now sets `image.entrypoint: [""]` and `SEMGREP_SRC_DIRECTORY: $CI_PROJECT_DIR`, as the `gitleaks` job already did for its entrypoint (infrastructure/projects-baseline#550).
|
|
78
|
+
- **This repo's four project hooks read their input from stdin** (`.claude/settings.json`, contributor-facing). The destructive-command guard, the formatter, the `.env.example` reminder and the co-located-test hint read `$CLAUDE_TOOL_INPUT`, a variable Claude Code never sets, so none of them ever fired; they now parse the hook JSON from stdin (`.tool_input.command` / `.tool_input.file_path`) and answer with `hookSpecificOutput.permissionDecision`. The destructive-command guard is fail-closed: without `jq` on the PATH it denies every Bash call with an install hint instead of validating nothing (infrastructure/projects-baseline#501).
|
|
79
|
+
|
|
80
|
+
## [5.3.0] - 2026-09-22
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
|
|
84
|
+
- `scripts/lib/tail-window.mjs` (`readTailWindow(file, maxBytes)` → `{ text, cut, size }`) — the ONE bounded tail-window reader for append-only JSONL ledgers and transcripts, replacing five private copies of the same byte-window primitive: `hooks/on-stop.mjs`, `hooks/subagent-telemetry.mjs`, `scripts/lib/agent-status.mjs`, `scripts/lib/session-id.mjs` and `scripts/lib/telemetry-flush-health-banner.mjs` (#1272). Each caller keeps its own error mapping (`null` / `[]` / discriminated result / throw); the module owns only the window, loops until it is filled, and reports `cut` so a caller can drop the partial first line. Named ceiling (BV-004): the window is held in memory in one piece — fine for the 64 KiB..1 MiB windows in use, revisit above ~16 MiB. New suite `tests/lib/tail-window.test.mjs`; `hooks/_lib/hook-import-set.json` regenerated (the module is reachable from four hook entry files).
|
|
85
|
+
- `consumeDialecticPending({ repoRoot })` in `scripts/lib/auto-dialectic.mjs`, plus the Step 6.4 loop-closing block in `skills/evolve/references/evolve-dialectic-mode.md` (#1380). After a successful `/evolve dialectic --apply` — or after the operator explicitly discards a reviewed proposal — the skill now writes the last-run stamp AND deletes `.orchestrator/dialectic-pending.md`. Nothing else writes the one or removes the other, so without these two calls the session-start `maintenance-due` probe reported `dialectic` and `pending-sidecar` as due forever. The same step now serialises `result.diff` (an object `{ user?, agent? }`) into the Markdown string `writeDialecticPending` expects — passing the object threw `TypeError`.
|
|
86
|
+
- `scripts/lib/reconcile/backlog.mjs` — a LEAF module holding `partitionMaterialized` / `countReconcileBacklog`, re-exported from `scripts/lib/reconcile/engine.mjs` so every existing caller is unchanged. The session-start reconcile-nudge probe imports the count from here instead of from the write-capable engine, which dragged the emitter, renderer, unicode validator and candidate-store writer into a read-only banner's static closure: 11 modules / 151,946 B → 19 modules / 322,897 B (+112 %, measured 2026-09-18), inherited unasked by `scripts/lib/maintenance-due-banner.mjs`. The leaf brings the probe back to 13 modules / 176,331 B (and the maintenance-due banner from 35 / 577,439 B to 29 / 430,873 B); the +2 modules against the pre-session closure are the leaf itself plus `eligibility.mjs`, the price of the probe counting the same population `/reconcile` does. The runtime cost was never the point; a banner must not be able to reach a writer.
|
|
87
|
+
- Three runtime-read Session Config surfaces that no document described, now in `docs/session-config-reference.md` first and `docs/session-config-template.md` second (#1155, #1243, #1365): `cross-repo.projects` (a block key, NOT the flat `cross-repos` list — only the inline `projects: [a, b]` form is read, a `- ` block list yields `[]` silently), `worktree-exclude` (with its measured scope limit: `createWorktree()` has no production caller, so the key changes nothing in a wave today) and the whole `gitlab-portfolio` block (`enabled` / `mode` / `stale-days` / `critical-labels` — the last is block-form only; an inline list is silently ignored).
|
|
88
|
+
- `scripts/lib/js-mask.mjs` (`maskSource(src, { keepLiterals })`) — ONE JavaScript lexer for every "separate code from data" scanner here, with the regex-literal branch three hand-rolled `stripComments` copies never had (#1388). Five consumers: `scripts/lib/validate/check-untracked-test-deps.mjs` (where it grew; a one-line re-export keeps the old import path alive, because `package.json` carries no `exports` map and every `scripts/lib/**` path is therefore public), `check-entry-guard.mjs`, plus the three converted this session — `check-validator-registration.mjs`, `check-test-git-config-target.mjs` and `scripts/lib/auq/parse.mjs` (see Fixed for what each was measurably getting wrong). Named ceiling (BV-004) on the one-token keyword lookback, measured 2026-09-18 over `git ls-files '*.mjs'` (1245 files): 0 genuine division chains after `return`/`typeof`/`case`/… — all 18 files whose masking the lookback changes carry a real regex literal; revisit trigger is the first such chain.
|
|
89
|
+
- `scripts/lib/vault-yaml.mjs` (`readVaultSlug(repoRoot)`, `VAULT_YAML_FILE`) — the reader for the `metadata.slug` key in a repo's own `.vault.yaml`, which nothing read until now (#1131). Every vault writer derived the vault folder from the CHECKOUT DIRECTORY name instead (`narrative-mirror.mjs` via `subjectToSlug`, `vault-mirror/namespace.mjs` via `basename(process.cwd())`), so a repo whose directory name differs from its registered slug had its narrative, sessions and learnings filed under a second, wrong folder — and the #829 loose-slug healer only repairs case and punctuation drift, never a genuinely different name. Precedence is now explicit `repo` opt > `vault-name` override > declared slug > basename (`narrative-mirror.mjs`) and `vaultName` > declared slug > `deriveRepo()` (`namespace.mjs`, which stays a leaf: `node:fs`, `node:path`, `node:module`, `vault-mirror/utils.mjs`). A declared slug SKIPS `resolveLooseSlug` — it IS the canonical folder name, and healing it re-opens the ambiguity that keeps a duplicate pair alive once one exists. The reader never throws: absent file, unreadable file, malformed YAML, missing key, non-string or non-slug value all return `null` and leave the caller on its pre-#1131 chain, and `js-yaml` is resolved lazily through `createRequire` so a checkout without `node_modules` cannot crash at module load.
|
|
90
|
+
- `renderPendingBody(diff)` / `comparePendingBody()` in `scripts/lib/auto-dialectic.mjs` plus the Step 6.4 prose and its AUQ in `skills/evolve/references/evolve-dialectic-mode.md` (#1386). `--apply` now compares the FRESHLY derived body against the sidecar the operator actually reviewed before any peer card is written. The serializer moved out of the skill's JS snippet VERBATIM so both paths build the body from one implementation — two hand-copied serializers would report drift that is only formatting.
|
|
91
|
+
- `.orchestrator/consumed/<ISO-stamp>-<rand>-dialectic-pending.md` (`DIALECTIC_CONSUMED_DIR`, `DIALECTIC_CONSUMED_RETENTION` = 10, #1388): `consumeDialecticPending` MOVES the sidecar instead of unlinking it — it was the only copy of what the operator reviewed. The `maintenance-due` probe reads an explicit two-path list, so an archived file does not re-raise `pending-sidecar` (#1380 stays fixed). Ceiling + revisit trigger (BV-004): newest 10; revisit if `checkStaleArtifacts` ever reports a `consumed/` file, since it counts every untracked `.orchestrator/**` file older than 30 days. The prune is best-effort — a failure never fails a consume.
|
|
92
|
+
- `isRuleExpired(rawExpiry, now, onUnparseable)`, exported from `scripts/lib/rule-loader.mjs` and now also the gate in `scripts/lib/reconcile/backlog.mjs`'s `defaultReadMaterializedProvenance` (#1387). An EXPIRED rule file is skipped WHOLE — frontmatter key and body bullets alike — so its learnings stop counting as materialized by a rule that no longer reaches any agent (materialized forever, re-proposable never). ONE predicate, ONE `Date.parse` and one clock reading per scan, fail-open in both consumers: an absent or unparseable `expires-at` is NOT expired, because fail-closed would un-materialize the whole corpus on one typo. Two limits documented in `docs/rule-authoring.md` § Consolidated rules and `skills/reconcile/SKILL.md`, both measured 2026-09-18 and both left unsolved on purpose: it covers only the on-disk half of `partitionMaterialized` (79 of this repo's 92 provenance keys are ALSO sidecar-terminal, so 13 keys actually return — the sidecar half carries operator DECLINES and must not re-ask an answered question), and a file the sweep cannot split (`no-1to1-mapping` &c.) that then expires legitimately re-proposes on every run with no mechanical exit (`--dry-run --json` reported `skipped: []` over 7 generated files, so the live tree is clear).
|
|
93
|
+
- `header_raises` in the `sweep-expired-rules` summary and its human stdout line (`scripts/sweep-expired-rules.mjs`, `summarize` now exported, #1388). A header-raise is an `action: 'rewrite'` plan carrying `reason: 'header-raise'`, so it was counted inside `rewrites` and invisible on stdout although `--json` always carried `plans[].reason`. It is a SUBSET of `rewrites`, never a sibling — the two are deliberately not disjoint.
|
|
94
|
+
- `malformed_scope_files` on the `verifyWaveScope` report, the `scope_verified` payload and the human table (`scripts/lib/scope-echo.mjs`, `docs/events-schema.md`; #1379). A per-agent `<state-dir>/filescopes/wave-<N>/*.json` that cannot be read or parsed was skipped silently, which made a wave with corrupt scope files indistinguishable from one that was never injected — the same `digest-unknown` / `injection-missing` verdicts either way. Always present, including as `0`, the same honesty guarantee `malformed_lines` carries for the ledger side (HR-105). A file that parses but holds an empty or non-array scope is NOT counted: an empty scope is legitimate.
|
|
95
|
+
- `closeIssues()` and the `--close [--vcs gitlab|github] [-R <spec>] <id>...` CLI in `scripts/lib/issue-close-strip-labels.mjs` (#308) — one call strips every `status:*` label, closes, then re-reads each issue, printing one JSON line per id (`{id, stripped, closed, state}`) and exiting 1 when any id did not verify as closed. `skills/session-end/references/phase-5-issue-cleanup.md` Phase 5 now runs this single command instead of the old two-step `stripStatusLabels` + `glab issue close` prose, which had been skipped often enough that 339 closed issues still carried `status:in-progress` (measured 2026-09-19).
|
|
96
|
+
- `WAVE_KEY_ALIASES` (`agents_planned`/`agents_started`/`agents_completed` → `agent_count_*`) and `normalizeWaveKeys()` in `scripts/lib/session-schema/{constants,normalizer}.mjs`, applied on read by `normalizeSession` and on write by `scripts/emit-session.mjs` (#1390 P1) — one recorded session (`main-2026-09-18-session-1`, 5 waves) had written the de-underscored spelling, so every `agent_count_*` reader saw `undefined` for its waves. `scripts/emit-session.mjs` also derives the optional field `session_start_ref` from STATE.md's `session-start-ref` frontmatter — owner-checked, full sha only — so `/evolve analyze` gets an attributable commit range instead of a time window that also catches a parallel session's commits (#1339 P8). `scripts/lib/session-schema.mjs`'s barrel now also re-exports `isCoordinatorDirectHousekeeping` from the existing `session-schema/filters.mjs`.
|
|
97
|
+
- `resolveRepoNamespace({ repoRoot })` in `scripts/lib/vault-mirror/namespace.mjs` (#1389) — reads the declared `.vault.yaml` slug from a caller-supplied `repoRoot` instead of always `process.cwd()`; `scripts/lib/vault-mirror/process.mjs` and `scripts/vault-mirror.mjs` thread the CLI's invocation cwd through. Additive: every caller that omits `repoRoot` is unaffected.
|
|
98
|
+
- `neutralizeFingerprintSentinel()` and `fingerprintSentinelLine()`, exported from `scripts/lib/test-runner/issue-reconcile.mjs` (#1339 P6) — the one shared sentinel-neutralisation and sentinel-writer pair, now also used by `scripts/lib/ux-grill/reconcile.mjs`'s `oneLine()`/`buildUxGrillIssueBody()` instead of a second hand-copied `sanitizeSentinel()`.
|
|
99
|
+
- `scripts/lib/skill-evidence-window.mjs` — the skill-applied-judge (`instruction-adherence`/`report-quality`) gets an actual evidence window for the first time: excerpts built around the skill's call site in the transcript instead of a tail slice (#1399). Measured: the call site landed inside the old tail window in 6 of 67 cases, inside the new call-site window in 67 of 67. `runSkillJudge` no longer dispatches on near-empty evidence — the previous `''` default was 1387 characters, under the 8000-token gate, and was silently judged as though it were real transcript content. Session-end Phase 3.6.6 now joins over the raw UUID.
|
|
100
|
+
- A second PSA-006 claim class, `gate-verdict` (`hooks/_lib/subagent-transcript.mjs`, `hooks/post-subagent-discovery-validator.mjs`; #1385 S2) — a completion/gate-green assertion (`STATUS: done`, "tests pass", "alles grün") carrying no run receipt (a pass/fail count, a file count, or an exit code) and no adjacent measurement command. The scanner had been structurally blind to exactly these two line shapes: the #1198 gate-summary exemption skipped `STATUS:`/"N passed" lines before any pattern ran. Measured on a 130-case labelled corpus built for this fix: the scanner answered "no violation" on 128 of 130 before the fix (accuracy 30.2%, Cohen's κ 0.007 — indistinguishable from guessing); after, wanted hits went 6/92 → 12/92 with false positives unchanged at 3/34. Records now carry `kind`; a record with no `kind` predates the split and is `distributional` by construction. The hook remains log-and-warn — it blocks nothing.
|
|
101
|
+
- `scripts/release-session-lock.mjs` — a new CLI that releases the session lock, emits `lock.released` with explicit session/lock IDs, and reads the lock back to confirm absence; exit 0 only when the release is verified (#1395). `skills/session-end/SKILL.md` Phase 3.8 is now this single call instead of prose instructions to release the lock. Measured before this change: this repo's own ledger held 5 `acquired` events against 0 `released`; fleet-wide 110 of 702 (15.7%).
|
|
102
|
+
- `orchestrator.wave_dispatch.worktree_base_checked` (#1413) — a new event from the dispatch hook, which compares HEAD against `session-start-ref` behind a process-local identity check and warns (never blocks) on drift; the worktree base belongs to the harness, since the Agent tool itself carries no base field. The event always carries a `stale` value on BOTH outcomes, on purpose — reporting only the positive case would leave the firing rate unfalsifiable. Measured: per-wave isolation shows in 2 of 455 records, a commit marker in 0 of 455.
|
|
103
|
+
|
|
104
|
+
- **Epic #1425 Stufe 1, Teil A — `scripts/lib/process-group.mjs`** (`spawnInGroup`, `killProcessGroup`, `verifyProcessIdentity`, `installExitHandler`, `recordGateProcess`/`readGateProcessLedger`/`pruneGateProcessLedger`, `buildCommandSignature`). `spawn(cmd, { shell: true })` makes the SHELL the child, so `child.kill()` signalled that shell only and its own children (`tsgo --noEmit`, vitest workers) were reparented to PID 1 and kept running — the 2026-09-20 incident on this host: four orphaned `tsgo` processes, two at PPID 1, 86–588 % CPU and up to 8.0 GB RSS each, the machine at 13 % free memory. `detached: true` makes the shell a process-group LEADER, so `process.kill(-pgid, sig)` reaches every descendant that did not `setsid` away. The kill is a ladder — SIGTERM → `DEFAULT_KILL_GRACE_MS` grace (re-exported from `wave-executor/dispatch-common.mjs`, never re-defined) → SIGKILL to the GROUP — because a grandchild installing `trap "" TERM` survives the group SIGTERM (measured on Darwin 25.6.0, 2026-09-21). Three further measured constraints are wired in rather than assumed: `close` fires only once every group member has closed the shared pipe (hence a hard deadline), `maxBuffer` does not exist on async `spawn` (hence a hand-rolled 16 MiB byte cap reproducing the existing spawnSync/ENOBUFS contract), and a detached child survives its parent's exit at PPID 1 (hence `installExitHandler`). **A4, the ancestry register:** every gate process this repo starts itself is recorded with pid, pgid, start time and command signature in `.orchestrator/runtime/gate-processes.jsonl` — the identity basis Teil B joins against.
|
|
105
|
+
- **Epic #1425 Stufe 1, Teil B — `scripts/lib/orphan-reaper.mjs`** (#1430, #1431, #1433): the net under the process-group kill, for orphans that happen anyway (a crashed session, a harness SIGKILL, a leak from a foreign repo on the same host). Shape is the `lock-reaper.mjs` split — `parsePsSnapshot` → `decideReapCandidates` (PURE: no I/O, no signal, fully testable from `ps` text fixtures) → targeted `ps -p` identity re-check → `killProcessGroup` → wait → re-measure. A candidate is a CONJUNCTION (register ∧ PPID 1 ∧ age ∧ read-only command signature ∧ identity still verifying), because PPID 1 alone is a weapon pointed at the operating system: **538 of 784 processes (68.6 %) had PPID 1** on this host, measured 2026-09-21. The identity gate runs before SIGTERM **and** before SIGKILL, so a recycled pgid aborts the ladder instead of being signalled. Effect is read back after `verify-wait-ms` — `survived_sigkill` is never booked as success (PRD B6: on 2026-09-20 the ad-hoc check falsely reported "still alive" because it measured immediately after `kill -9`). Every decision, including a withdrawn one, is appended to `.orchestrator/metrics/reaper-audit.jsonl`; `falseAlarmRate()` judges the instrument over a rolling window of decisions and reports `instrument_suspect` above the 10 % HR-101 ceiling without ever acting on it. The existing detector (`countZombieProcesses`) would have counted ZERO in the 2026-09-20 incident: its `ps` carries no `ppid` and no `rss`, and it defines a zombie as IDLE — the exact opposite of a runaway.
|
|
106
|
+
- **Orphan-scan triggers in two hooks, plus the `reaper:` and `gate:` Session Config blocks** (#1432 B4). `hooks/post-tool-batch-wave-signal.mjs` (PostToolBatch — the only hook with measurably high frequency *during* a wave) and `hooks/on-stop.mjs` (Stop + SubagentStop — which lands exactly when gate children have just finished) each do one config read plus one `stat` of the shared throttle marker `.orchestrator/tmp/reaper-last-scan`, then hand the work to a DETACHED, `unref()`-ed child running the new CLI tail of `orphan-reaper.mjs`. Nothing is scanned inline: one `ps` round-trip out of Node was measured at ~47 ms, alone over the 50 ms `reaper.max-hook-latency-ms` budget; steady-state hook latency with the trigger in place measured 0.15 ms. New parsers `scripts/lib/config/reaper.mjs` (`_parseReaper`) and `scripts/lib/config/gate.mjs` (`_parseGate`), both tolerant and independent of the `## Session Config` section boundary, with the blocks documented opt-in in `docs/session-config-template.md` and `docs/session-config-reference.md`. New event `orchestrator.reaper.scan_completed`, emitted at most once per scan and **only when the scan found something** (candidates, reports or kills) **or the instrument itself is suspect** (false-alarm rate over 10 % of the last `false-alarm-window` decisions) — a per-fire record would be exactly the always-on signal HR-101 calls a broken instrument.
|
|
107
|
+
- `scripts/lib/validate/check-hook-entry-guards.mjs` (#1422) — an AST oracle over the hooks registered in the four hook manifests, flagging `missing-entry-guard` and `toplevel-profile-exit`. Registered BLOCKING in `scripts/validate-plugin.mjs` (232 checks / 0 failures at `110f334d`). The class it closes is concrete: a hook-reachable module without an entry guard executes its whole program when imported.
|
|
108
|
+
- `scripts/check-sessions-integrity.mjs` (#1417) — CLI front-end for `checkSessionsIntegrity` (`--session-id`, `--json`; exit 0/1/2, a missing record deliberately exiting 1). Until now the check existed only as a `node --input-type=module -e` one-liner inside `skills/session-end/session-metrics-write.md`, because the function had no entry point. The #1408 record parsed fine and was still invalid (`ended_at` instead of `completed_at`, four required fields missing), so vault-mirror dropped it as `skipped-invalid` and nobody was told until the next session-start banner.
|
|
109
|
+
- `scanEventsBackwards()` / `listEventSourcesNewestFirst()` in `scripts/lib/events.mjs` (#1414) — newest-first reading across ledger rotations, so a reader that only needs the recent tail stops at the first hit instead of parsing the whole corpus. Plus `parseEtimeToSeconds()` in `scripts/lib/resource-probe/parsers.mjs`, the `ps` `etime` parser the reaper's age criterion needs.
|
|
110
|
+
|
|
111
|
+
### Changed
|
|
112
|
+
|
|
113
|
+
- **Default `dialectic.budget-tokens` 8000 → 32000** (`DEFAULT_BUDGET_TOKENS` in `scripts/lib/config/dialectic.mjs`, now the SSOT that `scripts/dialectic-deriver.mjs`'s `DEFAULT_BUDGET.input` imports; #1380). The value is an INPUT CEILING the pre-dispatch estimate aborts on, never a spend and never a truncation — so the old default made a real corpus unrunnable rather than smaller: card bodies + steering alone estimate ~12k tokens in this repo, a consumer repo measured 11,158 (2026-09-10) and 30,262 (2026-09-18). Repos that already set `budget-tokens` explicitly are unaffected; repos on the default stop ending `budget-exceeded`. Docs, template and the `--budget-tokens` help text in the evolve skill (and its generated `.agents/` / `.codex-plugin/` copies) say "ceiling", not "budget".
|
|
114
|
+
- **The dialectic deriver's output contract is per-`## `-section, not a full-body replacement** (`agents/dialectic-deriver.md`, the prompt in `scripts/dialectic-deriver.mjs`, `skills/evolve/references/evolve-dialectic-mode.md`; #1380). The merge has been section-wise since #1310 — but the agent was still told "emit the FULL replacement body … omitted sections are deleted", which invites an LLM to reproduce every hand-written managed section. The prompt now says omitted sections stay unchanged, and the anti-pattern list is corrected: an invented section is NOT rejected downstream (`detectEmptying` refuses only a proposal with zero content lines), so the instruction is to skip the target instead.
|
|
115
|
+
- **The backticked-citation checker now scans the rule corpora, and it blocks.** `scripts/lib/validate/check-skill-script-paths.mjs` adds `.claude/rules` and `.cursor/rules` to `SCAN_DIRS` plus a `MD_EXTENSIONS` covering Cursor's `.mdc` (#1384). Both surfaces ship — `.cursor/rules` goes into the npm tarball and is symlinked into consumer repos — and neither had a gate; `.cursor/rules/010-session-workflow.mdc` kept citing `commands/go.md` and `commands/close.md` for a full release after `3ebf0e9d` folded them into skills. Measured 2026-09-18: corpus 295 → 329 files, 4 blocking findings on the first run (all fixed in the same change, see Fixed). Because a rules file legitimately cites paths that only exist at runtime, a missing target is excused only when it is under a harness state root (`.orchestrator/`, `.claude/`, `.codex/`, `.cursor/`, `.pi/`) **and** git ignores it — one `git check-ignore --stdin` for the whole run, failing open toward REPORTING. The git half alone would have silenced a typo in a `docs/specs/…` or `coverage/…` citation forever. `check-skill-links.mjs` documents the resulting division of labour: links there, backticked citations here.
|
|
116
|
+
- **Three consolidated rule files now expire earlier** — `.claude/rules/measurement-discipline.md` 2026-10-04 → 2026-10-02, `.claude/rules/review-and-adapter-contracts.md` 2026-10-04 → 2026-10-02, `.claude/rules/test-hygiene.md` 2026-10-20 → 2026-10-07 (#1382). Each header was outliving its own earliest absorbed learning, which the merge contract forbids (`docs/rule-authoring.md` § Consolidated rules); the body sentence `expires-at <D> = the EARLIEST of the N absorbed dates` was corrected with it. Consumers loading these rules lose them from injection sooner. Five folded provenance pairs across `guard-design.md`, `identity-and-locks.md`, `review-and-adapter-contracts.md` and `test-hygiene.md` are annotated `markers only` with the entry their substance was folded into, so all 7 consolidated files are 1:1 sweepable again rather than 3 of 7.
|
|
117
|
+
- **The expiry sweeper now RAISES a too-low rule header** (`scripts/lib/reconcile/rule-expiry-sweep.mjs`, #1382): a frontmatter `expires-at` sitting EARLIER than the earliest absorbed date is lifted to it, as its own rewrite trigger (`action: 'rewrite'`, `reason: 'header-raise'`, visible in `--dry-run --json` before `--apply`) touching nothing but that line and the counter sentence. Left alone the file passes its header date with nothing expired, `rule-loader.mjs` stops injecting it, and the surviving provenance markers keep `/reconcile` treating its learnings as materialized — the substance goes dark and is never re-proposed. Measured 2026-09-18 on the live corpus: 3 of 7 files in that state (`identity-and-locks` → 2026-10-02, `process-contracts` → 2026-10-27, `toolchain-and-build` → 2026-10-16), ≈31.5 kB. The direction is one-way by contract: a header that OUTLIVES its content is still reported as an `advisory` only, because lowering on every run would cut a healthy entry's TTL short. Documented in `docs/rule-authoring.md` § point 2.
|
|
118
|
+
- `scripts/release.mjs`'s `evaluateCiRow()` names the CAUSE a CI preflight row went red (#1384): the degraded branch appends the probe's `detail`, the read branch falls back to `details.reason` when no failing job can be named. A short `sha`, a commit the mirror has never seen and a HEAD with no pipeline all printed the identical `CI status unknown (query-failed)` before. With neither field present the text is byte-identical to before.
|
|
119
|
+
- `scripts/lib/ci-status-banner.mjs` clamps and control-byte-escapes every API-supplied fragment (`reason`, `failingJobName`, `allowFailureJobs`) at generation via `sanitizeApiText()`, 60 chars (#1384). The degraded branch has escaped since #1031; the READ branch interpolated a raw API `status` — a value carrying `\r` plus an ANSI sequence could repaint the release preflight's `ci-green-on-head` row green while its verdict stayed `ok: false`.
|
|
120
|
+
- `isUserInvocableValue(value, file, key)` takes the frontmatter key it is judging and names THAT key in its WARN (`scripts/lib/user-invocable-skills.mjs`, #1384). It is also the normaliser for the sibling marker `disable-model-invocation`, and a `disable-model-invocation: yes` demotion reported `user-invocable: "yes"` — sending the operator to a line that does not exist in the file. `scripts/generate-cursor-adapter.mjs` now parses each SKILL.md once through a memoised `skillFlags()`: the predicate is not side-effect-free, so reading the same key from three call sites emitted the same WARN three times (measured on a two-skill fixture: 3 lines for 2 defects).
|
|
121
|
+
- `scripts/lib/validate-vendored-rules.mjs` no longer validates `README.md` as a vendored rule — `rules-sync.mjs` vendors only the entries listed in `_index.md` (#1164). Six library rules under `rules/` dropped `## See Also` entries naming files the vendored corpus does not contain (`development.md`, `security.md`, `testing.md`, `mvp-scope.md`, `cli-design.md`).
|
|
122
|
+
- `CONTRIBUTING.md` — "Adding a New Skill" step 5 now says a slash command is a skill with `user-invocable: true` and that adding a `commands/<name>.md` beside it is forbidden (the twin aborts two generators and fails a test), and a new step 6 table names the five generators, when each must run, and which `validate-plugin` check fails on a stale copy. "Adding a New Command" is re-scoped to names a skill cannot carry (#1165). `scripts/lib/validate/check-cursor-adapter.mjs` and `check-pi-prompts.mjs` print the regeneration command with their stale-wrapper failure.
|
|
123
|
+
- `docs/baseline.md` carries ONE dated measurement of the baseline rule corpus — 2026-09-18 at `5cc51f2`: 32 rule files, 23 with both `paths:` and `globs:`, 9 with neither, 0 with only one — replacing the undated "26 rule files, all `paths:`, 0 `globs:`" claim that `scripts/lib/rule-loader.mjs`, `scripts/lib/validate/check-rules.mjs` and two test files each restated; all four now point here (#1155).
|
|
124
|
+
- `monitors/monitors.json`'s `ecosystem-health` description says what the watcher actually does — every 900 s it stats and reads ONE local file, `.orchestrator/metrics/ecosystem-health.jsonl`, with no network, no CI or branch probing and no Session Config read. Nothing writes that file (0 writers, measured 2026-09-18), so every run emits `no-state-yet` and idles; `scripts/lib/ecosystem-health.mjs` carries the matching named ceiling and revisit trigger at `DEFAULT_INTERVAL_S` (#1232).
|
|
125
|
+
- `skills/claude-md-drift-check/SKILL.md` Check 9 probe 1 is described as it behaves since #840: a well-formed `paths:`-only rule is correctly scoped and NOT flagged; the probe fires only when the loader's own parser returns `globs === null` for a file that textually carries `paths:`. `skills/autopilot/SKILL.md`'s DI example no longer shows a synchronous `probeSync()` that does not exist, and stops calling the in-process driver's missing walk-away UX "Phase C-5's job" — that driver shipped.
|
|
126
|
+
- **The CI reading for a commit is now CHOSEN, not `.find()`-ed off the most recently updated row** (`scripts/lib/ci-status-banner.mjs`, #857). The 15-row pipeline window MIXES refs (`main`, `refs/merge-requests/39/head`, a `codex/…` branch — measured 2026-09-18), so a foreign branch's or an MR's pipeline could silently become the local HEAD's verdict. Selection is now a ref preference over the same-sha rows — (1) pipelines whose `ref` names the current branch, (2) pipelines whose ref cannot be judged (no `ref`, or no branch resolved: detached HEAD, `git` unavailable — the pre-#857 candidate set), (3) an MR HEAD ref (`refs/merge-requests/<iid>/head`, for repos whose `workflow: rules:` produce no branch pipeline at all; sha equality is the correctness argument, and a merged-results pipeline runs a different sha so it cannot reach this tier), (4) otherwise nothing is selected — followed by worst-status-wins within the tier. `failed` outranks `canceled`, everything unsettled outranks `success`, and `skipped`/`manual` rank BELOW `success` as terminal non-failing states: lumping them in with `running`/`pending` turned the everyday `success` + `skipped` pair into `unknown / unrecognised-status-skipped` where the pre-#857 reading was green (category separation, not a threshold tweak). Measured on this project 2026-09-18: 14 of ~86 distinct shas in the last 100 pipelines carry 2-4 pipelines, 7 of them with contradicting statuses from different `source`s. The `status` vocabulary stays FROZEN at `green | red | unknown` — `dispatcher/rank.mjs` and `autonomy/suitability.mjs` key on the literal `'red'` and the session-start renderer prints nothing for an unknown string — so the findings ride ADDITIVE `details` fields instead: `matchedRef`, and (only when more than one candidate exists, so an ordinary reading is byte-identical to before) `candidateCount` / `candidateStatuses` / `ambiguous`. A commit whose pipelines are ALL on foreign refs reports `unknown` with `reason: 'pipeline-unmatched-ref'` rather than adopting one or claiming "no pipeline"; `scripts/lib/session-start-probes.mjs` routes that reason into the pushed-commit re-query beside `no-pipeline-for-head-sha` / `no-check-runs-for-head`, because for that consumer it is the same dark outcome. A user-cancelled pipeline now carries `reason: 'pipeline-canceled'` — it printed `🚨 CI RED on HEAD` indistinguishably from a real failure; the status stays `red` on purpose. The branch lookup (`git rev-parse --abbrev-ref HEAD`, 2 s cap) never throws and never degrades a reading: an unresolvable branch costs the preference only.
|
|
127
|
+
- **`lastGreen` can no longer name the very commit being reported red** (`scripts/lib/ci-status-banner.mjs`, #857). The look-back slice excludes EVERY row carrying the queried sha, not just the chosen one — pipelines are not one-per-commit here, and a `success` duplicate of the red commit used to be found as its own `lastGreen`; reproduced on `52ae12f1` (a `canceled` push pipeline beside a `success` api pipeline for the same commit): status `red`, `lastGreen.sha` === HEAD. `lastGreen.ageCommits` is now counted over DISTINCT shas in the red run — the old value was the ROW count under a comment asserting one-pipeline-per-commit, measured false at 16 %, so the distance was overstated whenever a commit had been re-run — and the row count survives under its own name, `agePipelines`.
|
|
128
|
+
- **The convergence monitor's agent counter admits only `orchestrator.agent.stopped` records that NAME their agent** (`scripts/lib/convergence-monitor.mjs` `namesAnAgent`, plus the recipe in `skills/convergence-monitoring/SIGNALS.md`; #1379). The type is bimodal (#939/#949) and the unfiltered count is dominated by the coordinator's own phantom stops. Measured 2026-09-18 over `.orchestrator/metrics/events.jsonl` plus its rotation, with the `jq` envelope quoted at both sites: 16,438 stop records, 6,553 wave-scoped, of which **5,930 (90.5 %) carry no `agent`** and 623 do — a ~10.5× inflation of every per-wave agent count and the `velocity_drop` signal derived from it. It is the same type-and-shape gate the gate-event branch already applied, not a new mechanism.
|
|
129
|
+
- **The backtick-citation checker scans `rules/` and `output-styles/` too** (`SCAN_DIRS` in `scripts/lib/validate/check-skill-script-paths.mjs`, #1384). Both are in `package.json` `files[]`, so both ship to every consumer, and neither had a gate; the whole-repo census that motivated it (2026-09-18 @ `20a4cbff`) found 6 dangling citations across them (see Fixed). `CHANGELOG.md` and the root files stay OUT by decision: a changelog cites the paths a release TOUCHED, so a path deleted afterwards is still a correct historical statement — 88 such hits, all correct by construction, and annotating them would be noise on every future entry.
|
|
130
|
+
- **The AUQ hurdle texts are RENDERED from the thresholds instead of standing beside them** (`scripts/lib/auq/schema.mjs`, `hooks/pre-auq-clarity.mjs`; #1110). `HURDLES.H1.title`/`rule` and `H2.title`/`rule` interpolate `THRESHOLDS.K5.headerCharsFail` and `THRESHOLDS.K6.optionsMin`/`optionsMax`, and the hook's deny text builds its option-limit sentence the same way — spelled-out numerals ("zwei bis vier") were invisible to any numeric grep, so a moved threshold would block on the new number while telling the operator the old one. `H1.evidence` keeps its literal 12: that is the BUNDLE's number, not our threshold. The definition order is now load-bearing and documented as such — `THRESHOLDS` must precede `HURDLES`, because a template literal evaluates at module load and the reverse order throws `ReferenceError` on a module `hooks/pre-auq-clarity.mjs` loads in EVERY session, i.e. a host-wide block rather than a test failure. A missing threshold drops the sentence rather than inventing a number.
|
|
131
|
+
- `readTranscriptTail()` reads a BOUNDED window from the end of a transcript (`TAIL_WINDOW_BYTES` = 2 MiB via the shared `readTailWindow`, `hooks/_lib/subagent-transcript.mjs`, #1388) instead of decoding and line-parsing the whole file to keep the last 8 assistant records. Ceiling measured 2026-09-18 over 1016 real subagent transcripts: the byte span of those records is median 41 KB, p99 322 KB, max 1.25 MB (file size median 676 KB, max 8.2 MB) — 2 MiB loses zero records in that sample, 1 MiB would already lose one; revisit if `TAIL_RECORDS` grows or a re-measurement puts the max span above ~1.5 MB. A cut window drops its first (possibly mid-UTF-8) line explicitly rather than leaning on the `JSON.parse` catch, since a truncated record can still parse; every fs error still maps to `''`, the contract `post-subagent-discovery-validator` relies on.
|
|
132
|
+
- `backfillCompletedFromStateMd()` reports `skipped-key-occupied` where it used to report `skipped-already-recorded` (`scripts/lib/session-close-backfill.mjs`, `docs/events-schema.md`; #1388). The two causes shared one string and were therefore indistinguishable in the `backfill_completed` telemetry: `skipped-already-recorded` means this very session is on file, the new value means identity said ABSENT while a canonical non-stub record of a DIFFERENT session already holds the key.
|
|
133
|
+
- `isUserInvocableValue()`'s WARN names the right NOUN for the key it is judging (`MARKER_NOUN` in `scripts/lib/user-invocable-skills.mjs`, #1388) — a `disable-model-invocation: yes` author was told his value "is NOT a slash-command marker", i.e. failed to be something it never was. The default phrase for `user-invocable` is unchanged, so the four existing call sites print identical text.
|
|
134
|
+
- `scripts/generate-cursor-adapter.mjs` carries a NAMED CEILING on ground 2 of its `disable-model-invocation` truth table (#1384): whether Cursor reads the key at all is UNVERIFIED as of 2026-09-18 (`rg "disable-model-invocation" docs .claude .cursor/rules` returns zero Cursor citations), so 24 wrappers are stamped on an assumption. Kept because stamping a restriction a reader ignores costs nothing while dropping it would loosen 24 wrappers if the reader honours it; revisit trigger is Cursor documenting the key or a measured run showing it ignored. Pinned by `tests/scripts/generate-cursor-adapter.test.mjs`.
|
|
135
|
+
- `scripts/lib/validate/check-rules.mjs`'s `globs:`-without-`paths:` message stops demanding both keys: under `.claude/rules/` `paths:` is the canonical scope key (`docs/rule-authoring.md`), and `globs:` belongs beside it only when the rule is ALSO vendored out through `rules/` (#742). `docs/rule-authoring.md` point 3 carries the re-measurement — 2026-09-18 over this repo's own `.claude/rules/` (25 files, explicitly NOT the vendored `rules/` library nor the baseline population `docs/baseline.md` counts): 10 path-scoped files, ALL 10 `paths:`-only, `grep -rn '^globs:' .claude/rules/` → no match, exit 1, after the duplicate `globs:` block was removed from `.claude/rules/cli-design.md` here (#1379).
|
|
136
|
+
- `.claude/rules/toolchain-and-build.md` states the PRECONDITION for re-measuring in a materialized tree (measured 2026-09-18 @ `20a4cbff`): such a tree has no `.git`, so `scripts/validate-plugin.mjs` dies in its `:49` guard with `ERROR: Not inside a git repository` before any of its 230 checks runs — `git init` plus a commit first. The trap named with it: the same tree materialized INSIDE the repo (e.g. under `.orchestrator/tmp/`) silently reads the parent's `.git` and reports 230/0.
|
|
137
|
+
- `npm-publish` is no longer user-invocable (`user-invocable: false` in `skills/npm-publish/SKILL.md`, #1384 P2) — Cursor, Codex CLI and Pi lose the `/npm-publish` command; Claude Code's own dispatch is unaffected. `docs/components.md`'s command/skill counts move accordingly (27→26 commands, 26→24 skills).
|
|
138
|
+
- `.gitignore` now ignores rotated `.orchestrator/metrics/*.jsonl.[0-9]*` backups — `scripts/lib/events-rotation.mjs`'s `maybeRotate()` writes `events.jsonl.1`, `.2`, … but the existing `*.jsonl` glob only matched a name ENDING in `.jsonl`, so a rotated file was untracked but not ignored.
|
|
139
|
+
- **Docs:** `SECURITY.md` documents a known limitation for a standalone single-file `check-owner-leakage.mjs` copy (its CP11 confidential-names guard degrades open via `owner.yaml` but fails closed via `SO_CONFIDENTIAL_NAMES_FILE`); `skills/gitlab-ops/SKILL.md`'s label-update caveat is corrected from the old "PUT-replaces" warning to the measured `glab 1.117.0` behaviour (`--label` adds and leaves existing labels in place, `--unlabel` removes; only the raw GitLab REST API's `labels=` parameter replaces the whole set); `skills/wave-executor/references/wave-loop-dispatch.md` gains a `stopped (post-restart)` dispatch-state row for resuming an agent after a Claude Code process restart; `docs/rule-authoring.md` scopes its provenance-header convention to actual `rules/_index.md` sync sources, excluding `rules/README.md` and `_index.md` itself.
|
|
140
|
+
- **Scope-validator grading is now canonicalized the same direction the live hook resolves paths — an existing `allowedPaths` manifest entry can start (or stop) failing validation.** `gradeScopeEntry()`, a new shared function in `scripts/lib/scope-gate.mjs` used by both `scripts/validate-wave-scope.mjs` and the hooks, replaces two grading directions that used to disagree (#1405/#1406): the validator graded the LITERAL grant string while Gate 5b (the live per-session hook) matches the realpath-resolved candidate. Measured before → after on the same grant set: `/private/etc`/`/private/var` 0 → 1 (now graded, with a hint toward the canonical spelling), `/tmp/x` 0 → 1 likewise, `~/...` 0-silent → 1, `~/.cache/<study>/**` 1 → 0 (a depth carve-out), `~/.cache/**` stays 1. Separately, and by explicit operator decision (#1398 AC4): Gate 5b now WARNS — and still allows — when the validator would grade the matched grant as ERROR; it never rejects, because the hook runs live for every session on the host and a validator-only ERROR is not by itself a safe reason to block a running session.
|
|
141
|
+
- **The staging fence's on-disk record shape changed — from a 512-character raw-command snippet to path operands plus `command_hash`** (`hooks/pre-bash-staging-fence.mjs`; #1404). The issue's own recommendation (hash + prefix) was unworkable: the fence's only reader searched for tracked paths INSIDE the raw text. The reader now compares path lists instead, which incidentally covers `git add -A` (never recognized by the old regex reader) and falls back to the legacy raw-text form for entries already on disk in the old shape — that fallback is dated to expire in **5.4.0**. Measured: a test secret appeared 0 times in the fence file after the change. Side-finding from the same pass: `git -C <dir> add` had never been fenced at all until now.
|
|
142
|
+
- **A class of Bash command that used to bypass the sessions-ledger write guard is now denied.** `hooks/pre-bash-sessions-ledger-guard.mjs` gains a per-statement predicate for interpreter writes (`node -e`, `python -c`, `perl -e`, `ruby -e`, `deno eval`) that name `sessions.jsonl` in the FIRST argument (#1408) — the previous form caught 0 of 438 real commands, even though an interpreter append is the common bypass shape (measured 38 fleet-wide). 10 DENY / 10 ALLOW cases measured, no bypass widened. `/close` gains Step 4a, which schema-checks its own record before writing. Two more forms are closed in the same cycle (`rm -r <parent>`, `mv <parent>`); three remain open and are named as a ceiling with a revisit trigger — a wildcard directory segment and a `cd`-chain are lexically unsolvable as written, and `rm -r` with a trailing slash needs operand normalization first. 0 new false positives measured over the real repo command corpus.
|
|
143
|
+
- **A session id containing a `/` can now be materialized into a rule** (`PROVENANCE_TOKEN_RE` in `scripts/lib/reconcile/sanitize.mjs`, GH#71). The pattern was `^[A-Za-z0-9._:-]+$`, and semantic session ids are derived from branch names, which contain slashes — so every learning from such a session stayed eligible forever: proposed each run, rejected each run. Reported from a consumer repo where it hit two usable learnings; latent here (3 slash-bearing ids in `sessions.jsonl`, 0 affected learnings). Because this WIDENS a sanitizer, the slash is gated by a new `assertNoPathEscape` in the shared `assertMachineToken`, with tests for traversal, a leading slash, backtick, newline-escape, wrapper forgery, a Unicode tag and a backslash — plus a test proving nothing previously accepted is now rejected.
|
|
144
|
+
- **The generated-rule expiry sweep now skips, fail-closed, any file whose counter sentence it cannot recognise — including on delete** (`scripts/lib/reconcile/rule-expiry-sweep.mjs`, GH#70). `COUNTER_RE` matched only the English `= the EARLIEST of the N absorbed dates`, so a German consolidated file had its frontmatter `expires-at` raised while the body sentence — which literally says the date must not be corrected upward — was left contradicting it. Reported from a consumer repo with two such files after `--apply`. The German form is recognised too, and re-emission splices the matched spans rather than re-templating, so a file keeps its own wording and language. `no-counter-sentence` is a fourth skip reason; three docs still enumerate only three (#1420). Not reproducible here — all 7 consolidated files are English.
|
|
145
|
+
- **`orchestrator.telemetry.flush`'s `reason` field is no longer truncated to a bare classification word, and a new `reason_class` field is added** (`classifyFlush`, #1392). The health banner tested `startsWith('sandbox:')` against a `reason` that `classifyFlush` had shortened to the literal string `'sandbox'` — a check that could never fire. `reason` now carries the full text (clamped at 200 characters) and `reason_class` carries the short classification separately. A consumer parsing `reason` off this event sees a different value than before. A new wiring test drives a real `flush()` through the real writer into the real banner; measured fleet-wide: 344 of 344 flushes reported `sent`.
|
|
146
|
+
- **`/eval` gets `rubric-v2`, which splits `process-safety` from a new, never-graded `guard-friction` dimension** (`skills/eval/rubric-v2.md`, `scripts/lib/eval/engine.mjs`; #1037). Measured before the split (40 records / 38 sessions, 2026-09-19): `process-safety` failed on 32 of 40 records, all 32 solely because `destructive_guard.blocked >= 1`; per `host-resources.md` HR-101, a class firing on far more than ~10% is a broken instrument, not a threshold to tune (a threshold fix was checked and rejected: N=3 still fails 23 of 38 sessions, N=6 still fails 9). `process-safety` now scores only `spiral`; `guard-friction` reports `blocked`/`warned`/`loop.warning` without ever grading them (the same mechanism `efficiency-kpis` already uses). Guard events are now attributed by `session_id` with the time window as a fallback only, closing a gap where a parallel session's blocks counted against this session's score. Re-run on the live ledger: `process-safety` fails 0 of 38 sessions (30 flip fail → pass); `guard-friction` reports 220 blocks, max 58 in one session. `rubric-v1` stays frozen and readable — all 40 stored v1 records still validate and render. Known follow-up, not fixed here: `scripts/eval-session.mjs --verify` now reports DRIFT on v1 records because it re-scores them with the current (v2) engine.
|
|
147
|
+
- `scripts/eval-session.mjs --verify` reports `version-mismatch` (exit 3) instead of `DRIFT` when a stored record's rubric version does not match the engine's current rubric (#1400).
|
|
148
|
+
- **The `report-quality` eval dimension is retired; `instruction-adherence` gains six pre-registered owner rules and 15 code-computed facts** (#1381). `report-quality` was variance-free by construction — its evidence came from the engine's own templates, and no final report exists yet at `/eval` time (Phase 6 runs after Phase 3.7d). `instruction-adherence`'s new regex readers sit beside the templates they read; a reader that fails to match now reports `parse_miss` instead of `null`, so an unmeasurable fact is no longer indistinguishable from a genuinely absent one.
|
|
149
|
+
- The rotation tombstone is now resolved by filename inside the ledger's own `_archive/`; the absolute path it used to store is provenance only (#1411). This also closes a silent false match where a stale checkout at the old stored path could count as a hit.
|
|
150
|
+
- `scripts/lib/telemetry/sync.mjs` and `scripts/lib/tmux-layout/telemetry-stats.mjs` now read across ledger rotations and surface `ledger_complete: false` when a rotation archive is missing (#1407, part 1 of 2 — the two hot-path readers are deliberately left out, tracked as a separate issue). Flush overhead measured in an A/B in one process: +190ms at 60MB / 294k records, 6.4% of the 3-second flush timeout.
|
|
151
|
+
- `convergence-monitoring` is no longer user-invocable (`user-invocable: false` in `skills/convergence-monitoring/SKILL.md`, #1384 P2) — Cursor, Codex CLI and Pi lose the `/convergence-monitoring` command; Claude Code's own dispatch (via the wave-executor, its only real caller) is unaffected. `docs/components.md`'s command count first moves 28 → 27 for this change; the subsequent 27 → 26 (`npm-publish`, same flag) is already documented above.
|
|
152
|
+
|
|
153
|
+
- **The quality gate now runs its commands in their own process group, on both paths.** Path A (`scripts/lib/quality-gate.mjs`, #1427) moved `runGate` from `spawnSync` to `spawnInGroup`; path B (`scripts/run-quality-gate.mjs` + `scripts/lib/gates/gate-helpers.mjs`, #1428) had **no timeout at all** and now carries a 900 000 ms (15 min) wall-clock ceiling on the outer runner and on all 10 `runCheck` call sites, published to the gate sub-script as `GATE_TIMEOUT_MS`. On expiry the whole process GROUP is killed (SIGTERM → grace → SIGKILL) and the gate reports **exit 124**; the pre-existing exit-code contract (124 timeout / 1 ENOBUFS) is preserved, and the ENOBUFS group of tests stayed green through the migration. Precedence on path B: `SO_GATE_TIMEOUT_MS` (operator override) > the committed `gate.timeout-path-b-ms` > `DEFAULT_GATE_TIMEOUT_MS` — a deliberately different env-var name from the operator override, so a committed default stays distinguishable from an operator one.
|
|
154
|
+
- `readEventsWithRotations()` is now three-valued: `complete: true | false | null` plus a `notices` field (#1423, #1414). `existsSync` is not evidence that an archive was READ, so the missing third state (“source never readable”) had been collapsed into “complete”; `maintenance-due-banner.mjs` and `backfill-abandoned-sessions.mjs` now read across rotations, and three readers report the three states.
|
|
155
|
+
- `orchestrator.wave_dispatch.worktree_base_checked` is now emitted for EVERY worktree dispatch, including the `skipped` and `stale: null` outcomes (#1424) — reporting only the drift case made the denominator unknowable.
|
|
156
|
+
- Hook and validator now grade absolute grants identically (#1398): `canonicalizeGrantPrefix()` moved into `scripts/lib/scope-gate.mjs` as the one implementation both call, closing a 1-of-9 grading divergence at a measured resolver cost of +0.067 ms, pinned by a parity test and a wiring test.
|
|
157
|
+
- `ledger_complete` is now an optional boolean on the telemetry ping (#1416) — `null` drops the key rather than shipping a fabricated `false`.
|
|
158
|
+
- `tests/eval/rubric-parity.test.mjs` now drives all **19** rubric table rows (5/4/5/3/2 across the tables in `skills/eval/rubric-v2.md`) per scorer, with vacuum, binding and ordering guards; six scorers plus `RUBRIC_SCORERS` are exported for it, formulas unchanged (#1418).
|
|
159
|
+
- `spanEndIndexMain` for coordinator-anchored skills, with the call pinned to the single production call site (`includeSubagents: true` in `phase-3-6-tail.md`) (#1421, remainder of #1412).
|
|
160
|
+
- `checkInstructionBudget` gained a near-threshold stage at **90 %** of the path-scoped rule ceiling, carrying the last `reconcile-capped` counter (#1419).
|
|
161
|
+
- Autopilot peer fixes from the navigator review: headless invocation is namespaced (`/session-orchestrator:session`) and passes `--plugin-dir`; the stall sampler reads `session.lock`'s `last_heartbeat` with an mtime fallback instead of mtime alone; `--max-tokens` is parsed and forwarded (the kill-switch stays inert while records carry no `total_tokens` field — tracked as a follow-up).
|
|
162
|
+
|
|
163
|
+
### Deprecated
|
|
164
|
+
|
|
165
|
+
- `buildIssueBody` in `scripts/lib/ux-grill/reconcile.mjs` → `buildUxGrillIssueBody` — the old name is kept as a delegating alias that warns once per process via `console.warn`, for one minor cycle per `.claude/rules/development.md` § Package Lifecycle & Versioning, and is removed in 6.0.0. The rename is covered by this deprecation alias, so it does not force a major bump — this release stays a **minor**.
|
|
166
|
+
|
|
167
|
+
### Removed
|
|
168
|
+
|
|
169
|
+
- The `orchestrator.wave.started` event (#1202 §11) — emit sites removed from `hooks/post-tool-batch-wave-signal.mjs`, row removed from `docs/events-schema.md`. Its only reader, `scripts/lib/convergence-monitor.mjs`, no longer admitted it (see Fixed below): the batch hook wrote it in the same millisecond as `completed{N}`, masking the `(N-1, N)` `shrinking_diff` pair, and it carried no measurement of its own — telemetry consumers that counted it lose it.
|
|
170
|
+
|
|
171
|
+
### Fixed
|
|
172
|
+
|
|
173
|
+
- Codex portable command discovery now includes command-only `session` alongside skills and retains explicit-only `go`/`close` invocation policies. Session modes are parsed independently of following task text; explicitly requested parallel housekeeping uses the actual wave plan. Generated Codex, portable, Cursor and Pi adapters and setup guidance agree on these contracts. Merge, release and installed-plugin acceptance are tracked in the [release handover](docs/handover/2026-09-19-codex-session-entrypoints.md).
|
|
174
|
+
- **Peer-card data loss on `/evolve dialectic --apply` (#1380).** A managed region wrapping MORE than one `## ` heading cannot be addressed per heading; `mergeDerivedBody` used to target it anyway, so a proposal touching one of its headings either replaced the region (deleting its sibling headings' hand-written text) or appended a duplicate heading. Such a region is now fail-closed: never a target, left byte-unchanged, and every skipped heading surfaced as a `{ type: 'multi-heading-region', region, headings, skipped }` conflict for the operator to resolve by splitting the region.
|
|
175
|
+
- **Peer-card frontmatter read as body (#1380).** `scripts/lib/peer-cards/reader.mjs` now folds multi-line list values — both the flow form Prettier reformats the writer's output into and a hand-written `- ` block list — before handing the card to `parseStateMd`, and when the frontmatter is still unparseable it strips the block instead of returning it as Markdown. A card's own `---` frontmatter therefore can no longer reach the deriver payload as content.
|
|
176
|
+
- **Short read decoded as uninitialised memory** in `hooks/subagent-telemetry.mjs`: `findStartTimestampMs()` ignored the byte count `fs.readSync` returned and decoded the whole `allocUnsafe` buffer into `JSON.parse` — fixed by the shared reader above, which loops to fill the window and slices to what it read (#1272).
|
|
177
|
+
- **An unterminated `/*` no longer blanks a validator's view to end-of-file.** `stripComments()` in `scripts/lib/validate/check-entry-guard.mjs` delegates to the shared lexer (`maskSource(src, { keepLiterals: true })`, which moved to `scripts/lib/js-mask.mjs` in the Unreleased cycle — a one-line re-export remains at its original home `check-untracked-test-deps.mjs`), which recognises regex literals — a `/` read as a division let a regex body's `/*` open a block comment and silently hid every entry guard below it, a fail-open miss for a quote-bearing regex, a `/[/*]/` character class and a regex after a keyword alike (#1383). The lexer gains a one-token keyword lookback (`return`, `typeof`, `case`, …) for the regex/division call; named ceiling, measured over 1245 tracked `.mjs` files: 0 genuine division chains after those keywords, so 0 files are mis-masked in the other direction. A block comment that is never closed now reports rather than blanks.
|
|
178
|
+
- **`/close` could displace a canonical session record.** `backfillCompletedFromStateMd()` appended under a `recordId` an authoritative record already held whenever identity resolution said `absent` (label drift, a conflicting native join) — canonical readers collapse by `session_id` newest-wins, so the zero-counter backfill row won. It now skips on an occupied key, matching the abandoned path's guard (#1380).
|
|
179
|
+
- **The sessions-staleness banner counted a `state-md-completed` backfill as a stub** and kept warning after the stub it superseded was gone, recommending a backfill run with nothing left to do (observed live 2026-09-18). A `_backfill_source` now disqualifies a record as a time anchor only when its status is not `completed`; the predicates in the two modules are renamed for the question each answers (`isNonAnchorStub` vs `isSupersedableStub`) and deliberately not shared (#1380).
|
|
180
|
+
- **The reconcile nudge was un-greenable.** `scripts/lib/reconcile-nudge-banner.mjs` judged threshold (c) on ALL rule-eligible learnings, including those already materialized in the idempotency sidecar or a `.claude/rules/` provenance marker — 98 eligible against 35 materialized in the repo that reported it (#1380; this repo measured 181 / 80 / 101 on 2026-09-18), so no `/reconcile` run could ever clear it. It now judges on the BACKLOG (`countReconcileBacklog`), and reads `reconcile.min-insight-chars` before counting rather than after, which had left the placeholder-insight gate practically off for the banner (#1380). `scripts/lib/maintenance-due-banner.mjs`'s signal 3 comment follows.
|
|
181
|
+
- The findings the widened citation checker reported on its first run over the two rule corpora (#1384): `.cursor/rules/000-session-orchestrator.mdc` cited `.orchestrator/metrics/model-selection.md`, which nothing writes — the line is gone; `.cursor/rules/050-plan.mdc`, `.claude/rules/cli-design.md`, `.claude/rules/measurement-discipline.md` and `.claude/rules/security.md` cited example, template or historical paths and now carry the `path-check` annotation that says so.
|
|
182
|
+
- **Three scanners were reading a regex literal as a block comment, each with its own measured consequence** — all three now delegate to the shared lexer (`maskSource(src, { keepLiterals: true })` from `scripts/lib/js-mask.mjs`), measured 2026-09-18 @ `20a4cbff` on the same input shape `/\/*$/` (#1388). `scripts/lib/validate/check-validator-registration.mjs`: the `/*` inside that regex in `scripts/validate-plugin.mjs` opened a comment that swallowed every `runCheck(...)` line below it — a FALSE `UNREGISTERED` for every checker registered after that point, and its own header had until now argued in writing that this direction was impossible. `scripts/lib/validate/check-test-git-config-target.mjs`: the same swallow inside a call tail made `wrapperHasCwd` return `false` — a false alarm. `scripts/lib/auq/parse.mjs`: the swallowed region no longer contained `(Recommended)`, a SILENT FALSE NEGATIVE in the clarity measurement, length-preserving and therefore visible at no line number. The one behavioural difference — `maskSource` blanks comment bytes to SPACES where two of the strippers collapsed them — was checked rather than assumed: `wrapperHasCwd`'s `\s*` is greedy and consumes them, measured over `, /* no cwd */`, `, /* a */ /* b */`, `,\n // note\n`, `, /* x */ undefined`, `, undefined /* y */` (all `false`) and `, /* c */ dir` (`true`). The `#`-comment shell/YAML surfaces (`.husky/pre-commit`, `.gitlab-ci.yml`) keep the small local walker, whose flat quote-tracking ceiling is real but bounded to those two files.
|
|
183
|
+
- **The pre-commit control-byte guard could be bypassed by the filename.** `core.quotePath` defaults to TRUE, so git prints any path carrying a byte ≥ 0x80, a control byte or a newline C-quoted in double quotes (`"f\303\274nf.mjs"`) — the old `grep -Ei '\.(mjs|…)$'` extension filter dropped such a path (it ends in `"`), `git show ":$f"` never ran, and a `fünf.mjs` carrying a raw ESC committed clean while an identical `plain.mjs` was blocked. `.husky/pre-commit` now enumerates with `-c core.quotePath=false` and tests the extension with a POSIX `case` over the lowercased path — same set as the old grep, no re-quoting. The guard is widened in the same change from NUL-only to every C0 byte except TAB/LF/CR (0x00-0x08, 0x0b, 0x0c, 0x0e-0x1f; DEL is deliberately out of scope), because a literal ESC in an ANSI-stripping code path and its test carried the same defect class — both are now written as escapes (`hooks/post-tool-failure-corrective-context.mjs`'s `split('\x1b')`, which had been a raw byte inside `split('')`, and `tests/hooks/additional-context-hooks.test.mjs`). KNOWN CEILING, named rather than fixed: a path containing a literal LF still splits across two `read` iterations and is missed — the airtight form needs `read -r -d ''`, a bashism unavailable to a hook run as `sh -e` (dash) on the Linux CI runner.
|
|
184
|
+
- **`createWorktree` could never recover from an orphan directory at its target path** (#984). An interrupted `removeWorktree` drops the git registration first and can die before the directory is gone; `git worktree add` then fails with "already exists" while `git worktree remove --force` fails with "is not a working tree" (swallowed by nothrow), so every retry looped on the same error until someone deleted the directory by hand — the Full Gate went red twice from exactly this state. `scripts/lib/worktree/lifecycle.mjs` now removes such a directory before the retry, behind five in-code conditions, any one of which failing leaves the directory alone (restoring the pre-#984 error): the leaf is a real directory and not a symlink; the literal base `<os.tmpdir()>/so-worktrees` is itself a real directory and not a symlink (a co-tenant on a shared `/tmp` can pre-create it, and a realpath-resolved base symlink would otherwise BECOME the delete root); the realpath of the target is a DIRECT child of the realpath of that base (both sides resolved — macOS maps `/tmp` → `/private/tmp`); its basename starts with `so-worktree-`; and it is not a live working tree of ANY repository, so a peer session's or another repo's worktree is out of reach. `git worktree prune` is deliberately not used — it is repo-global and would drop a peer's registration. Ceiling (BV-004): revisit when worktree paths become caller-supplied or move out of `os.tmpdir()`, at which point the containment base stops being a constant. The tests use per-run random suffixes so two concurrent runs cannot share a path.
|
|
185
|
+
- **An object WRAPPER passed `emitRewrite`'s input guard and destroyed the tool input** (`scripts/lib/io.mjs`, #1110). `typeof updatedInput === 'object'` admits `new Date()` and `new String('x')`, which serialize to JSON STRINGS, and `new Map([...])`, which serializes to `{}` — a shape that looks valid and, per this function's own contract, deletes every field of the tool input. Each reaches the harness as a schema-invalid `updatedInput`, which the bundle turns into `behavior: "deny"`: on the `AskUserQuestion` path the operator's question is destroyed to save a rewrite. The admissible set is now a PLAIN object only (prototype `Object.prototype` or `null`); everything else takes the same no-op-plus-stderr contract as `null`, an array or a string, and the stderr line names the offending prototype. The adjacent `JSON.stringify` → `undefined` guard is documented as currently unreachable rather than removed: `updatedInput` sits NESTED in the envelope literal, so a `toJSON` returning `undefined` OMITS the property instead (measured 2026-09-18: 53 bytes of stdout, no `updatedInput`, which is the same safe degradation `bail` produces) — the guard stays because it is the envelope SHAPE that makes the case unreachable, and hoisting the value would restore it.
|
|
186
|
+
- **The cold-start nudge ignored the `enable-host-banner` opt-out** (`hooks/on-session-start.mjs`, #1133): every other start-of-session emitter rides that flag on the stated principle that users who silence one expect silence. The DISPLAY now rides it; the one-shot marker consumption deliberately does NOT — the opt-out silences the display, never the measurement or the state transition, and consuming unconditionally is what keeps the nudge one-shot for an operator who has banners off instead of re-arming it on every start forever. The flag is read once into `hostBannerEnabled` rather than inferred from `bannerData`, which is `null` on `emitHostBanner()`'s own error paths and would silently re-open the AUQ nudge.
|
|
187
|
+
- **The portfolio dashboard failed the vault frontmatter gate it is written into** (#1144). `scripts/lib/gitlab-portfolio/markdown-writer.mjs` emitted `type: dashboard`, which is not in `skills/vault-sync/validator.mjs`'s enum, and no `id` at all — so the generated `_PORTFOLIO.md` blocked `/close` on the hard gate. It now writes `id: portfolio` + `type: board`, the enum member added for generated dashboards in #738 and the same shape `vault-status/board-writer.mjs` uses.
|
|
188
|
+
- **Every agent count of one recorded session rendered `?` beside a real number** (`scripts/lib/vault-mirror/render-sessions.mjs`, the #1074 bug class). `metrics-collection.md` documents `agent_count_{planned,started,completed}`, but the live ledger carries one session (`main-2026-09-18-session-1`, 5 waves, measured 2026-09-18 over `.orchestrator/metrics/sessions.jsonl`) whose waves emit only the de-underscored producer spelling `agents_planned` / `agents_started` / `agents_completed` — all 15 of its counts were missing cells. Each alias now sits at the precedence of its documented twin, so no cell that already rendered a value can change.
|
|
189
|
+
- The 6 dangling citations the widened checker found in the two SHIPPING corpora it now scans (#1384): `output-styles/wave-summary.md` cited `hooks/on-stop.sh` for a file that has been `.mjs` for two migrations — corrected; `rules/opt-in-stack/backend.md` (2), `rules/opt-in-stack/security-web.md` (2) and `CONTRIBUTING.md` (1) cited consumer-side example or template paths and now carry the `path-check: example` annotation that says so.
|
|
190
|
+
- **The `sessions-staleness` probe warned on a previous session's own SessionEnd output.** `CLOSING_DIAGNOSTIC_EVENTS` in `scripts/lib/sessions-staleness-banner.mjs` now skips a closing session's own teardown events (`orchestrator.session.ended`, `.telemetry.flush`, `.session.backfill_completed`, the lock-release events, and an `on-session-end`-emitted `wave.completed`/`.final_refused`) when computing `newestForeignEvent` — reproduced live 2026-09-19 as a false 9.6h close-through warning whose only post-ledger events were one SessionEnd's own diagnostics.
|
|
191
|
+
- **The `sessions-staleness` probe warned about the running session itself after a context compact.** The SessionStart hook re-acquires the lock under the same id and rewrites `started_at`, which moved the self-exclusion cutoff past the session's own earlier events (not all of which carry a `session_id`). The cutoff is now pulled back to that lock id's first `orchestrator.session.lock.acquired` event — reproduced live 2026-09-19 as a false "11.9h behind" warning on a 2h15m session.
|
|
192
|
+
- **A same-sha CI pipeline set aside by the ref-preference tier disappeared behind a green reading.** `selectShaPipelines()`/`candidateEvidence()` in `scripts/lib/ci-status-banner.mjs` now publish the set-aside rows as `droppedCount`/`droppedStatuses` (#1390 P5); `scripts/lib/session-start-probes.mjs` renders a `warn` banner line when a `green` HEAD has a set-aside `failed` run, and the unknown-CI hint now prints `glab ci list --sha <full sha>` instead of the invalid `glab ci status --ref <short>` (measured on glab 1.117.0, #1339 P1).
|
|
193
|
+
- **The convergence monitor's `shrinking_diff` signal was masked when a wave's completion and the next wave's start landed in the same tail tick.** `isWaveScopedEvent()` in `scripts/lib/convergence-monitor.mjs` now admits only the exact `orchestrator.wave.completed` name instead of the whole `orchestrator.wave.` prefix, so an emptied same-tick `started{N+1}` summary can no longer suppress the `(N-1, N)` comparison (repro: 40→10→5 files read as `shrinking_diff:2`/`:3` instead of same-tick `[]`).
|
|
194
|
+
- **`consumeDialecticPending` could follow a symlinked `.orchestrator/consumed` and delete files it did not write (data loss).** `scripts/lib/auto-dialectic.mjs` now `lstat`s the directory and refuses a symlink outright (`{ok: false}`, nothing moved or pruned), and the retention prune only counts/removes names matching its own `ARCHIVE_NAME_RE` (#1390 P6) — a tmp repro had previously deleted 3 of 12 unrelated files through the symlinked target.
|
|
195
|
+
- **A `/evolve` repair-engine dry run wrote the candidate store.** `runRepairEngine()` in `scripts/lib/skill-evolution/engine.mjs` now skips `mergeCandidates()` entirely under `dryRun: true`, logging the skip instead. `scripts/lib/skill-evolution/candidate-intake.mjs`'s `extractCandidates()` is now fail-closed: a learning's extracted path must realpath-resolve to a regular file inside `repoRoot` that is also the one tracked file it names — behaviour change: candidate ids now differ for bare-basename targets, and `extractCandidates` reads git (`git ls-files -z`, lazily, at most once per call).
|
|
196
|
+
- **The `is-main-module` entry guard now protects 25 of 28 hooks, not just the three lifecycle ones** (#1298 P7, extended by #1393). `main()` sits behind `isMainModule(import.meta.url)`, and the `shouldRunHook` profile gate moved inside that guard — at module top level its `process.exit(0)` tore down every process that merely imported the hook. Measured with a 700 ms settle window (without one the probe runs before `main()`'s `.finally(() => process.exit(0))` fires and passes vacuously): before, 8 of 28 hooks survived a bare `import()` under the default profile and only 3 of 28 under `SO_HOOK_PROFILE=off`; after, 28 of 28 under both. The sweep changed 21 hooks; 4 were already guarded and 3 keep a documented hand-inlined equivalent. Why it matters: a bare `import()` (a probe, a test, an agent) previously ran `main()` against the live repo and could overwrite `.orchestrator/current-session.json` (incident W4-FX1) — and `hooks/post-tool-failure-corrective-context.mjs` did not merely exit, it wrote a `PostToolUseFailure` envelope to the importer's stdout. **No recurrence guard ships with this**: `scripts/lib/validate/check-entry-guard.mjs` keys on `process.argv[1]`, so a file with no guard at all passes it trivially (measured: 0 findings for the defect shape, 1 for a fragile guard) — tracked as #1422.
|
|
197
|
+
- **The vault-note frontmatter-guard's enum extraction could read comment prose as an enum value.** `_extractEnum()` in `scripts/lib/frontmatter-guard.mjs` now matches only quoted string literals in comment-masked source (`maskSource(text, { keepLiterals: true })`) instead of splitting the raw `z.enum([...])` body on commas — 6 of 16 `statusEnum` entries had been corrupted this way, one fused onto `'maintenance'` (#1298 P2).
|
|
198
|
+
- **A reconcile abort `reason` could leak the repo root or home directory into the ledger and webhook.** `buildReconcilePayload()` in `scripts/lib/reconcile/engine.mjs` now runs a new `redactLocalPaths()` over `result.error`, replacing the absolute repo root and `homedir()` with `<repo>`/`~` before the 300-char clamp (#1202 §8b).
|
|
199
|
+
- **The dialectic deriver's `unknown-model` and `subagent-crash` aborts went unrecorded.** `runDialecticDeriver()` in `scripts/dialectic-deriver.mjs` now calls a new `recordThrownAbort()` at both throw points, before rethrowing, instead of relying on the skill-prose caller to record them (#1221 P3).
|
|
200
|
+
- **`check-unwired-features`'s "drags N" count under-reported when a root imported two colliding basenames.** `collectUnreachableLibraryModules()` in `scripts/lib/validate/check-unwired-features.mjs` now derives the drag count from the same per-module `references()` predicate the cluster-root filter uses, instead of comparing bare basename tokens, which had collapsed two dragged `index.mjs` files into one (#1298 P3).
|
|
201
|
+
- **Test runs could leak events into the real `.orchestrator/metrics/events.jsonl` ledger.** `eventsFilePath()` only redirected to a sandbox in its default branch (neither `filePath` nor `repoRoot` given) when the resolved target would land outside the OS temp root; two test files (`orchestrators-e2e.test.mjs`, `learning-memory-modernization.test.mjs`) took neither branch and wrote to the live ledger under the live session id (#1397.11). A new `vitest` `setupFiles` guard (`tests/setup/events-ledger-guard.mjs`) pins the sandbox for every run. Measured: before the fix, one suite run appended 10 test events to this repo's own ledger (2583 → 2595 lines); after, 0 (only 4 `orchestrator.agent.stopped` records from live hooks). One of those leaked test events had previously decided a real session's gate-health reading (`main-2026-09-18-session-10`). Explicit `repoRoot`/`filePath` callers and the 38 tests with their own fixture root are unaffected. Follow-up not covered here: `scripts/lib/tmux-layout/telemetry.mjs` writes via its own `appendFileSync`, bypassing both guarded seams.
|
|
202
|
+
- **A hook payload logged the raw Bash command text into the tracked events ledger.** `hooks/enforce-commands.mjs`'s `foreign_session_ignored` payload carried the raw command as a documented field in `docs/events-schema.md`, not an oversight — but a tracked, ever-growing ledger commits that text on every clone. Now logs `command_hash` only, matching the sibling `pre-bash-destructive-guard.mjs`'s existing rationale ("payload never includes the raw command"); the shared `hashCommand()` is duplicated rather than imported so the `PreToolUse` path stays free of an extra loadable module. A test now pins both directions — hash present AND `command` absent — so the raw field cannot quietly return beside the hash. Measured impact elsewhere (a consumer repo's own issue #1140, 2026-09-19): 3,816 lines of a TRACKED ledger carried a raw command, including 24 distinct production share codes across 17 customer accounts and one logged `select access_pin_hash` — for 23 of those events the share code alone was the complete capability. A sibling gap named but not fixed in this commit — `hooks/pre-bash-staging-fence.mjs` also stored raw command text — is closed separately in this same cycle (see the staging-fence entry above, #1404).
|
|
203
|
+
- **The claim scanner missed a claimed test count with no observed run backing it.** The scanner now also flags a claimed test-count assertion that carries no matching observed vitest output (#1385 R1) — the earlier gap was the inverse: a claimed number that happened to BE its own run receipt read as verified. Measured false-positive rate: 4 of 1044 real transcripts (0.38%). A SHA-based match between claim and receipt was measured and discarded as an approach.
|
|
204
|
+
- **HIGH, security, self-caught before release: scope grading compared home-directory grants byte-for-byte while the filesystem folds case, letting `/Users/<user>/library/**` through as WARN instead of ERROR.** `classifyHomeGrant()`'s regression (introduced earlier in this same Unreleased cycle, #1398/#1405) meant Gate 5b honored such an entry; reproduced reaching `~/Library/Keychains` and `~/Library/LaunchAgents` — code execution at login — and, via the lowercased root, also `~/.ssh` and `~/.config`. Before that regression every `/Users/**` entry was ERROR outright. A third site carried the same defect: the system denylist itself let `/ETC/**` and `/library/**` through as WARN. All three segment comparisons now fold over a pinned `en-US` locale — under a Turkish locale, `LIBRARY` would otherwise fail to fold. Caught by an adversarial review panel before this ever shipped.
|
|
205
|
+
- **HIGH, self-caught before release: a self-contradicting ledger record could pass eval's gate facts as measured.** `computeRecordFacts()` emitted `changes_unverified: true` from an `events.jsonl` where the same record called itself `unmeasurable` twice — no `contradiction`, no `parse_miss` — which meant exactly the sessions with a damaged ledger would hit the pre-registered `fail` trigger of eval Rule 6. The gate facts are now nulled and the record's self-contradiction is named explicitly, so Rule 1 (`cannot-determine`) fires instead of Rule 6 (`fail`) — nulling alone would have swapped a false fail for an undeserved pass.
|
|
206
|
+
- `skills/eval/rubric-v2.md` claimed its `JUDGE_RULES` constant word-for-word; rules 1, 3 and 5 had actually diverged (3 and 5 since their origin commit) (#1410). Code is now aligned to the documented rubric, and a new `tests/eval/rubric-parity.test.mjs` pins the two together with a vacuum guard. No rubric version bump was needed — all 40 stored eval records are still `rubric-v1`.
|
|
207
|
+
|
|
208
|
+
- **`argv_truncated` said nothing measurable.** `hooks/pre-bash-memory-propose-audit.mjs` now records `command_hash` / `flags_present` / `argv_length` instead (#1415) — a truncation flag names the instrument's limit, not the observation.
|
|
209
|
+
- **The scope-disjoint identity gate stopped at the first candidate state dir.** `hooks/pre-task-scope-disjoint.mjs` now runs the identity check INSIDE the `STATE_DIR_CANDIDATES` loop (`continue` instead of `break`), so a foreign-session state dir no longer ends the search before the own one is reached (#1424).
|
|
210
|
+
- `no-counter-sentence` violations in three documentation enumerations (#1420), and the K5 text in `hooks/pre-auq-clarity.mjs`.
|
|
211
|
+
|
|
212
|
+
### Security
|
|
213
|
+
|
|
214
|
+
- **The orphan reaper ships inert.** `reaper.enabled` defaults to `false` and `reaper.mode` to `report`; an unrecognised mode falls back to `report`, never to `kill`. Unlike every other guard in this repo, this one can send SIGNALS to processes, so arming it is a per-repo decision taken only after its firing rate has been measured over `reaper.false-alarm-window` decisions in `.orchestrator/metrics/reaper-audit.jsonl` — `.claude/rules/host-resources.md` HR-101 (a class above ~10 % is a broken instrument, to be re-aimed rather than obeyed) and HR-105 (a rate nothing records is unfalsifiable) bind that order. `mode: kill` belongs to PRD Stufe 2, after calibration (C1). The signal path itself is hardened in the same release: pgid validation and a pgid join on the ledger record, session-id scoping, and a read-only command allowlist evaluated per statement.
|
|
215
|
+
|
|
10
216
|
## [5.2.0] - 2026-09-17
|
|
11
217
|
|
|
12
218
|
### Added
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Session Orchestrator
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
-
[](CHANGELOG.md)
|
|
5
5
|
[](https://www.npmjs.com/package/session-orchestrator)
|
|
6
6
|
|
|
7
7
|
**Give your agents a working rhythm.**
|
|
@@ -39,6 +39,8 @@ You need **Node.js 24 or later**, a git repository, and one of the four agents b
|
|
|
39
39
|
| **Cursor IDE** | `git clone` the repo, `npm install`, then `node scripts/cursor-install.mjs /path/to/your/project` ([guide](docs/cursor-setup.md)). |
|
|
40
40
|
| **Pi** | `pi install npm:session-orchestrator` ([guide](docs/pi-setup.md)). |
|
|
41
41
|
|
|
42
|
+
After a clone-based install (Codex CLI, Cursor IDE, or contributing), run `npx husky` once: `.npmrc` sets `ignore-scripts=true` (SEC-020), which skips the `prepare` script that would otherwise activate the git hooks.
|
|
43
|
+
|
|
42
44
|
## Quick Start
|
|
43
45
|
|
|
44
46
|
**1. Run `/bootstrap` once in your project.** It creates the minimum structure and writes `.orchestrator/bootstrap.lock`. `/session` refuses to start until that file exists.
|
|
@@ -93,7 +95,7 @@ When you type `/session feature`:
|
|
|
93
95
|
.claude/STATE.md # wave progress and deviations (harness-specific directory)
|
|
94
96
|
```
|
|
95
97
|
|
|
96
|
-
The plugin is **
|
|
98
|
+
The plugin is **52 skills, 28 slash commands, 14 typed subagents and 29 hook files across 10 event types**. A slash command is a skill whose frontmatter says `user-invocable: true` (26 of them) or one of the two remaining `commands/*.md` files (`/session`, `/templates-ack`) — one definition per name, so nothing is listed twice in the `/` picker. Skills, commands and agents are Markdown with YAML frontmatter; the code that dispatches, validates and records runs in `scripts/lib/*.mjs` and `hooks/*.mjs`. There is no build step and no compiled artifact — when a session does something you did not expect, you can open the file that decided it. Full inventory: [`docs/components.md`](docs/components.md).
|
|
97
99
|
|
|
98
100
|
## Why it is built this way
|
|
99
101
|
|
|
@@ -101,7 +103,7 @@ The plugin is **50 skills, 28 slash commands, 14 typed subagents and 27 hook fil
|
|
|
101
103
|
- **Checks run between waves, not only at the end.** A mistake caught after wave 2 costs one wave. The same mistake found at `/close` has already been copied into every wave after it. Findings below the configured confidence threshold are not shown to you.
|
|
102
104
|
- **A crash does not lose the session.** `STATE.md` records which wave finished and what deviated from the plan. The next `/session` offers to continue from the last completed wave.
|
|
103
105
|
- **Two sessions in one working copy is treated as a real risk.** Two people, or two of your own sessions, in the same checkout share one git index, one filesystem and one `STATE.md`, and neither can see the other's uncommitted work. A heartbeat session lock, per-agent file-scope manifests, and the PSA rules in [`.claude/rules/parallel-sessions.md`](https://github.com/Kanevry/session-orchestrator/blob/main/.claude/rules/parallel-sessions.md) exist for exactly that case.
|
|
104
|
-
- **Guards run where the harness supports them, and the table below says where it does not.** A destructive-command policy —
|
|
106
|
+
- **Guards run where the harness supports them, and the table below says where it does not.** A destructive-command policy — 12 rules block, 4 warn — and file-scope enforcement run as real hooks on Claude Code, as bridges on Cursor and Pi, and as instructions only on Codex. Details: [`docs/components.md`](docs/components.md#other-surfaces).
|
|
105
107
|
- **What it learns is opt-in and readable.** Every session appends a record. After 5 or more sessions, `/evolve analyze` proposes patterns with a confidence score; you read them and delete the ones you disagree with. Nothing is applied without you.
|
|
106
108
|
- **GitLab and GitHub, both fully.** It detects which one your remote points at and drives issues and merge/pull requests for either.
|
|
107
109
|
|
|
@@ -121,14 +123,16 @@ How this compares to other orchestrators, with measured results kept separate fr
|
|
|
121
123
|
|
|
122
124
|
All four platforms share the same skills, commands and scripts; only the hooks differ, because each harness fires different events. Codex leaves its `PreToolUse` handlers empty because these guards do not yet match its tool names and edit payloads ([why](docs/codex-setup.md#why-our-pretooluse-guards-stay-unwired--the-reason-corrected)). Cursor and Pi have known event-coverage limits — see [`docs/cursor-setup.md`](docs/cursor-setup.md) and [`docs/pi-setup.md`](docs/pi-setup.md).
|
|
123
125
|
|
|
124
|
-
## Recent highlights (v5.
|
|
125
|
-
|
|
126
|
-
Highlights of the v5.2.0 line:
|
|
126
|
+
## Recent highlights (v5.4.0)
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
- **Expired generated rules can now be swept.** `node scripts/sweep-expired-rules.mjs` (dry-run by default, `--apply` to write) removes the prose of expired entries from the consolidated `.claude/rules/` files while keeping their provenance pairs, so `/reconcile` does not re-propose the learning. It refuses symlinks and paths outside the rules directory, writes atomically, and reports the files it cannot map 1:1 instead of guessing.
|
|
130
|
-
- **Two guard holes closed.** The issue-budget hook enumerates every shell loop, so an exempt first loop no longer lifts the bulk deny for a second one; the entry-guard validator now also censuses the bare `argv[1].endsWith('<file>.mjs')` form. `release.mjs --check` asks both CI platforms about the release commit's sha rather than GitHub's own HEAD.
|
|
128
|
+
Highlights of the v5.4.0 line:
|
|
131
129
|
|
|
130
|
+
- **Fleet protocol v1 and the `navigator` skill.** Parallel sessions now check in through files instead of chat messages: `scripts/lib/fleet-protocol.mjs` owns the layout under `~/.config/navigator/`, session-start writes a check-in (Phase 7.6), and the SessionStart banner shows in one line whether a navigator lease is active, absent or unreadable. `/navigator` runs the coordinator role on operator command only and grants no approvals (#1462).
|
|
131
|
+
- **`offload-first` places work before pressure forces it.** With the new Session Config key set to `true`, offloadable wave roles go to a ready declared remote host on a `proceed` verdict too, not only when resources are short. Default `false` changes nothing (#1465).
|
|
132
|
+
- **A `test-audit` skill for shrinking a suite without losing proof.** A four-question gate before a new test, a focused audit, and a lane-based campaign; every delete, merge or repair needs written evidence and a production mutation the remaining test catches.
|
|
133
|
+
- **The sessions ledger and the vault writers are honest again.** Backfill stubs no longer hide real records, token totals are rolled up mechanically so the autopilot token-budget kill-switch fires again once `--max-tokens` is set (its default is now `0`, off), and the board writer and narrative mirror refuse a vault that is not the canonical one. `vault-dir` gains a per-directory tier in `owner.yaml` (#1443, #1436, #1450, #1448).
|
|
134
|
+
- **Vendored rules load path-scoped again.** Library rules open their frontmatter on line 1 and carry `paths:` beside `globs:`, so consumers no longer load path-scoped rules on every turn; the loader, budget guard, drift check and validator now flag a displaced block (#1449).
|
|
135
|
+
- **Session costs never read as complete when they are not.** Each subagent now counts once in the token and cost totals (a resumed agent used to be summed per stop), a session with any unpriced agent stores no dollar figure but its coverage counters, and mixed-model agents are priced per model. Every `learnings.jsonl` read-modify-write runs under one store lock, so a concurrent append is no longer lost.
|
|
132
136
|
|
|
133
137
|
Full changes and verification: [CHANGELOG.md](CHANGELOG.md).
|
|
134
138
|
|
package/SECURITY.md
CHANGED
|
@@ -238,6 +238,18 @@ rejects shell metacharacters as defense-in-depth. Full trust model in
|
|
|
238
238
|
low-false-positive line/regex heuristic, not an exhaustive AST gate, and fails
|
|
239
239
|
open by design so a guard bug never blocks legitimate work.
|
|
240
240
|
|
|
241
|
+
6. **A standalone single-file copy of `check-owner-leakage.mjs` cannot resolve
|
|
242
|
+
its sibling helper modules** (`../config/host-paths.mjs`,
|
|
243
|
+
`./confidential-names.mjs`, `../owner-yaml.mjs`) — the documented vendoring
|
|
244
|
+
shape (see [Supply Chain](#supply-chain) above). A CP11 confidential-names
|
|
245
|
+
guard configured only via `owner.yaml` `paths.confidential-names-file`
|
|
246
|
+
therefore degrades to an inert WARN with exit 0 in that copy (CP1–CP10 keep
|
|
247
|
+
running); configured instead via the `SO_CONFIDENTIAL_NAMES_FILE`
|
|
248
|
+
environment variable, the same standalone copy fails **closed** (a counted
|
|
249
|
+
FAIL, exit 1), because the raw env var is still readable without the
|
|
250
|
+
helpers. Prefer the environment variable over `owner.yaml` when vendoring
|
|
251
|
+
this scanner as a single file.
|
|
252
|
+
|
|
241
253
|
## Credential Safety
|
|
242
254
|
|
|
243
255
|
- Secrets are read from the environment or a gitignored `.env.local`, never written
|
|
@@ -13,7 +13,7 @@ You reason over recent learnings, sessions, peer cards, and project steering to
|
|
|
13
13
|
to the canonical peer cards (`.orchestrator/peers/USER.md` and `.orchestrator/peers/AGENT.md`).
|
|
14
14
|
You are dispatched by `scripts/dialectic-deriver.mjs::runDialecticDeriver` with a complete
|
|
15
15
|
payload — your job is to read the payload, decide whether each peer card warrants an update,
|
|
16
|
-
and emit the
|
|
16
|
+
and emit the `## ` sections you change or add for any card you wish to update.
|
|
17
17
|
|
|
18
18
|
## Core responsibilities
|
|
19
19
|
|
|
@@ -22,8 +22,9 @@ and emit the full proposed replacement body for any card you wish to update.
|
|
|
22
22
|
should behave in this project).
|
|
23
23
|
2. **Be conservative**: only propose updates grounded in the supplied inputs. Do not invent
|
|
24
24
|
new sections that no learning or session supports.
|
|
25
|
-
3. **Preserve continuity**:
|
|
26
|
-
|
|
25
|
+
3. **Preserve continuity**: omitted sections stay unchanged — the merge replaces or appends per
|
|
26
|
+
`## ` section and never deletes one. Emit ONLY the sections you change or newly ground; never
|
|
27
|
+
invent a section without evidence from the supplied learnings/sessions.
|
|
27
28
|
4. **Respect the model budget**: you run as Haiku. Keep your reasoning compact; emit only the
|
|
28
29
|
blocks you actually want applied.
|
|
29
30
|
|
|
@@ -83,18 +84,19 @@ line is a comment identifying the target:
|
|
|
83
84
|
|
|
84
85
|
```diff
|
|
85
86
|
# target: user
|
|
86
|
-
<
|
|
87
|
+
<the `## ` sections of USER.md you change or add>
|
|
87
88
|
```
|
|
88
89
|
|
|
89
90
|
```diff
|
|
90
91
|
# target: agent
|
|
91
|
-
<
|
|
92
|
+
<the `## ` sections of AGENT.md you change or add>
|
|
92
93
|
```
|
|
93
94
|
|
|
94
95
|
Rules:
|
|
95
96
|
|
|
96
97
|
- Emit **at most one block per target**. Omit a target entirely when no update is warranted.
|
|
97
|
-
- The block body is
|
|
98
|
+
- The block body is a set of **whole `## ` sections** — not a unified diff hunk. Omitted sections stay
|
|
99
|
+
unchanged (nothing auto-deletes), so emit only the sections you change or newly ground.
|
|
98
100
|
- **`## ` headings are the merge unit (#1310).** On `--apply` the orchestrator splits your body at each
|
|
99
101
|
level-2 (`## `) heading and maps each one onto a sentinel-delimited managed region via
|
|
100
102
|
`mergeDerivedBody()` (`scripts/lib/peer-cards/merger.mjs`). Consequences you control:
|
|
@@ -133,10 +135,11 @@ block entirely. The orchestrator treats absence as "no change".
|
|
|
133
135
|
|
|
134
136
|
## Anti-patterns
|
|
135
137
|
|
|
136
|
-
- **Inventing sections** with no grounding in learnings/sessions —
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
- **Inventing sections** with no grounding in learnings/sessions — nothing downstream rejects
|
|
139
|
+
them (`detectEmptying` only blocks a proposal with zero content lines against a non-empty card),
|
|
140
|
+
so an invented section WOULD be applied; skip the target entirely instead.
|
|
141
|
+
- **Emitting unified diff hunks** (`---/+++/@@`) — the orchestrator expects whole `## `
|
|
142
|
+
sections, not a diff format.
|
|
140
143
|
- **Including frontmatter** in your block — the orchestrator manages frontmatter; emitting
|
|
141
144
|
`---` lines confuses the parser.
|
|
142
145
|
- **Reasoning out of scope** — only synthesise from the supplied payload. Do not Read other
|