session-orchestrator 5.11.0 → 5.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +12 -1
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/agents/explorer.toml +1 -1
- package/.codex-plugin/agents/session-reviewer.toml +2 -2
- package/.codex-plugin/agents/wave-worker.toml +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +8 -0
- package/.orchestrator/policy/public-export.json +30 -0
- package/CHANGELOG.md +180 -3
- package/README.md +126 -117
- package/agents/analyst.md +7 -2
- package/agents/architect-reviewer.md +7 -2
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +9 -2
- package/agents/dialectic-deriver.md +9 -3
- package/agents/docs-writer.md +7 -2
- package/agents/eval-judge.md +7 -1
- package/agents/qa-strategist.md +7 -2
- package/agents/security-reviewer.md +9 -2
- package/agents/session-reviewer.md +18 -2
- package/agents/skill-applied-judge.md +7 -1
- package/agents/test-writer.md +9 -2
- package/agents/ui-developer.md +9 -2
- package/agents/ux-evaluator.md +6 -1
- package/commands/session.md +4 -2
- package/commands/templates-ack.md +89 -18
- package/docs/agent-authoring.md +30 -11
- package/docs/ci-setup.md +38 -2
- package/docs/codex-setup.md +33 -9
- package/docs/components.md +13 -2
- package/docs/cursor-setup.md +3 -3
- package/docs/events-schema.md +5 -4
- package/docs/evidence-candidates.md +63 -0
- package/docs/fleet-checkin-audit.md +7 -4
- package/docs/github-mirror-protection.md +150 -3
- package/docs/install.md +102 -25
- package/docs/instruction-delivery.md +1 -1
- package/docs/migration-v4.md +2 -2
- package/docs/mods.md +118 -0
- package/docs/owner-config-schema.md +1 -0
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/pm-skills-marketplace.md +4 -3
- package/docs/rule-authoring.md +24 -13
- package/docs/scope-collision-guard.md +2 -1
- package/docs/session-config-reference.md +55 -21
- package/docs/session-config-template.md +32 -24
- package/docs/sessions-ledger-repair.md +137 -0
- package/docs/vault-docs-architecture.md +1 -1
- package/hooks/_lib/hook-import-set.json +406 -73
- package/hooks/_lib/hook-root.mjs +301 -0
- package/hooks/_lib/lock-bootstrap.mjs +434 -4
- package/hooks/_lib/orchestrator-ignore.mjs +3 -0
- package/hooks/_lib/subagent-transcript.mjs +2 -2
- package/hooks/_lib/vcs-create-matcher.mjs +13 -17
- package/hooks/cwd-change-restore.mjs +5 -1
- package/hooks/enforce-commands.mjs +4 -3
- package/hooks/enforce-scope.mjs +11 -1
- package/hooks/hooks-codex.json +48 -2
- package/hooks/hooks.json +31 -31
- package/hooks/on-session-end.mjs +38 -15
- package/hooks/on-session-start.mjs +158 -59
- package/hooks/on-stop.mjs +11 -6
- package/hooks/post-bash-issue-budget-refund.mjs +6 -1
- package/hooks/post-bash-write-verify.mjs +8 -4
- package/hooks/post-edit-validate.mjs +45 -52
- package/hooks/post-subagent-discovery-validator.mjs +2 -2
- package/hooks/post-tool-batch-wave-signal.mjs +8 -4
- package/hooks/post-tool-failure-corrective-context.mjs +5 -1
- package/hooks/pre-bash-destructive-guard.mjs +220 -8
- package/hooks/pre-bash-issue-budget.mjs +6 -1
- package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
- package/hooks/pre-bash-staging-fence.mjs +258 -14
- package/hooks/pre-bash-templates-first.mjs +28 -27
- package/hooks/run-node.sh +1 -1
- package/hooks/skill-invocation-telemetry.mjs +15 -8
- package/hooks/subagent-telemetry.mjs +77 -14
- package/hooks/wave-scope-commit-guard.mjs +143 -14
- package/mods/so-hud/.claude-plugin/plugin.json +11 -0
- package/mods/so-hud/README.md +57 -0
- package/mods/so-hud/hooks/beacon-io.mjs +101 -0
- package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
- package/mods/so-hud/hooks/hooks.json +1 -0
- package/mods/so-hud/hooks/logic.ts +182 -0
- package/mods/so-hud/hooks/register.tsx +670 -0
- package/mods/so-hud/hooks/rules.ts +2998 -0
- package/mods/so-hud/tsconfig.json +8 -0
- package/mods/so-hud/types/index.d.ts +36 -0
- package/package.json +4 -3
- package/rules/_index.md +1 -1
- package/rules/always-on/ask-via-tool.md +1 -1
- package/rules/always-on/loop-and-monitor.md +17 -194
- package/rules/always-on/parallel-sessions.md +3 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +1 -1
- package/rules/opt-in-stack/security-web.md +5 -1
- package/scripts/agent-usage-census.mjs +79 -0
- package/scripts/apply-session-learnings.mjs +34 -6
- package/scripts/archive-closed-prds.mjs +21 -5
- package/scripts/autopilot.mjs +11 -5
- package/scripts/backfill-abandoned-sessions.mjs +13 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +61 -6
- package/scripts/backfill-sessions.mjs +16 -2
- package/scripts/check-package-manager.mjs +26 -8
- package/scripts/check-sessions-integrity.mjs +4 -1
- package/scripts/ci/assert-vitest-green.mjs +3 -0
- package/scripts/ci/windows-smoke.mjs +201 -0
- package/scripts/close-precheck.mjs +55 -0
- package/scripts/dialectic-deriver.mjs +20 -12
- package/scripts/emit-event.mjs +2 -2
- package/scripts/emit-session.mjs +239 -20
- package/scripts/eval-session.mjs +17 -3
- package/scripts/export-hw-learnings.mjs +29 -2
- package/scripts/fleet-checkin-audit.mjs +8 -4
- package/scripts/fleet-checkin.mjs +73 -3
- package/scripts/gc-stale-worktrees.mjs +6 -1
- package/scripts/github-export.mjs +128 -0
- package/scripts/github-protection-audit.mjs +3 -1
- package/scripts/handover-queue-check.mjs +209 -0
- package/scripts/harness-audit.mjs +6 -4
- package/scripts/lib/agent-frontmatter.mjs +57 -10
- package/scripts/lib/agent-status.mjs +125 -7
- package/scripts/lib/agent-usage-census.mjs +394 -0
- package/scripts/lib/auto-dialectic.mjs +17 -10
- package/scripts/lib/auto-dream.mjs +22 -14
- package/scripts/lib/autopilot/loop.mjs +3 -1
- package/scripts/lib/autopilot/recent-runs.mjs +4 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
- package/scripts/lib/autopilot/telemetry.mjs +5 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/baseline-archetypes.mjs +10 -2
- package/scripts/lib/blocked-commands-policy.mjs +4 -3
- package/scripts/lib/build-live-signals.mjs +13 -9
- package/scripts/lib/ci-status-banner.mjs +329 -24
- package/scripts/lib/codex/plugin-contract.mjs +8 -0
- package/scripts/lib/cold-start-detector.mjs +5 -2
- package/scripts/lib/command-blocker.mjs +603 -43
- package/scripts/lib/common.mjs +3 -2
- package/scripts/lib/config/cross-repo.mjs +74 -7
- package/scripts/lib/config/host-paths.mjs +6 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config/section-extractor.mjs +65 -11
- package/scripts/lib/config/template-keys.mjs +153 -0
- package/scripts/lib/config/vault-integration.mjs +1 -0
- package/scripts/lib/config.mjs +3 -3
- package/scripts/lib/coordinator-snapshot.mjs +1 -1
- package/scripts/lib/discovery/triage-state.mjs +2 -1
- package/scripts/lib/dispatcher/rank.mjs +5 -7
- package/scripts/lib/ecosystem-health.mjs +2 -2
- package/scripts/lib/eval/engine.mjs +3 -2
- package/scripts/lib/eval/session-resolve.mjs +6 -2
- package/scripts/lib/eval/sink.mjs +4 -3
- package/scripts/lib/events-retention-banner.mjs +3 -2
- package/scripts/lib/events-schema.mjs +10 -0
- package/scripts/lib/events.mjs +23 -12
- package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
- package/scripts/lib/fleet-protocol.mjs +21 -0
- package/scripts/lib/gates/gate-full.mjs +172 -11
- package/scripts/lib/gates/gate-helpers.mjs +236 -23
- package/scripts/lib/git-config-drift.mjs +141 -7
- package/scripts/lib/github-export.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
- package/scripts/lib/handover-queue-marks.mjs +159 -0
- package/scripts/lib/hardware-pattern-detector.mjs +103 -13
- package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
- package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
- package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
- package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
- package/scripts/lib/host-identity.mjs +9 -5
- package/scripts/lib/http-request.mjs +64 -0
- package/scripts/lib/instruction-budget-guard.mjs +30 -22
- package/scripts/lib/io.mjs +6 -3
- package/scripts/lib/issue-budget-reconcile.mjs +3 -2
- package/scripts/lib/issue-budget.mjs +36 -7
- package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
- package/scripts/lib/learnings/io.mjs +289 -15
- package/scripts/lib/learnings/select.mjs +53 -4
- package/scripts/lib/learnings/store-role.mjs +209 -0
- package/scripts/lib/learnings/surface.mjs +46 -7
- package/scripts/lib/ledger-path.mjs +113 -0
- package/scripts/lib/maintenance-due-banner.mjs +63 -12
- package/scripts/lib/memory-banner.mjs +5 -6
- package/scripts/lib/memory-proposals/collector.mjs +5 -2
- package/scripts/lib/memory-proposals/sink.mjs +7 -4
- package/scripts/lib/memory-proposals/store.mjs +22 -5
- package/scripts/lib/mode-selector-accuracy.mjs +2 -1
- package/scripts/lib/no-follow-open.mjs +57 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
- package/scripts/lib/own-lock-root.mjs +95 -0
- package/scripts/lib/owner-interview.mjs +2 -1
- package/scripts/lib/owner-yaml.mjs +70 -7
- package/scripts/lib/platform.mjs +161 -9
- package/scripts/lib/plugin-update-banner.mjs +15 -11
- package/scripts/lib/pre-push-local-partial.mjs +117 -0
- package/scripts/lib/pre-push-remote-gate.mjs +151 -25
- package/scripts/lib/process-group.mjs +270 -34
- package/scripts/lib/project-hygiene.mjs +28 -0
- package/scripts/lib/quality-gate.mjs +33 -9
- package/scripts/lib/reconcile/backlog.mjs +29 -15
- package/scripts/lib/reconcile/engine.mjs +26 -2
- package/scripts/lib/reconcile/idempotency.mjs +65 -3
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
- package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
- package/scripts/lib/resource-probe/evaluate.mjs +32 -9
- package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
- package/scripts/lib/rule-loader.mjs +20 -10
- package/scripts/lib/scope-baseline.mjs +123 -8
- package/scripts/lib/scope-echo.mjs +5 -2
- package/scripts/lib/scope-gate.mjs +55 -20
- package/scripts/lib/session-close-backfill.mjs +75 -7
- package/scripts/lib/session-end/close-precheck.mjs +151 -0
- package/scripts/lib/session-end/phase-skip.mjs +8 -7
- package/scripts/lib/session-end/tail-runner.mjs +7 -2
- package/scripts/lib/session-end/vault-orphans.mjs +205 -0
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-invocation.mjs +14 -3
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +85 -12
- package/scripts/lib/session-schema/constants.mjs +9 -0
- package/scripts/lib/session-schema/filters.mjs +56 -1
- package/scripts/lib/session-schema/validator.mjs +15 -5
- package/scripts/lib/session-start-probes.mjs +149 -22
- package/scripts/lib/session-token-rollup.mjs +2 -1
- package/scripts/lib/session-work-trees.mjs +166 -0
- package/scripts/lib/sessions-canonical.mjs +61 -3
- package/scripts/lib/sessions-integrity-banner.mjs +17 -39
- package/scripts/lib/sessions-staleness-banner.mjs +8 -5
- package/scripts/lib/sh-quote.mjs +1 -0
- package/scripts/lib/skill-evidence-window.mjs +5 -1
- package/scripts/lib/skill-health/join.mjs +10 -9
- package/scripts/lib/skill-invocations-schema.mjs +13 -0
- package/scripts/lib/skill-judgments-schema.mjs +6 -2
- package/scripts/lib/snapshot-probe.mjs +414 -0
- package/scripts/lib/so-hud/collect.mjs +174 -0
- package/scripts/lib/so-hud/runtime.mjs +87 -0
- package/scripts/lib/spiral-carryover.mjs +8 -1
- package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
- package/scripts/lib/state-md/idle-reset.mjs +248 -0
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md/recommendations.mjs +26 -4
- package/scripts/lib/state-md-peer-guard.mjs +2 -0
- package/scripts/lib/state-md.mjs +31 -8
- package/scripts/lib/sunset/walker.mjs +4 -3
- package/scripts/lib/telemetry/consent.mjs +4 -2
- package/scripts/lib/telemetry/queue.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +45 -9
- package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
- package/scripts/lib/tmux-layout/layouts.mjs +20 -3
- package/scripts/lib/ux-grill/paths.mjs +5 -1
- package/scripts/lib/validate/check-agents.mjs +37 -6
- package/scripts/lib/validate/check-component-paths.mjs +25 -1
- package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
- package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
- package/scripts/lib/validate/check-json-files.mjs +3 -1
- package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
- package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
- package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
- package/scripts/lib/validate/check-unwired-features.mjs +4 -2
- package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
- package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
- package/scripts/lib/validate/confidential-names.mjs +3 -1
- package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
- package/scripts/lib/vault-mirror/namespace.mjs +22 -9
- package/scripts/lib/vault-mirror/process.mjs +197 -65
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +8 -7
- package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
- package/scripts/lib/vault-mirror/retention.mjs +2 -2
- package/scripts/lib/vault-mirror/utils.mjs +106 -13
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +34 -5
- package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
- package/scripts/lib/win-process-tree.mjs +137 -0
- package/scripts/lib/workspace.mjs +1 -1
- package/scripts/lib/worktree/constants.mjs +1 -1
- package/scripts/lib/worktree/lifecycle.mjs +1 -1
- package/scripts/lib/worktree/listing.mjs +1 -1
- package/scripts/lib/zx.mjs +50 -0
- package/scripts/mcp-server.mjs +16 -7
- package/scripts/mcp-server.sh +13 -83
- package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
- package/scripts/memory-propose.mjs +10 -3
- package/scripts/migrate-cold-start-seed.mjs +7 -0
- package/scripts/migrate-legacy-learnings.sh +22 -0
- package/scripts/migrate-sessions-jsonl.mjs +16 -2
- package/scripts/migrate-vault-paths.mjs +29 -13
- package/scripts/parse-config.mjs +71 -14
- package/scripts/print-learnings-index.mjs +25 -2
- package/scripts/release-session-lock.mjs +94 -7
- package/scripts/release.mjs +451 -24
- package/scripts/relocate-vault-corpus.mjs +11 -7
- package/scripts/repair-invalid-sessions.mjs +68 -3
- package/scripts/resolve-session-invocation.mjs +57 -8
- package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
- package/scripts/run-quality-gate.mjs +33 -7
- package/scripts/self-update.mjs +56 -7
- package/scripts/session-commit-files.mjs +133 -0
- package/scripts/site-numbers.mjs +52 -9
- package/scripts/snapshot-probe.mjs +156 -0
- package/scripts/so-hud.mjs +68 -0
- package/scripts/state-md-idle-reset.mjs +82 -0
- package/scripts/sweep-expired-learnings.mjs +54 -6
- package/scripts/sweep-expired-rules.mjs +4 -1
- package/scripts/validate-plugin.mjs +22 -0
- package/scripts/vault-integration-watcher.mjs +54 -6
- package/scripts/vault-mirror.mjs +9 -0
- package/scripts/vault-orphans.mjs +171 -0
- package/skills/_shared/fleet-protocol.md +166 -3
- package/skills/_shared/instruction-file-resolution.md +1 -1
- package/skills/_shared/monitor-patterns.md +44 -25
- package/skills/_shared/platform-tools.md +7 -0
- package/skills/_shared/state-ownership.md +5 -4
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/autopilot/SKILL.md +3 -2
- package/skills/bootstrap/SKILL.md +3 -1
- package/skills/bootstrap/_shared-template.md +4 -4
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +1 -1
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
- package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
- package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
- package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
- package/skills/claude-md-drift-check/SKILL.md +3 -3
- package/skills/claude-md-drift-check/checker.mjs +61 -101
- package/skills/close/SKILL.md +20 -7
- package/skills/discovery/probes/docs-staleness.mjs +7 -5
- package/skills/discovery/probes/frontend-slop.mjs +7 -3
- package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
- package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
- package/skills/discovery/probes/vault-staleness.mjs +5 -3
- package/skills/discovery/probes-session.md +3 -0
- package/skills/discovery/probes-vault.md +2 -0
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eval/SKILL.md +3 -1
- package/skills/evolve/SKILL.md +2 -0
- package/skills/evolve/references/evolve-analyze-mode.md +3 -3
- package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
- package/skills/gitlab-ops/SKILL.md +7 -6
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/go/SKILL.md +13 -1
- package/skills/npm-publish/SKILL.md +5 -2
- package/skills/plan/SKILL.md +23 -10
- package/skills/plan/mode-feature.md +13 -8
- package/skills/plan/mode-new.md +6 -14
- package/skills/plan/prd-feature-template.md +19 -0
- package/skills/plan/prd-full-template.md +12 -5
- package/skills/plan/prd-premortem-panel.md +48 -0
- package/skills/plan/prd-reviewer-prompt.md +11 -3
- package/skills/plan/soul.md +1 -1
- package/skills/portfolio/SKILL.md +5 -6
- package/skills/quality-gates/SKILL.md +2 -4
- package/skills/reconcile/SKILL.md +27 -4
- package/skills/release/SKILL.md +16 -8
- package/skills/session-end/SKILL.md +97 -13
- package/skills/session-end/phase-3-2-docs-verification.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +11 -7
- package/skills/session-end/phase-3-7a-recommendations.md +9 -2
- package/skills/session-end/plan-verification.md +16 -1
- package/skills/session-end/references/phase-3-7e-vault-orphans.md +63 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +25 -13
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +8 -0
- package/skills/session-end/session-metrics-write.md +7 -4
- package/skills/session-plan/SKILL.md +5 -3
- package/skills/session-start/SKILL.md +10 -4
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-7-5-mode-selector.md +3 -1
- package/skills/session-start/phase-8-5-express-path.md +4 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
- package/skills/session-start/references/phase-1-5-session-continuity.md +13 -5
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
- package/skills/tmux-layout/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +3 -2
- package/skills/wave-executor/SKILL.md +5 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
- package/skills/wave-executor/references/wave-loop-review.md +13 -5
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
- package/AGENTS.md +0 -207
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verdict-ledger.mjs — append-only record of reconcile verdicts and runs (#1710).
|
|
3
|
+
*
|
|
4
|
+
* The idempotency sidecar (`.orchestrator/runtime/reconcile-candidates.jsonl`,
|
|
5
|
+
* `./idempotency.mjs`) is gitignored and resolved per checkout, so a fresh git
|
|
6
|
+
* worktree or another host starts without it: an operator's explicit
|
|
7
|
+
* `rejected` verdict was lost there and `/reconcile` re-proposed the declined
|
|
8
|
+
* rule, while the session-start nudge reported "no reconcile run on record".
|
|
9
|
+
*
|
|
10
|
+
* This ledger lives at `.orchestrator/metrics/reconcile-verdicts.jsonl` under
|
|
11
|
+
* `resolveEventsLedgerRoot(repoRoot)` — a linked worktree maps to its MAIN
|
|
12
|
+
* checkout — beside `learnings.jsonl`, so it shares learnings' tracking status
|
|
13
|
+
* in every repo (tracked + `merge=union` in the vault, host-local here). It is
|
|
14
|
+
* NEVER rewritten, only appended. Two record shapes, one JSON object per line:
|
|
15
|
+
* { kind: 'verdict', learning_key, outcome, processed_at, recorded_at }
|
|
16
|
+
* { kind: 'run', at, learnings } (`learnings`: count the run loaded; optional on older lines)
|
|
17
|
+
*
|
|
18
|
+
* Verdict lines carry DECISIONS only (see {@link BRANCH_SCOPED_OUTCOMES}): an
|
|
19
|
+
* outcome that describes one branch's tree (`written`, `already-on-disk`, or no
|
|
20
|
+
* outcome at all) already has a cross-tree witness in the tracked rule file's
|
|
21
|
+
* provenance marker, and mirroring it from a worktree whose branch is later
|
|
22
|
+
* abandoned would make the learning permanently non-re-proposable in main.
|
|
23
|
+
* Run recency (`lastRunAt`) comes from `run` lines only — a verdict is not a run.
|
|
24
|
+
*
|
|
25
|
+
* Writes happen ONLY for an explicit non-empty `repoRoot` — never a
|
|
26
|
+
* `process.cwd()` fallback, so a test without a root can never append to this
|
|
27
|
+
* repo's real ledger. Readers without a root return the empty result.
|
|
28
|
+
*
|
|
29
|
+
* Ceiling (BV-004): no lock. Concurrent worktree sessions append through
|
|
30
|
+
* O_APPEND with small lines (one `appendFileSync` per call), which keeps lines
|
|
31
|
+
* whole in practice; the sidecar stays the checkout-local work queue and this
|
|
32
|
+
* file only carries terminality and run recency. Revisit trigger: an
|
|
33
|
+
* interleaved or garbled ledger line observed (`skipped > 0` on a ledger no
|
|
34
|
+
* human edited).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
38
|
+
import { dirname, join } from 'node:path';
|
|
39
|
+
|
|
40
|
+
import { resolveEventsLedgerRoot } from '../platform.mjs';
|
|
41
|
+
|
|
42
|
+
/** Repo-relative location of the verdict ledger (resolved against the MAIN checkout). */
|
|
43
|
+
export const VERDICT_LEDGER_PATH = '.orchestrator/metrics/reconcile-verdicts.jsonl';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Terminal outcomes that describe ONE branch's tree, not a decision, and are
|
|
47
|
+
* therefore never mirrored into the ledger: `written` (writer.mjs persisted the
|
|
48
|
+
* rule file), `already-on-disk` (engine.mjs found its provenance marker) and the
|
|
49
|
+
* empty string (a record without `outcome` — pre-#1042, read as `written`). Every
|
|
50
|
+
* other outcome is a decision (today `rejected`, `expired-swept`).
|
|
51
|
+
*/
|
|
52
|
+
export const BRANCH_SCOPED_OUTCOMES = Object.freeze(new Set(['', 'written', 'already-on-disk']));
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {unknown} outcome - a sidecar record's `outcome` (absent/null ⇒ branch-scoped).
|
|
56
|
+
* @returns {boolean} true iff the outcome is a decision the ledger carries.
|
|
57
|
+
*/
|
|
58
|
+
export function isDecisionOutcome(outcome) {
|
|
59
|
+
return typeof outcome === 'string' && !BRANCH_SCOPED_OUTCOMES.has(outcome);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {unknown} repoRoot
|
|
64
|
+
* @returns {string|null} absolute ledger path, or null without an explicit root
|
|
65
|
+
*/
|
|
66
|
+
function ledgerPathFor(repoRoot) {
|
|
67
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0) return null;
|
|
68
|
+
return join(resolveEventsLedgerRoot(repoRoot), VERDICT_LEDGER_PATH);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {Object} VerdictLedger
|
|
73
|
+
* @property {Map<string, string>} terminalKeys - learning_key → outcome of its first recorded verdict.
|
|
74
|
+
* @property {string|null} lastRunAt - max over run records' `at`, ISO (a verdict is not a run).
|
|
75
|
+
* @property {number|null} lastRunLearnings - `learnings` of the run record with the latest `at`;
|
|
76
|
+
* null when that record (or every run record) predates the field or carries no valid integer.
|
|
77
|
+
* @property {number} skipped - lines that were not a valid verdict/run record (counted, never silent).
|
|
78
|
+
* @property {boolean} present - the file was read.
|
|
79
|
+
* @property {boolean} readError - the file exists (or may) but could not be read (non-ENOENT).
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/** @returns {VerdictLedger} */
|
|
83
|
+
function emptyLedger() {
|
|
84
|
+
return {
|
|
85
|
+
terminalKeys: new Map(),
|
|
86
|
+
lastRunAt: null,
|
|
87
|
+
lastRunLearnings: null,
|
|
88
|
+
skipped: 0,
|
|
89
|
+
present: false,
|
|
90
|
+
readError: false,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `<path>\0<skipped>` pairs already warned about in this process. */
|
|
95
|
+
const warnedSkipped = new Set();
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Load the verdict ledger for `repoRoot`. A missing file (ENOENT) is the empty
|
|
99
|
+
* result, silently; any other read failure is the empty result plus ONE stderr
|
|
100
|
+
* WARN with `readError: true` (same split as `defaultLoadLearnings`). Blank lines
|
|
101
|
+
* are ignored; every other line that is not a valid verdict/run record is
|
|
102
|
+
* counted in `skipped`, and a non-zero count emits ONE stderr WARN naming the
|
|
103
|
+
* path and the count — once per path and count per process (no production
|
|
104
|
+
* caller reads `skipped`). Never throws.
|
|
105
|
+
*
|
|
106
|
+
* @param {string|undefined} repoRoot - explicit root; absent → empty result.
|
|
107
|
+
* @returns {VerdictLedger}
|
|
108
|
+
*/
|
|
109
|
+
export function loadVerdictLedger(repoRoot) {
|
|
110
|
+
const result = emptyLedger();
|
|
111
|
+
const absPath = ledgerPathFor(repoRoot);
|
|
112
|
+
if (absPath === null) return result;
|
|
113
|
+
|
|
114
|
+
let raw;
|
|
115
|
+
try {
|
|
116
|
+
raw = readFileSync(absPath, 'utf8');
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (!err || err.code !== 'ENOENT') {
|
|
119
|
+
result.readError = true;
|
|
120
|
+
process.stderr.write(
|
|
121
|
+
`⚠ loadVerdictLedger: cannot read ${absPath} ` +
|
|
122
|
+
`(${err?.code ?? '?'}: ${err?.message ?? String(err)}) — treating as EMPTY\n`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
result.present = true;
|
|
128
|
+
|
|
129
|
+
let maxMs = -Infinity;
|
|
130
|
+
let latestRunMs = -Infinity;
|
|
131
|
+
for (const line of raw.split('\n')) {
|
|
132
|
+
const trimmed = line.trim();
|
|
133
|
+
if (trimmed.length === 0) continue;
|
|
134
|
+
let rec;
|
|
135
|
+
try {
|
|
136
|
+
rec = JSON.parse(trimmed);
|
|
137
|
+
} catch {
|
|
138
|
+
result.skipped += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (rec && rec.kind === 'verdict' && typeof rec.learning_key === 'string' && rec.learning_key.length > 0) {
|
|
142
|
+
if (!result.terminalKeys.has(rec.learning_key)) {
|
|
143
|
+
result.terminalKeys.set(rec.learning_key, typeof rec.outcome === 'string' ? rec.outcome : '');
|
|
144
|
+
}
|
|
145
|
+
} else if (rec && rec.kind === 'run' && typeof rec.at === 'string' && Number.isFinite(Date.parse(rec.at))) {
|
|
146
|
+
const t = Date.parse(rec.at);
|
|
147
|
+
if (t > maxMs) maxMs = t;
|
|
148
|
+
if (t >= latestRunMs) {
|
|
149
|
+
latestRunMs = t;
|
|
150
|
+
result.lastRunLearnings =
|
|
151
|
+
Number.isSafeInteger(rec.learnings) && rec.learnings >= 0 ? rec.learnings : null;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
result.skipped += 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (maxMs > -Infinity) result.lastRunAt = new Date(maxMs).toISOString();
|
|
158
|
+
// Once per path and count per process: one /reconcile reads the ledger once per
|
|
159
|
+
// stamp, and the same bad line must not print N+2 identical warnings.
|
|
160
|
+
const warnKey = `${absPath}\0${result.skipped}`;
|
|
161
|
+
if (result.skipped > 0 && !warnedSkipped.has(warnKey)) {
|
|
162
|
+
warnedSkipped.add(warnKey);
|
|
163
|
+
process.stderr.write(
|
|
164
|
+
`⚠ loadVerdictLedger: ${result.skipped} line(s) in ${absPath} are not a valid verdict/run record — ignored\n`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Append all `records` as JSONL in ONE write (mkdir -p first). Never throws.
|
|
172
|
+
* @param {string|undefined} repoRoot
|
|
173
|
+
* @param {Array<Record<string, unknown>>} records
|
|
174
|
+
* @returns {{ ok: boolean, appended: number, error?: string }}
|
|
175
|
+
*/
|
|
176
|
+
function appendRecords(repoRoot, records) {
|
|
177
|
+
const absPath = ledgerPathFor(repoRoot);
|
|
178
|
+
if (absPath === null) return { ok: false, appended: 0, error: 'no repoRoot' };
|
|
179
|
+
if (records.length === 0) return { ok: true, appended: 0 };
|
|
180
|
+
try {
|
|
181
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
182
|
+
appendFileSync(absPath, records.map((r) => JSON.stringify(r)).join('\n') + '\n', 'utf8');
|
|
183
|
+
return { ok: true, appended: records.length };
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return { ok: false, appended: 0, error: err && err.message ? err.message : String(err) };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Append one verdict line per terminal candidate (`processed_at` set). The
|
|
191
|
+
* caller decides which records are new; this function does not dedupe.
|
|
192
|
+
*
|
|
193
|
+
* @param {string|undefined} repoRoot - explicit root; absent → nothing written.
|
|
194
|
+
* @param {Array<{ learning_key?: unknown, outcome?: unknown, processed_at?: unknown }>} candidates
|
|
195
|
+
* @param {string} [recordedAt] - ISO; defaults to now.
|
|
196
|
+
* @returns {{ ok: boolean, appended: number, error?: string }}
|
|
197
|
+
*/
|
|
198
|
+
export function appendVerdicts(repoRoot, candidates, recordedAt) {
|
|
199
|
+
const at = typeof recordedAt === 'string' && recordedAt.length > 0 ? recordedAt : new Date().toISOString();
|
|
200
|
+
const records = (Array.isArray(candidates) ? candidates : [])
|
|
201
|
+
.filter((c) => c && typeof c.learning_key === 'string' && c.learning_key.length > 0 && typeof c.processed_at === 'string')
|
|
202
|
+
.map((c) => ({
|
|
203
|
+
kind: 'verdict',
|
|
204
|
+
learning_key: c.learning_key,
|
|
205
|
+
outcome: typeof c.outcome === 'string' ? c.outcome : '',
|
|
206
|
+
processed_at: c.processed_at,
|
|
207
|
+
recorded_at: at,
|
|
208
|
+
}));
|
|
209
|
+
return appendRecords(repoRoot, records);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Append one run record.
|
|
214
|
+
* @param {string|undefined} repoRoot - explicit root; absent → nothing written.
|
|
215
|
+
* @param {string} at - ISO timestamp of the run.
|
|
216
|
+
* @param {number} [learnings] - how many learnings the run loaded; omitted from the
|
|
217
|
+
* record unless a non-negative integer (the nudge's "new since last run" baseline).
|
|
218
|
+
* @returns {{ ok: boolean, appended: number, error?: string }}
|
|
219
|
+
*/
|
|
220
|
+
export function appendRun(repoRoot, at, learnings) {
|
|
221
|
+
const rec = { kind: 'run', at };
|
|
222
|
+
if (Number.isSafeInteger(learnings) && learnings >= 0) rec.learnings = learnings;
|
|
223
|
+
return appendRecords(repoRoot, [rec]);
|
|
224
|
+
}
|
|
@@ -35,8 +35,18 @@
|
|
|
35
35
|
* high-water mark across all runs to date), used for the "new learnings
|
|
36
36
|
* since last run" delta heuristic.
|
|
37
37
|
* There is no separate `reconcile`-scoped event in `.orchestrator/metrics/events.jsonl`
|
|
38
|
-
* — the idempotency sidecar
|
|
39
|
-
* that directly rather than re-deriving a signal from the generic
|
|
38
|
+
* — the idempotency sidecar is the durable checkout-local run record, so this
|
|
39
|
+
* module reads that directly rather than re-deriving a signal from the generic
|
|
40
|
+
* event log. Since #1710 a second, checkout-independent run record exists: the
|
|
41
|
+
* append-only verdict ledger (`./reconcile/verdict-ledger.mjs`, main checkout
|
|
42
|
+
* via `resolveEventsLedgerRoot`). Its `run` lines carry `at` and the number of
|
|
43
|
+
* learnings that run loaded; the ledger's run recency comes from those `run`
|
|
44
|
+
* lines ONLY (a verdict line is not a run — a ledger holding only hand stamps or
|
|
45
|
+
* `sweep-expired-rules --apply` verdicts reports no run, so rule (a) says so
|
|
46
|
+
* honestly instead of rule (b) counting every learning as new). `lastRunAt` is
|
|
47
|
+
* the later of both sources and the delta baseline is the larger of the sidecar
|
|
48
|
+
* row count and that learnings count, so a fresh worktree without the sidecar
|
|
49
|
+
* still sees the run.
|
|
40
50
|
*
|
|
41
51
|
* Reuses the existing learnings-corpus helpers rather than re-implementing
|
|
42
52
|
* filter logic:
|
|
@@ -72,12 +82,13 @@
|
|
|
72
82
|
* - Issue #723.
|
|
73
83
|
*/
|
|
74
84
|
|
|
75
|
-
import { join } from 'node:path';
|
|
76
85
|
|
|
86
|
+
import { resolveLedgerPath } from './ledger-path.mjs';
|
|
77
87
|
import { readLearnings } from './learnings/io.mjs';
|
|
78
88
|
import { surfaceTopN } from './learnings/surface.mjs';
|
|
79
89
|
import { countReconcileBacklog } from './reconcile/backlog.mjs';
|
|
80
90
|
import { loadCandidates } from './reconcile/idempotency.mjs';
|
|
91
|
+
import { loadVerdictLedger } from './reconcile/verdict-ledger.mjs';
|
|
81
92
|
import { readConfigFile } from './config/io.mjs';
|
|
82
93
|
import { _parseReconcile } from './config/reconcile.mjs';
|
|
83
94
|
|
|
@@ -169,6 +180,17 @@ function _lastRunAt(candidates) {
|
|
|
169
180
|
return Number.isFinite(maxMs) && maxMs > -Infinity ? new Date(maxMs).toISOString() : null;
|
|
170
181
|
}
|
|
171
182
|
|
|
183
|
+
/**
|
|
184
|
+
* @param {string|null} a - ISO timestamp or null
|
|
185
|
+
* @param {string|null} b - ISO timestamp or null
|
|
186
|
+
* @returns {string|null} the later of the two, or null when both are null
|
|
187
|
+
*/
|
|
188
|
+
function _laterIso(a, b) {
|
|
189
|
+
if (a === null) return b;
|
|
190
|
+
if (b === null) return a;
|
|
191
|
+
return Date.parse(b) > Date.parse(a) ? b : a;
|
|
192
|
+
}
|
|
193
|
+
|
|
172
194
|
/**
|
|
173
195
|
* Pure computation — always returns the full shape (never null, never throws).
|
|
174
196
|
* Touches disk only via the injected/default learnings + candidates readers,
|
|
@@ -215,7 +237,7 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
215
237
|
};
|
|
216
238
|
|
|
217
239
|
const repoRoot = typeof opts.repoRoot === 'string' && opts.repoRoot.length > 0 ? opts.repoRoot : process.cwd();
|
|
218
|
-
const learningsPath =
|
|
240
|
+
const learningsPath = resolveLedgerPath(LEARNINGS_PATH, repoRoot);
|
|
219
241
|
|
|
220
242
|
/** @type {object[]} */
|
|
221
243
|
let entries;
|
|
@@ -282,6 +304,9 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
282
304
|
// sentence (HR-106). `repoRoot` stays required — the sidecar and the
|
|
283
305
|
// `.claude/rules/` provenance scan are resolved from it.
|
|
284
306
|
// Never throws (all-zero on failure).
|
|
307
|
+
// #1710 — the append-only verdict ledger (main checkout, outlives the
|
|
308
|
+
// checkout-local sidecar) is read ONCE and handed to the count as well.
|
|
309
|
+
const ledger = loadVerdictLedger(repoRoot);
|
|
285
310
|
const {
|
|
286
311
|
eligible: eligibleCount,
|
|
287
312
|
alreadyMaterialized,
|
|
@@ -290,12 +315,21 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
290
315
|
repoRoot,
|
|
291
316
|
learnings: entries,
|
|
292
317
|
...(candidatesRead ? { existingCandidates: candidates } : {}),
|
|
318
|
+
ledgerKeys: ledger.terminalKeys,
|
|
293
319
|
now: opts.now,
|
|
294
320
|
minInsightChars,
|
|
295
321
|
});
|
|
296
322
|
|
|
297
|
-
|
|
298
|
-
|
|
323
|
+
// The later of the sidecar's and the ledger's run evidence: a fresh worktree
|
|
324
|
+
// has no sidecar, yet the run on record in the ledger is still a run.
|
|
325
|
+
const lastRunAt = _laterIso(_lastRunAt(candidates), ledger.lastRunAt);
|
|
326
|
+
// The ledger's run record carries the learnings count that run saw, so a
|
|
327
|
+
// fresh worktree (no sidecar) does not report every learning as new. The max
|
|
328
|
+
// keeps the main checkout's delta unchanged — never larger than before.
|
|
329
|
+
const lastRunCandidateCount = Math.max(
|
|
330
|
+
Array.isArray(candidates) ? candidates.length : 0,
|
|
331
|
+
ledger.lastRunLearnings ?? 0,
|
|
332
|
+
);
|
|
299
333
|
const delta = entries.length - lastRunCandidateCount;
|
|
300
334
|
const quarantined = typeof skippedCandidates === 'number' ? skippedCandidates : 0;
|
|
301
335
|
|
|
@@ -26,8 +26,16 @@
|
|
|
26
26
|
* number"), one noting "five consecutive sessions wrongly capped at 2".
|
|
27
27
|
* 2. `concurrent-sessions-warn` is named for SESSIONS but was compared against
|
|
28
28
|
* `claude_processes_count`. Measured ratio processes:sessions = 6.0. That
|
|
29
|
-
* threshold fired on 93.6% of starts; against the real peer count it
|
|
30
|
-
* on 4.2
|
|
29
|
+
* threshold fired on 93.6% of starts; against the real peer count it fired
|
|
30
|
+
* on 4.2% (2026-08-21). That rate did not hold: re-measured 2026-10-17 over
|
|
31
|
+
* starts since 2026-08-24, `peer_count >= 5` fires on 880/1305 = 67.4%
|
|
32
|
+
* (M4) and 255/991 = 25.7% (M5) — #1650. The session-start banner's WARN
|
|
33
|
+
* icon was re-aimed to a start wave (`hooks/on-session-start.mjs`,
|
|
34
|
+
* START_WAVE_*). #1654 finished the re-aim here: a state present on two
|
|
35
|
+
* thirds of M4 starts is a standing condition, and HR-104 forbids letting
|
|
36
|
+
* one count as a second opinion. concurrencySignal() below therefore still
|
|
37
|
+
* REPORTS the peer count (an `info:` reason) but no longer contributes a
|
|
38
|
+
* soft signal — neither the registry branch nor the process fallback.
|
|
31
39
|
* 3. Any ONE soft signal capped agents-per-wave at 2 outright, so the union of
|
|
32
40
|
* three noisy axes decided wave size on its noisiest member.
|
|
33
41
|
*
|
|
@@ -51,6 +59,8 @@
|
|
|
51
59
|
* when present. `claude_processes_count` survives only as a fallback, scaled
|
|
52
60
|
* by the measured PROCESSES_PER_SESSION factor so the configured threshold
|
|
53
61
|
* keeps meaning "sessions" on hosts where the registry is unavailable.
|
|
62
|
+
* Since #1654 the concurrency axis is REPORT-ONLY: it never enters
|
|
63
|
+
* `signals.soft`, so it can neither cap alone nor pair into a cap.
|
|
54
64
|
* - TWO-SIGNAL RULE: a single soft signal is informational — reported in
|
|
55
65
|
* `reasons`, capping nothing. A cap needs two INDEPENDENT soft signals
|
|
56
66
|
* agreeing, or one hard signal. Simulated on the same 1477-sample corpus:
|
|
@@ -219,12 +229,24 @@ function memorySignal(snapshot, ramMin, ramCrit) {
|
|
|
219
229
|
}
|
|
220
230
|
|
|
221
231
|
/**
|
|
222
|
-
* Resolve the concurrency
|
|
232
|
+
* Resolve the concurrency report, preferring the live session registry count
|
|
223
233
|
* over the raw process count. Returns `null` when neither is available.
|
|
224
234
|
*
|
|
235
|
+
* REPORT-ONLY since #1654: `soft` is always `false`. The peer count is a
|
|
236
|
+
* standing condition, not live load — `peer_count >= 5` held on 880/1305 =
|
|
237
|
+
* 67.4% of M4 and 255/991 = 25.7% of M5 starts since 2026-08-24 (#1650,
|
|
238
|
+
* re-measured 2026-10-17: 78/115 = 67.8% M4). HR-104: a usually-present signal
|
|
239
|
+
* reports and NEVER counts as a second opinion, so letting it pair with one
|
|
240
|
+
* real soft signal would restore the one-signal cap under a two-signal name
|
|
241
|
+
* (the same defect zombies had). The rare, decision-changing concurrency signal
|
|
242
|
+
* is the start wave in `hooks/on-session-start.mjs` (START_WAVE_*), which is
|
|
243
|
+
* not a snapshot field. The process-count fallback measures the same standing
|
|
244
|
+
* state and is report-only for the same reason.
|
|
245
|
+
*
|
|
225
246
|
* @param {object} snapshot
|
|
226
247
|
* @param {number} concWarn — configured threshold, denominated in SESSIONS
|
|
227
|
-
* @returns {{soft:
|
|
248
|
+
* @returns {{soft: false, reason: string|null}|null} — `reason` set when the
|
|
249
|
+
* threshold is reached (caller reports it as `info:`); never a soft signal.
|
|
228
250
|
*/
|
|
229
251
|
function concurrencySignal(snapshot, concWarn) {
|
|
230
252
|
const { peer_sessions_count, claude_processes_count } = snapshot;
|
|
@@ -234,7 +256,7 @@ function concurrencySignal(snapshot, concWarn) {
|
|
|
234
256
|
if (hasPeers) {
|
|
235
257
|
if (peer_sessions_count >= concWarn) {
|
|
236
258
|
return {
|
|
237
|
-
soft:
|
|
259
|
+
soft: false,
|
|
238
260
|
reason: `${peer_sessions_count} peer session(s) live in the registry (threshold: ${concWarn}) — consider sequencing this session after others finish.`,
|
|
239
261
|
};
|
|
240
262
|
}
|
|
@@ -252,7 +274,7 @@ function concurrencySignal(snapshot, concWarn) {
|
|
|
252
274
|
const procThreshold = concWarn * PROCESSES_PER_SESSION;
|
|
253
275
|
if (claude_processes_count >= procThreshold) {
|
|
254
276
|
return {
|
|
255
|
-
soft:
|
|
277
|
+
soft: false,
|
|
256
278
|
reason: `${claude_processes_count} Claude processes running (fallback threshold ${procThreshold} = ${concWarn} sessions × ${PROCESSES_PER_SESSION} processes/session; the live session registry was unavailable).`,
|
|
257
279
|
};
|
|
258
280
|
}
|
|
@@ -339,10 +361,11 @@ export function evaluate(snapshot, thresholds, options = {}) {
|
|
|
339
361
|
}
|
|
340
362
|
|
|
341
363
|
// --- Concurrency axis: registry sessions, not raw process count -----------
|
|
364
|
+
// REPORT-ONLY (#1654): a standing condition on 67.4% of M4 starts must not
|
|
365
|
+
// become the second signal that caps a wave (HR-104, see concurrencySignal).
|
|
342
366
|
const conc = concurrencySignal(snapshot, concWarn);
|
|
343
|
-
if (conc && conc.
|
|
344
|
-
|
|
345
|
-
reasons.push(conc.reason);
|
|
367
|
+
if (conc && conc.reason) {
|
|
368
|
+
reasons.push(`info: ${conc.reason} Not counted as a capacity signal (standing condition, #1654).`);
|
|
346
369
|
}
|
|
347
370
|
|
|
348
371
|
// --- Swap: only meaningful when the memory signal is NOT healthy ----------
|
|
@@ -306,7 +306,9 @@ export async function ramAvailableGb() {
|
|
|
306
306
|
* #1089 the threshold was compared against `claude_processes_count` instead —
|
|
307
307
|
* a measured 6x unit error (median processes:sessions = 6.0 over 1461 paired
|
|
308
308
|
* samples), which is why a threshold of 5 fired on 93.6% of session starts
|
|
309
|
-
* while the same threshold against the real count
|
|
309
|
+
* while the same threshold against the real count fired on 4.2% at the time.
|
|
310
|
+
* Re-measured 2026-10-17 (#1650) it fires on 67.4% (M4) / 25.7% (M5) — a
|
|
311
|
+
* standing signal (HR-101), so the banner icon follows a start wave instead.
|
|
310
312
|
*
|
|
311
313
|
* Semantics mirror `detectPeers()` exactly: EXCLUDES the calling session and
|
|
312
314
|
* counts only entries whose `last_heartbeat` is within `freshnessMin` minutes.
|
|
@@ -203,8 +203,8 @@ function unquoteYamlScalar(valuePart) {
|
|
|
203
203
|
* its `paths:` alias (issue #795), and the scalar activation keys (issue #694).
|
|
204
204
|
*
|
|
205
205
|
* Returns:
|
|
206
|
-
* - `{ globs: string[] | null, meta: object }` on success
|
|
207
|
-
* - `{ globs: null, meta: {}, defect: 'frontmatter-not-at-top' }` when a
|
|
206
|
+
* - `{ globs: string[] | null, paths: string[] | null, meta: object }` on success
|
|
207
|
+
* - `{ globs: null, paths: null, meta: {}, defect: 'frontmatter-not-at-top' }` when a
|
|
208
208
|
* frontmatter block exists but does not start on line 1 (#1449)
|
|
209
209
|
* - throws `Error` on malformed frontmatter so the caller can fall back
|
|
210
210
|
*
|
|
@@ -212,8 +212,11 @@ function unquoteYamlScalar(valuePart) {
|
|
|
212
212
|
* present (always-on). `paths:` is parsed with the identical inline-array and
|
|
213
213
|
* block-list forms as `globs:`; when BOTH keys are present on the same rule,
|
|
214
214
|
* `globs:` wins silently and `paths:` is discarded (no merge, no warning —
|
|
215
|
-
* see module doc).
|
|
216
|
-
*
|
|
215
|
+
* see module doc). `globs` never tells callers which key produced the value;
|
|
216
|
+
* the additive `paths` field does carry the raw `paths:` list on its own
|
|
217
|
+
* (`null` when the key is absent), because Claude Code reads ONLY `paths:` and
|
|
218
|
+
* ignores `globs:` (#1638) — a caller asking "what does Claude Code scope?"
|
|
219
|
+
* needs that key alone, not the merged value. `meta` carries only the recognised scalar
|
|
217
220
|
* keys that were present, with type coercion applied:
|
|
218
221
|
* - `alwaysApply`, `auto-generated` → boolean ('true'/'false')
|
|
219
222
|
* - `confidence` → number (undefined when NaN)
|
|
@@ -226,15 +229,15 @@ function unquoteYamlScalar(valuePart) {
|
|
|
226
229
|
* the `defect` field names the misplacement (see {@link FRONTMATTER_NOT_AT_TOP}).
|
|
227
230
|
*
|
|
228
231
|
* @param {string} contents - raw file contents
|
|
229
|
-
* @returns {{ globs: string[] | null, meta: Record<string, unknown>, defect?: 'frontmatter-not-at-top' }}
|
|
232
|
+
* @returns {{ globs: string[] | null, paths: string[] | null, meta: Record<string, unknown>, defect?: 'frontmatter-not-at-top' }}
|
|
230
233
|
*/
|
|
231
234
|
export function parseGlobsFrontmatter(contents) {
|
|
232
235
|
const match = FRONTMATTER_RE.exec(contents);
|
|
233
236
|
if (match) return parseFrontmatterBlock(match[1]);
|
|
234
237
|
if (hasDisplacedFrontmatter(contents)) {
|
|
235
|
-
return { globs: null, meta: {}, defect: FRONTMATTER_NOT_AT_TOP };
|
|
238
|
+
return { globs: null, paths: null, meta: {}, defect: FRONTMATTER_NOT_AT_TOP };
|
|
236
239
|
}
|
|
237
|
-
return { globs: null, meta: {} };
|
|
240
|
+
return { globs: null, paths: null, meta: {} };
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
/**
|
|
@@ -274,7 +277,7 @@ function hasDisplacedFrontmatter(contents) {
|
|
|
274
277
|
* Throws on a malformed top-level line — see {@link parseGlobsFrontmatter}.
|
|
275
278
|
*
|
|
276
279
|
* @param {string} fmText - frontmatter body without its fences
|
|
277
|
-
* @returns {{ globs: string[] | null, meta: Record<string, unknown> }}
|
|
280
|
+
* @returns {{ globs: string[] | null, paths: string[] | null, meta: Record<string, unknown> }}
|
|
278
281
|
*/
|
|
279
282
|
function parseFrontmatterBlock(fmText) {
|
|
280
283
|
const lines = fmText.split(/\r?\n/);
|
|
@@ -370,7 +373,7 @@ function parseFrontmatterBlock(fmText) {
|
|
|
370
373
|
// Precedence (issue #795): `globs:` wins silently when both keys are present.
|
|
371
374
|
const globs = globsValue !== null ? globsValue : pathsValue;
|
|
372
375
|
|
|
373
|
-
return { globs, meta };
|
|
376
|
+
return { globs, paths: pathsValue, meta };
|
|
374
377
|
}
|
|
375
378
|
|
|
376
379
|
/**
|
|
@@ -565,6 +568,10 @@ function applyGates(meta, filePath, mode, hostClass, now, context = null) {
|
|
|
565
568
|
* @param {string|null} [opts.context] - 'wave' | 'coordinator' | null; when non-null, applies
|
|
566
569
|
* tier gating: 'wave' excludes coordinator-only rules, 'coordinator' excludes wave-only rules.
|
|
567
570
|
* null (default) disables tier gating entirely — backward-compatible with all existing callers.
|
|
571
|
+
* @param {'merged'|'paths'} [opts.scopeKey] - which frontmatter key scopes a rule. 'merged'
|
|
572
|
+
* (default) is this loader's own wave-injection semantics: `globs:`, else its `paths:` alias.
|
|
573
|
+
* 'paths' is Claude Code's native semantics (#1638): only `paths:` scopes, so a rule with
|
|
574
|
+
* `globs:` but no `paths:` is always-on — Claude Code ignores `globs:` without an error.
|
|
568
575
|
* @returns {RuleEntry[]}
|
|
569
576
|
*/
|
|
570
577
|
export function loadApplicableRules({
|
|
@@ -574,6 +581,7 @@ export function loadApplicableRules({
|
|
|
574
581
|
hostClass = null,
|
|
575
582
|
now = Date.now(),
|
|
576
583
|
context = null,
|
|
584
|
+
scopeKey = 'merged',
|
|
577
585
|
}) {
|
|
578
586
|
/** @type {string[]} */
|
|
579
587
|
let entries;
|
|
@@ -622,7 +630,9 @@ export function loadApplicableRules({
|
|
|
622
630
|
let parseError = false;
|
|
623
631
|
|
|
624
632
|
try {
|
|
625
|
-
|
|
633
|
+
const parsed = parseGlobsFrontmatter(content);
|
|
634
|
+
({ meta, defect } = parsed);
|
|
635
|
+
globs = scopeKey === 'paths' ? parsed.paths : parsed.globs;
|
|
626
636
|
} catch (err) {
|
|
627
637
|
process.stderr.write(
|
|
628
638
|
`[rule-loader] Frontmatter parse error in ${filePath}: ${err.message} — treating as always-on\n`,
|