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
|
@@ -12,9 +12,12 @@
|
|
|
12
12
|
* - File-absent or all-null-token sessions return a sentinel shape with null
|
|
13
13
|
* totals (not 0) so callers can distinguish "session had no token data" from
|
|
14
14
|
* "session was genuinely free / cost $0".
|
|
15
|
+
* - `match_status` and `ledger_records` distinguish why totals are absent
|
|
16
|
+
* without interpreting the session key's shape (#1027 Nachtrag 7).
|
|
15
17
|
* - Malformed JSONL lines are silently skipped (resilience over strictness).
|
|
16
18
|
* - `subagents_with_tokens` counts distinct agent_ids that have at least one
|
|
17
|
-
* TOKEN-BEARING record (coverage metric)
|
|
19
|
+
* TOKEN-BEARING record (coverage metric); every total counts each agent once
|
|
20
|
+
* (§ One record per agent).
|
|
18
21
|
*
|
|
19
22
|
* ## Token provenance — why a bare Σ over token_input is wrong (#949)
|
|
20
23
|
*
|
|
@@ -64,10 +67,48 @@
|
|
|
64
67
|
* `total_token_*` and reports the excluded ones as `legacy_v1_records`: the
|
|
65
68
|
* boundary is DECLARED, never silent.
|
|
66
69
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
70
|
+
* ## One record per agent — a stop record carries a RUNNING total
|
|
71
|
+
*
|
|
72
|
+
* `hooks/subagent-telemetry.mjs` re-reads the agent's WHOLE transcript at every
|
|
73
|
+
* stop, so an agent that stops twice (resumed via SendMessage, for one) writes
|
|
74
|
+
* its running total twice, and summing both counts the first part again. Every
|
|
75
|
+
* total below therefore takes ONE v2 record per `agent_id`: its tokens come from
|
|
76
|
+
* the agent's last stop that found its transcript and carried tokens, its cost
|
|
77
|
+
* candidacy from its last stop that found its transcript ("last" = latest
|
|
78
|
+
* `timestamp`, ties and unparseable timestamps by file order). Those two differ
|
|
79
|
+
* only when the last read yielded no tokens — an oversized transcript: the
|
|
80
|
+
* earlier running total is then a lower bound for the tokens and the cost is
|
|
81
|
+
* unknown. Measured 2026-10-01 over the 31 local fleet ledgers (1,002 distinct
|
|
82
|
+
* session/agent pairs with two or more found stops, 407 sessions): no v2 token
|
|
83
|
+
* bucket ever decreased from one found stop to the next, so the last record is
|
|
84
|
+
* also the largest; 2 pairs end in a token-less stop, both transcripts now past
|
|
85
|
+
* the hook's 50 MiB read limit. FORWARD-ONLY like the #949 cut: totals already
|
|
86
|
+
* in `sessions.jsonl` are not recomputed.
|
|
87
|
+
*
|
|
88
|
+
* `total_cost_usd` is computed per agent via `costUsd()` and is null whenever
|
|
89
|
+
* `cost_records_priced < cost_records_total` (#1475) — a partial cost is worse
|
|
90
|
+
* than no cost, because it reads as a complete one. An agent is unpriced when the
|
|
91
|
+
* model of its last found record is unknown to the price table, when it has no model (a token-bearing turn
|
|
92
|
+
* named none), when it has no tokens at all (transcript found but oversized,
|
|
93
|
+
* without usage turns, or unreadable, #1474), or when the agent has a start
|
|
94
|
+
* record but its stop found no transcript of its own and no other record of the
|
|
95
|
+
* same agent did (`start_record_found: true`, `subagent_transcript_found: false`
|
|
96
|
+
* — mostly `workflow-subagent`). A phantom stop (#939, no start record) is no
|
|
97
|
+
* agent and no candidate. A record whose four token buckets
|
|
98
|
+
* are all 0 is priced at $0 whatever its model, and never turns a null total into
|
|
99
|
+
* a number on its own. A record whose turns span several models carries
|
|
100
|
+
* `models_usage` (#1470) and is priced part by part. `cost_records_priced` /
|
|
101
|
+
* `cost_records_total` say how much of the session the estimate covers; with
|
|
102
|
+
* nothing to price (total 0) the cost is null as well.
|
|
103
|
+
*
|
|
104
|
+
* Blind spots the ledger cannot close: an agent whose start record lies outside
|
|
105
|
+
* the hook's tail window reads `start_record_found: false` and is
|
|
106
|
+
* indistinguishable from a phantom, and an agent that left neither a start nor a
|
|
107
|
+
* stop record is invisible. One blind spot is a rule of this module, not the
|
|
108
|
+
* ledger: a session spanning the 2026-09-09 schema boundary is priced over its v2
|
|
109
|
+
* records only — its v1 records are `legacy_v1_records`, never cost candidates
|
|
110
|
+
* (4 distinct fleet sessions carry such a numeric cost, 31 ledgers, 2026-10-01).
|
|
111
|
+
* "Never a partial sum" holds only over the v2 agents the ledger can see.
|
|
71
112
|
*
|
|
72
113
|
* @module session-token-rollup
|
|
73
114
|
*/
|
|
@@ -102,6 +143,26 @@ function isTokenBearing(record) {
|
|
|
102
143
|
return record?.subagent_transcript_found === true;
|
|
103
144
|
}
|
|
104
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Did a REAL agent run whose transcript the hook could not find? (#1475 follow-up)
|
|
148
|
+
*
|
|
149
|
+
* A stop record with a start record (`start_record_found: true`) but no own
|
|
150
|
+
* transcript (`subagent_transcript_found: false`) is not a phantom: the agent
|
|
151
|
+
* existed, it just left nothing to price — 813 of the 877 such records in the
|
|
152
|
+
* 33 fleet ledgers are `workflow-subagent` (2026-10-01). Its cost is unknown, so it must reach
|
|
153
|
+
* `cost_records_total`. A phantom stop (#939) has `start_record_found` false and
|
|
154
|
+
* stays out; a record written before either flag existed matches neither.
|
|
155
|
+
* @param {object} record
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
function isStartedWithoutTranscript(record) {
|
|
159
|
+
return (
|
|
160
|
+
record?.event === 'stop' &&
|
|
161
|
+
record.start_record_found === true &&
|
|
162
|
+
record.subagent_transcript_found === false
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
105
166
|
/**
|
|
106
167
|
* Is this record inside the schema_version 2 token contract? (#1244)
|
|
107
168
|
* @param {object} record
|
|
@@ -111,18 +172,75 @@ function isV2(record) {
|
|
|
111
172
|
return typeof record?.schema_version === 'number' && record.schema_version >= 2;
|
|
112
173
|
}
|
|
113
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Does this record carry a token count on either side?
|
|
177
|
+
* @param {object} record
|
|
178
|
+
* @returns {boolean}
|
|
179
|
+
*/
|
|
180
|
+
function hasTokens(record) {
|
|
181
|
+
const { token_input: inp, token_output: out } = record;
|
|
182
|
+
return (typeof inp === 'number' && inp >= 0) || (typeof out === 'number' && out >= 0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Was `candidate` written at a later stop than `current`? Ordered by
|
|
187
|
+
* `timestamp`; a tie or an unparseable timestamp falls back to file order, and
|
|
188
|
+
* the caller reads in file order, so the candidate then wins.
|
|
189
|
+
* @param {object} candidate — read after `current` in the ledger
|
|
190
|
+
* @param {object} current
|
|
191
|
+
* @returns {boolean}
|
|
192
|
+
*/
|
|
193
|
+
function isLaterStop(candidate, current) {
|
|
194
|
+
const a = Date.parse(candidate.timestamp);
|
|
195
|
+
const b = Date.parse(current.timestamp);
|
|
196
|
+
if (Number.isNaN(a) || Number.isNaN(b) || a === b) return true;
|
|
197
|
+
return a > b;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* USD cost of one v2 record, or null when it cannot be priced. A record whose
|
|
202
|
+
* token-bearing turns span several models carries `models_usage` (#1470): each
|
|
203
|
+
* part is priced at its own model's rates, and one unknown part leaves the whole
|
|
204
|
+
* record unpriced. Without that field the record's single `model` prices all
|
|
205
|
+
* four buckets.
|
|
206
|
+
* @param {object} record
|
|
207
|
+
* @returns {number|null}
|
|
208
|
+
*/
|
|
209
|
+
function recordCostUsd(record) {
|
|
210
|
+
const parts =
|
|
211
|
+
Array.isArray(record.models_usage) && record.models_usage.length > 0
|
|
212
|
+
? record.models_usage
|
|
213
|
+
: [record];
|
|
214
|
+
let total = 0;
|
|
215
|
+
for (const part of parts) {
|
|
216
|
+
const cost = costUsd({
|
|
217
|
+
model: part?.model,
|
|
218
|
+
tokenInputUncached: part?.token_input_uncached,
|
|
219
|
+
tokenCacheRead: part?.token_cache_read,
|
|
220
|
+
tokenCacheCreation: part?.token_cache_creation,
|
|
221
|
+
tokenOutput: part?.token_output,
|
|
222
|
+
});
|
|
223
|
+
if (cost === null) return null;
|
|
224
|
+
total += cost;
|
|
225
|
+
}
|
|
226
|
+
return total;
|
|
227
|
+
}
|
|
228
|
+
|
|
114
229
|
/**
|
|
115
230
|
* @typedef {Object} TokenRollupResult
|
|
116
|
-
* @property {
|
|
117
|
-
* @property {number|null}
|
|
231
|
+
* @property {'invalid-key'|'ledger-absent'|'ledger-empty'|'unmatched'|'matched'} match_status - Distinguishes absent telemetry from an unmatched key (#1027 Nachtrag 7).
|
|
232
|
+
* @property {number|null} ledger_records - Count of parsed non-null, non-array objects; null when the ledger was not read (#1027 Nachtrag 7).
|
|
233
|
+
* @property {number|null} total_token_input - Sum of token_input over one v2 token-bearing record per agent (its last stop that carried tokens — § One record per agent); null when none had a non-null value.
|
|
234
|
+
* @property {number|null} total_token_output - Sum of token_output over the same one record per agent; null when none had a non-null value.
|
|
235
|
+
* @property {number|null} total_tokens - total_token_input + total_token_output (#1436); a null side counts as absent, null only when BOTH are null.
|
|
118
236
|
* @property {number} subagents_with_tokens - Count of distinct agent_ids with at least one token-bearing record. This is the numerator of the honest coverage ratio.
|
|
119
237
|
* @property {number} matched_records - Total count of JSONL records matched by parentSessionId. Counts start records, phantom stops and pre-#949 records alike, so it is NOT the denominator for a token-coverage ratio — dividing by it is what made healthy sessions read as 12% covered.
|
|
120
|
-
* @property {number|null} total_token_input_uncached - Sum of token_input_uncached
|
|
121
|
-
* @property {number|null} total_token_cache_read - Sum of token_cache_read
|
|
122
|
-
* @property {number|null} total_token_cache_creation - Sum of token_cache_creation
|
|
123
|
-
* @property {number|null} total_cost_usd - Σ
|
|
124
|
-
* @property {number} cost_records_priced - How many
|
|
125
|
-
* @property {number} cost_records_total - How many token-bearing
|
|
238
|
+
* @property {number|null} total_token_input_uncached - Sum of token_input_uncached over the same one record per agent.
|
|
239
|
+
* @property {number|null} total_token_cache_read - Sum of token_cache_read over the same one record per agent.
|
|
240
|
+
* @property {number|null} total_token_cache_creation - Sum of token_cache_creation over the same one record per agent.
|
|
241
|
+
* @property {number|null} total_cost_usd - Σ cost over the agents counted in cost_records_priced; null when cost_records_priced < cost_records_total (#1475 — any unpriced candidate: unknown model, no model, no tokens, or a started agent without a transcript), null when there is nothing to price (cost_records_total 0), and null when no record with a non-zero bucket was priced (all-zero records alone never yield a fabricated $0). Never 0 for unknown — see telemetry/pricing.mjs.
|
|
242
|
+
* @property {number} cost_records_priced - How many agents of cost_records_total carry a known cost on their last found v2 record: priced by the table (per model part when the record carries models_usage, #1470), or all four token buckets 0 (priced at $0 whatever the model, #1474).
|
|
243
|
+
* @property {number} cost_records_total - How many agents were candidates for pricing, each counted once however often it stopped: every agent with a v2 record whose own transcript was found — priced on its LAST such record, so one whose last read yielded no tokens (transcript oversized, without usage turns, or unreadable, #1474) is unpriced — plus every started agent whose v2 stops found no transcript of its own (`start_record_found: true`, `subagent_transcript_found: false`) and that has no token-bearing record in the session. Both latter kinds have an unknown cost. Phantom stops (no start record, #939) are never candidates. priced < total nulls total_cost_usd.
|
|
126
244
|
* @property {number} legacy_v1_records - Token-bearing records EXCLUDED from every total above because their schema_version < 2 (their token_input is a different quantity).
|
|
127
245
|
* @property {2} _token_schema - The token contract these totals were computed under.
|
|
128
246
|
*/
|
|
@@ -142,8 +260,11 @@ export function rollupSessionTokens({
|
|
|
142
260
|
}) {
|
|
143
261
|
/** @type {TokenRollupResult} */
|
|
144
262
|
const ZERO = {
|
|
263
|
+
match_status: 'invalid-key',
|
|
264
|
+
ledger_records: null,
|
|
145
265
|
total_token_input: null,
|
|
146
266
|
total_token_output: null,
|
|
267
|
+
total_tokens: null,
|
|
147
268
|
subagents_with_tokens: 0,
|
|
148
269
|
matched_records: 0,
|
|
149
270
|
total_token_input_uncached: null,
|
|
@@ -169,7 +290,7 @@ export function rollupSessionTokens({
|
|
|
169
290
|
raw = readFileSync(resolvedPath, 'utf8');
|
|
170
291
|
} catch (err) {
|
|
171
292
|
if (err.code === 'ENOENT') {
|
|
172
|
-
return { ...ZERO };
|
|
293
|
+
return { ...ZERO, match_status: 'ledger-absent' };
|
|
173
294
|
}
|
|
174
295
|
throw err;
|
|
175
296
|
}
|
|
@@ -177,6 +298,7 @@ export function rollupSessionTokens({
|
|
|
177
298
|
// Parse JSONL — skip malformed lines, filter to parentSessionId.
|
|
178
299
|
const lines = raw.split('\n');
|
|
179
300
|
const matched = [];
|
|
301
|
+
let ledgerRecords = 0;
|
|
180
302
|
for (const line of lines) {
|
|
181
303
|
const trimmed = line.trim();
|
|
182
304
|
if (!trimmed) continue;
|
|
@@ -187,13 +309,19 @@ export function rollupSessionTokens({
|
|
|
187
309
|
// Malformed line — skip silently.
|
|
188
310
|
continue;
|
|
189
311
|
}
|
|
190
|
-
if (record
|
|
312
|
+
if (record === null || typeof record !== 'object' || Array.isArray(record)) continue;
|
|
313
|
+
ledgerRecords += 1;
|
|
314
|
+
if (record.parent_session_id === parentSessionId) {
|
|
191
315
|
matched.push(record);
|
|
192
316
|
}
|
|
193
317
|
}
|
|
194
318
|
|
|
195
319
|
if (matched.length === 0) {
|
|
196
|
-
return {
|
|
320
|
+
return {
|
|
321
|
+
...ZERO,
|
|
322
|
+
match_status: ledgerRecords === 0 ? 'ledger-empty' : 'unmatched',
|
|
323
|
+
ledger_records: ledgerRecords,
|
|
324
|
+
};
|
|
197
325
|
}
|
|
198
326
|
|
|
199
327
|
// Aggregate — skip null/undefined token values.
|
|
@@ -205,7 +333,6 @@ export function rollupSessionTokens({
|
|
|
205
333
|
let sumCost = null;
|
|
206
334
|
let costPriced = 0;
|
|
207
335
|
let costTotal = 0;
|
|
208
|
-
let costUnknownModel = false;
|
|
209
336
|
let legacyV1 = 0;
|
|
210
337
|
|
|
211
338
|
// Track distinct agent_ids that contributed at least one non-null token.
|
|
@@ -214,13 +341,50 @@ export function rollupSessionTokens({
|
|
|
214
341
|
const addNonNegative = (acc, value) =>
|
|
215
342
|
typeof value === 'number' && value >= 0 ? (acc ?? 0) + value : acc;
|
|
216
343
|
|
|
344
|
+
// Agents with at least one record whose own transcript WAS read. A later stop
|
|
345
|
+
// of the same agent can find a transcript an earlier one missed (measured
|
|
346
|
+
// 2026-10-01: 4 agents in one fleet session, `workflow-subagent` found:false
|
|
347
|
+
// first, then found:true) — its cost is then known and must not null the total.
|
|
348
|
+
const agentsWithTranscript = new Set(
|
|
349
|
+
matched.filter(isTokenBearing).map((r) => r.agent_id).filter((id) => typeof id === 'string'),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// One agent, one count (see module header § One record per agent). Per agent:
|
|
353
|
+
// `last` is its last stop that found its transcript — the cost candidate —
|
|
354
|
+
// and `lastWithTokens` its last such stop that carried tokens — the token
|
|
355
|
+
// contribution. They differ only when the last read yielded no tokens (an
|
|
356
|
+
// oversized transcript): the earlier running total is then a known lower bound
|
|
357
|
+
// for the tokens, while the cost stays unknown. A record without an agent_id
|
|
358
|
+
// cannot be joined to another and stands alone.
|
|
359
|
+
/** @type {Map<unknown, {last: object, lastWithTokens: object|null}>} */
|
|
360
|
+
const agents = new Map();
|
|
361
|
+
/** Started agents whose stops found no transcript — each counts once. */
|
|
362
|
+
const startedWithoutTranscript = new Set();
|
|
363
|
+
let unkeyedStartedWithoutTranscript = 0;
|
|
364
|
+
|
|
217
365
|
for (const record of matched) {
|
|
218
366
|
// Provenance gate (#949) — a record whose tokens describe the PARENT
|
|
219
|
-
// transcript, or no transcript at all, contributes
|
|
367
|
+
// transcript, or no transcript at all, contributes no TOKENS. Skipping it
|
|
220
368
|
// entirely (rather than treating its values as 0) preserves the null
|
|
221
369
|
// sentinel: a session of only untrustworthy records reports "no data",
|
|
222
370
|
// which is true, instead of a fabricated 0.
|
|
223
|
-
if (!isTokenBearing(record))
|
|
371
|
+
if (!isTokenBearing(record)) {
|
|
372
|
+
// …but a v2 agent that really ran without a readable transcript has an
|
|
373
|
+
// unknown cost: an unpriced candidate (total, not priced), so priced <
|
|
374
|
+
// total nulls `total_cost_usd` (#1475). Phantoms never reach this branch.
|
|
375
|
+
if (
|
|
376
|
+
isV2(record) &&
|
|
377
|
+
isStartedWithoutTranscript(record) &&
|
|
378
|
+
!agentsWithTranscript.has(record.agent_id)
|
|
379
|
+
) {
|
|
380
|
+
if (record.agent_id === undefined || record.agent_id === null) {
|
|
381
|
+
unkeyedStartedWithoutTranscript += 1;
|
|
382
|
+
} else {
|
|
383
|
+
startedWithoutTranscript.add(record.agent_id);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
224
388
|
|
|
225
389
|
// Schema gate (#1244) — a v1 record's token_input is raw uncached input,
|
|
226
390
|
// a different quantity from a v2 record's billable prompt volume. Count it
|
|
@@ -230,52 +394,81 @@ export function rollupSessionTokens({
|
|
|
230
394
|
continue;
|
|
231
395
|
}
|
|
232
396
|
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
397
|
+
const withTokens = hasTokens(record) ? record : null;
|
|
398
|
+
const key = record.agent_id ?? Symbol('unkeyed');
|
|
399
|
+
const seen = agents.get(key);
|
|
400
|
+
if (!seen) {
|
|
401
|
+
agents.set(key, { last: record, lastWithTokens: withTokens });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
// A token-bearing `last` is always `lastWithTokens` too — set together, so
|
|
405
|
+
// an unparseable timestamp (no transitive order) cannot split them.
|
|
406
|
+
if (isLaterStop(record, seen.last)) {
|
|
407
|
+
seen.last = record;
|
|
408
|
+
if (withTokens) seen.lastWithTokens = withTokens;
|
|
409
|
+
} else if (withTokens && (!seen.lastWithTokens || isLaterStop(withTokens, seen.lastWithTokens))) {
|
|
410
|
+
seen.lastWithTokens = withTokens;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
241
413
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
414
|
+
for (const [key, { last, lastWithTokens }] of agents) {
|
|
415
|
+
if (lastWithTokens) {
|
|
416
|
+
sumInput = addNonNegative(sumInput, lastWithTokens.token_input);
|
|
417
|
+
sumOutput = addNonNegative(sumOutput, lastWithTokens.token_output);
|
|
418
|
+
sumUncached = addNonNegative(sumUncached, lastWithTokens.token_input_uncached);
|
|
419
|
+
sumCacheRead = addNonNegative(sumCacheRead, lastWithTokens.token_cache_read);
|
|
420
|
+
sumCacheCreation = addNonNegative(sumCacheCreation, lastWithTokens.token_cache_creation);
|
|
421
|
+
if (typeof key !== 'symbol') agentsWithTokens.add(key);
|
|
422
|
+
}
|
|
249
423
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
424
|
+
// Cost: every agent with a found transcript is ONE pricing candidate. One
|
|
425
|
+
// unpriced agent poisons the SESSION total — a cost covering some of the
|
|
426
|
+
// agents reads as covering all of them.
|
|
427
|
+
costTotal += 1;
|
|
428
|
+
if (last !== lastWithTokens) {
|
|
429
|
+
// #1474 — the agent's last read of its own transcript yielded no tokens:
|
|
430
|
+
// it exceeded the hook's MAX_TRANSCRIPT_BYTES and was not read, it held no
|
|
431
|
+
// usage turns, or it was unreadable. Either way the agent ran and its cost
|
|
432
|
+
// is unknown, so it counts as an unpriced candidate (total, not priced),
|
|
433
|
+
// and priced < total nulls `total_cost_usd` (#1475).
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
// #1474 — all four buckets 0 costs $0 whatever the model says. The hook
|
|
437
|
+
// names such a record by its last turn, typically `<synthetic>`, which
|
|
438
|
+
// the price table does not know; pricing it would null the session total
|
|
439
|
+
// for a record that added nothing to it.
|
|
440
|
+
const allZero = [
|
|
441
|
+
last.token_input_uncached,
|
|
442
|
+
last.token_cache_read,
|
|
443
|
+
last.token_cache_creation,
|
|
444
|
+
last.token_output,
|
|
445
|
+
].every((v) => v === 0);
|
|
446
|
+
const cost = allZero ? 0 : recordCostUsd(last);
|
|
447
|
+
if (cost !== null) {
|
|
448
|
+
costPriced += 1;
|
|
449
|
+
// An all-zero record adds nothing, so it must not be what turns
|
|
450
|
+
// `sumCost` from null into a number: next to a token-less record it
|
|
451
|
+
// would persist a fabricated $0 for a session whose real cost is unknown.
|
|
452
|
+
if (!allZero) sumCost = (sumCost ?? 0) + cost;
|
|
267
453
|
}
|
|
268
454
|
}
|
|
455
|
+
costTotal += startedWithoutTranscript.size + unkeyedStartedWithoutTranscript;
|
|
269
456
|
|
|
270
457
|
return {
|
|
458
|
+
match_status: 'matched',
|
|
459
|
+
ledger_records: ledgerRecords,
|
|
271
460
|
total_token_input: sumInput,
|
|
272
461
|
total_token_output: sumOutput,
|
|
462
|
+
// #1436 — the one figure a session-level budget compares against. Null only
|
|
463
|
+
// when neither side has data; never a fabricated 0.
|
|
464
|
+
total_tokens: sumInput === null && sumOutput === null ? null : (sumInput ?? 0) + (sumOutput ?? 0),
|
|
273
465
|
subagents_with_tokens: agentsWithTokens.size,
|
|
274
466
|
matched_records: matched.length,
|
|
275
467
|
total_token_input_uncached: sumUncached,
|
|
276
468
|
total_token_cache_read: sumCacheRead,
|
|
277
469
|
total_token_cache_creation: sumCacheCreation,
|
|
278
|
-
|
|
470
|
+
// #1475 — a cost that leaves out any candidate record is not persisted.
|
|
471
|
+
total_cost_usd: costPriced < costTotal ? null : sumCost,
|
|
279
472
|
cost_records_priced: costPriced,
|
|
280
473
|
cost_records_total: costTotal,
|
|
281
474
|
legacy_v1_records: legacyV1,
|
|
@@ -20,6 +20,18 @@
|
|
|
20
20
|
* (2026-05-10) carries a byte-identical duplicate LINE — an older
|
|
21
21
|
* collision class than (3), and rule (1) alone resolves it.
|
|
22
22
|
*
|
|
23
|
+
* EXCEPT: a backfill stub never shadows a record that is not one (#1443).
|
|
24
|
+
* A stub (a synthesised `_backfill_source` — repair provenance is not one,
|
|
25
|
+
* see `isBackfillStub()` — and no wave, agent or changed file, whatever the
|
|
26
|
+
* `status` or `_repair_source`) appended AFTER the real record for the
|
|
27
|
+
* same id hid that record from every reader — the writers were closed by
|
|
28
|
+
* #1368 and the key-occupancy guard, this reader was the remaining gap.
|
|
29
|
+
* Among non-stubs, and among stubs, the newest still wins. The same guard
|
|
30
|
+
* joins ACROSS ids: a stub sharing a non-empty `raw_session_id` with a
|
|
31
|
+
* non-stub under a DIFFERENT `session_id` is dropped for the non-stub. That
|
|
32
|
+
* join runs LAST, so it can neither orphan a synthetic twin of (2) nor
|
|
33
|
+
* change what (3) reports.
|
|
34
|
+
*
|
|
23
35
|
* (2) NARROW COLLAPSE OF THE SYSTEMIC DOUBLE-STUB CLASS.
|
|
24
36
|
* Two `abandoned` records with an EXACT `started_at` + `completed_at`
|
|
25
37
|
* tuple match are one physical session recorded twice by the two backfill
|
|
@@ -70,8 +82,8 @@
|
|
|
70
82
|
* refused marker keeps both records and is reported (never logged)
|
|
71
83
|
* via `canonicalizeSessionsDetailed().ignoredSupersedes`.
|
|
72
84
|
*
|
|
73
|
-
* RULE ORDER: (1) → (2) → (3). The double-stub collapse
|
|
74
|
-
* supersede removal: with the reverse order a `supersedes` append deleted the
|
|
85
|
+
* RULE ORDER: (1) → (2) → (3) → (1)'s raw-id join. The double-stub collapse
|
|
86
|
+
* must run BEFORE supersede removal: with the reverse order a `supersedes` append deleted the
|
|
75
87
|
* authentic stub first, shrank the tuple group to a single member, and the
|
|
76
88
|
* synthetic phantom then survived the very session that refuted it.
|
|
77
89
|
*
|
|
@@ -158,6 +170,80 @@ function supersedeRejectReason(superseder, target) {
|
|
|
158
170
|
return 'no-shared-join-key';
|
|
159
171
|
}
|
|
160
172
|
|
|
173
|
+
/**
|
|
174
|
+
* `_backfill_source` prefix that `session-record-repair.mjs` stamps
|
|
175
|
+
* (`REPAIR_SOURCE`, `'repair-invalid-sessions/1004'`) on a record it repaired
|
|
176
|
+
* in place that had NO origin of its own — i.e. a REAL record. A record that
|
|
177
|
+
* already carried a synthesised origin keeps it, and the repair goes to the
|
|
178
|
+
* separate `_repair_source` field, which this module deliberately ignores
|
|
179
|
+
* (#1443 F2). Kept as a literal so this module stays a stdlib-only leaf.
|
|
180
|
+
*/
|
|
181
|
+
const REPAIR_PROVENANCE_PREFIX = 'repair-invalid-sessions/';
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* True when a record is a BACKFILL STUB for rule (1)'s #1443 guard: it carries
|
|
185
|
+
* SYNTHESISED provenance (`_backfill_source`, but not repair provenance) AND
|
|
186
|
+
* measured no work (0 waves, agents and files) — whatever its `status`. A
|
|
187
|
+
* missing `status` is the norm on real records, so status cannot decide it.
|
|
188
|
+
*
|
|
189
|
+
* Repair provenance (`repair-invalid-sessions/…`) in `_backfill_source` is
|
|
190
|
+
* never a stub: the repair tool puts it there only on a record without an
|
|
191
|
+
* origin, i.e. a real record it fixed in place, not one it invented (#1443 F1 —
|
|
192
|
+
* a repaired 4-wave record lost to an older 2-wave one). A repaired STUB keeps
|
|
193
|
+
* its synthesised `_backfill_source` (`state-md-completed`, `events-jsonl`,
|
|
194
|
+
* `abandoned`) and is judged on that and on its work, whatever its
|
|
195
|
+
* `_repair_source` says (#1443 F2 — a repaired 0-wave stub shadowed a 4-wave
|
|
196
|
+
* record once the repair overwrote its origin). Legacy records repaired before
|
|
197
|
+
* F2 lost that origin irrecoverably and read as real.
|
|
198
|
+
*
|
|
199
|
+
* Exported for `scripts/autopilot.mjs`, which must not read a stub as a
|
|
200
|
+
* healthy 0-token iteration (#1457 F2). Not shared with
|
|
201
|
+
* `session-close-backfill.mjs::isSupersedableStub()` on purpose: that asks a
|
|
202
|
+
* narrower WRITER question ("may this be overwritten?" — `abandoned` only), and
|
|
203
|
+
* that module imports this one, so reusing it here would be a cycle.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} rec
|
|
206
|
+
* @returns {boolean}
|
|
207
|
+
*/
|
|
208
|
+
export function isBackfillStub(rec) {
|
|
209
|
+
if (!isNonEmptyString(rec._backfill_source)) return false;
|
|
210
|
+
if (rec._backfill_source.startsWith(REPAIR_PROVENANCE_PREFIX)) return false;
|
|
211
|
+
const hasWork = (Array.isArray(rec.waves) && rec.waves.length > 0)
|
|
212
|
+
|| rec.total_waves > 0 || rec.total_agents > 0 || rec.total_files_changed > 0;
|
|
213
|
+
return !hasWork;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Rule (1), cross-id half (#1443) — drop every backfill stub that shares a
|
|
218
|
+
* non-empty `raw_session_id` with at least one non-stub. A group of stubs only
|
|
219
|
+
* (or of non-stubs only) is left intact: with no non-stub to prefer there is no
|
|
220
|
+
* evidence which record is the artefact. Mutates `byId`.
|
|
221
|
+
*
|
|
222
|
+
* LIMIT (#1457 point 9): the join assumes one raw_session_id = one physical
|
|
223
|
+
* session. A raw id can also span a CLOSED session and a later, separate
|
|
224
|
+
* abandoned tail (measured: a stub starting 4 minutes after its partner's
|
|
225
|
+
* `completed_at`, LeadPipeDACH `main-2026-09-03-session-11`) — that stub is
|
|
226
|
+
* dropped too, so the tail is invisible to every reader. No time-window check
|
|
227
|
+
* guards against it; adding one is a separate decision.
|
|
228
|
+
* @param {Map<string, object>} byId
|
|
229
|
+
* @returns {void}
|
|
230
|
+
*/
|
|
231
|
+
function collapseStubsByRawSessionId(byId) {
|
|
232
|
+
const byRaw = new Map();
|
|
233
|
+
for (const rec of byId.values()) {
|
|
234
|
+
if (!isNonEmptyString(rec.raw_session_id)) continue;
|
|
235
|
+
const group = byRaw.get(rec.raw_session_id);
|
|
236
|
+
if (group) group.push(rec);
|
|
237
|
+
else byRaw.set(rec.raw_session_id, [rec]);
|
|
238
|
+
}
|
|
239
|
+
for (const group of byRaw.values()) {
|
|
240
|
+
if (group.length < 2 || group.every(isBackfillStub)) continue;
|
|
241
|
+
for (const rec of group) {
|
|
242
|
+
if (isBackfillStub(rec)) byId.delete(rec.session_id);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
161
247
|
/**
|
|
162
248
|
* Rule (3) — collapse the systemic two-writer double stub. Mutates `byId` by
|
|
163
249
|
* deleting the synthetic twin of each qualifying pair.
|
|
@@ -285,10 +371,13 @@ function resolveSupersedes(byId, ignored) {
|
|
|
285
371
|
*
|
|
286
372
|
* Rules, applied in this order (see the module header for the measured
|
|
287
373
|
* justification of each):
|
|
288
|
-
* 1. newest-wins per `session_id` (file order is chronological)
|
|
374
|
+
* 1. newest-wins per `session_id` (file order is chronological), except
|
|
375
|
+
* that a later backfill stub never shadows an earlier non-stub (#1443);
|
|
289
376
|
* 2. two `abandoned` records with an exact, both-present
|
|
290
377
|
* `started_at` + `completed_at` tuple collapse to the non-synthetic one;
|
|
291
|
-
* 3. a surviving record's ATTESTABLE `supersedes: X` removes record `X
|
|
378
|
+
* 3. a surviving record's ATTESTABLE `supersedes: X` removes record `X`;
|
|
379
|
+
* 4. a backfill stub sharing a non-empty `raw_session_id` with a surviving
|
|
380
|
+
* non-stub under another `session_id` is dropped (#1443).
|
|
292
381
|
*
|
|
293
382
|
* The double-stub collapse runs BEFORE supersede removal so that a stub which
|
|
294
383
|
* is itself about to be superseded still shadows its synthetic twin — with the
|
|
@@ -325,6 +414,9 @@ export function canonicalizeSessionsDetailed(records, { keepUnidentified = false
|
|
|
325
414
|
if (keepUnidentified) unidentified.push(rec);
|
|
326
415
|
continue;
|
|
327
416
|
}
|
|
417
|
+
// #1443 — a later backfill stub never shadows an earlier non-stub.
|
|
418
|
+
const prev = byId.get(rec.session_id);
|
|
419
|
+
if (prev && isBackfillStub(rec) && !isBackfillStub(prev)) continue;
|
|
328
420
|
byId.set(rec.session_id, rec);
|
|
329
421
|
}
|
|
330
422
|
|
|
@@ -335,6 +427,9 @@ export function canonicalizeSessionsDetailed(records, { keepUnidentified = false
|
|
|
335
427
|
const ignoredSupersedes = [];
|
|
336
428
|
resolveSupersedes(byId, ignoredSupersedes);
|
|
337
429
|
|
|
430
|
+
// -- (1b) stub vs non-stub across ids, joined by raw_session_id (#1443) ----
|
|
431
|
+
collapseStubsByRawSessionId(byId);
|
|
432
|
+
|
|
338
433
|
return { records: [...byId.values(), ...unidentified], ignoredSupersedes };
|
|
339
434
|
}
|
|
340
435
|
|
|
@@ -399,6 +494,25 @@ export function countSessionsInJsonl(raw) {
|
|
|
399
494
|
* is skipped rather than aborting the whole read (same posture as the readers
|
|
400
495
|
* in `session-close-backfill.mjs` and `backfill-abandoned-sessions.mjs`).
|
|
401
496
|
*
|
|
497
|
+
* NOT EVERY RAW READ OF `sessions.jsonl` IS A GAP TO CLOSE (#1221 P2). Four
|
|
498
|
+
* more readers bypass this module's collapse on purpose — verified via
|
|
499
|
+
* `rg -n "sessions.jsonl|SESSIONS"` against each file — and none of them
|
|
500
|
+
* should be "migrated" to `readCanonicalSessions`:
|
|
501
|
+
* - `scripts/lib/sessions-integrity-banner.mjs` reads every raw line
|
|
502
|
+
* (`readJsonlLines`) because it EXISTS to count malformed/mirror-invalid
|
|
503
|
+
* records; canonicalizing first would hide the corruption it reports on.
|
|
504
|
+
* - `scripts/lib/session-record-repair.mjs` is the repair tool itself — it
|
|
505
|
+
* must see every raw record, malformed and duplicate alike, to fix them;
|
|
506
|
+
* a canonical read would collapse or drop the very rows it needs to repair.
|
|
507
|
+
* - `scripts/migrate-sessions-jsonl.mjs` rewrites legacy-shaped records one
|
|
508
|
+
* raw line at a time; a canonicalized read could silently skip a
|
|
509
|
+
* superseded or duplicate legacy record before the migration ever sees it.
|
|
510
|
+
* - `scripts/gc-stale-worktrees.mjs` (`loadRecentSessionRefs`) scans every
|
|
511
|
+
* raw line's string content for worktree references within a 7-day
|
|
512
|
+
* window; a canonical read could drop a duplicate/superseded stub that
|
|
513
|
+
* still names a worktree, making a live worktree look unreferenced and
|
|
514
|
+
* eligible for garbage collection.
|
|
515
|
+
*
|
|
402
516
|
* @param {object} [args]
|
|
403
517
|
* @param {string} [args.repoRoot] project root; the ledger is resolved as
|
|
404
518
|
* `<repoRoot>/.orchestrator/metrics/sessions.jsonl`. Defaults to
|
|
@@ -103,7 +103,13 @@ import {
|
|
|
103
103
|
} from './vault-mirror/render-sessions.mjs';
|
|
104
104
|
|
|
105
105
|
/** Repo-relative path to the session ledger (one record per closed session). */
|
|
106
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Ledger path, relative to the repo root. EXPORTED (#1417) so the CLI front-end
|
|
108
|
+
* `scripts/check-sessions-integrity.mjs` addresses the same file this banner
|
|
109
|
+
* judges instead of re-typing the literal — two spellings of one path is how a
|
|
110
|
+
* guard ends up measuring a file nobody writes.
|
|
111
|
+
*/
|
|
112
|
+
export const SESSIONS_PATH = '.orchestrator/metrics/sessions.jsonl';
|
|
107
113
|
|
|
108
114
|
/** Max session_ids listed inline before the message collapses to "+N more". */
|
|
109
115
|
export const MAX_LISTED_IDS = 5;
|