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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pricing.mjs — USD price table for token-bucket cost estimation (#1244).
|
|
2
|
+
* pricing.mjs — USD price table for token-bucket cost estimation (#1244, #1470).
|
|
3
3
|
*
|
|
4
4
|
* The subagent ledger records four token buckets per stop record
|
|
5
5
|
* (`token_input_uncached`, `token_cache_read`, `token_cache_creation`,
|
|
@@ -8,23 +8,39 @@
|
|
|
8
8
|
* an order of magnitude on a cache-heavy run — which is the whole reason this
|
|
9
9
|
* table exists rather than a single `$/token` constant.
|
|
10
10
|
*
|
|
11
|
-
* ##
|
|
11
|
+
* ## Every rate is a documented figure
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* carries
|
|
16
|
-
* in this prose.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* multiplied out here.
|
|
13
|
+
* All four rates of every row are first-party Anthropic API list prices, read
|
|
14
|
+
* off the price page each row names in `source_url` on the date
|
|
15
|
+
* `PRICING_TABLE_DATE` carries — that constant is the SSOT, never a second date
|
|
16
|
+
* repeated in this prose. Nothing here is multiplied out. `cache_read` in
|
|
17
|
+
* particular is never derived from `input`: the ratio is not a constant (Opus
|
|
18
|
+
* 5.5 reads at 0.05× its input rate, Fable 5.1 at 0.025×, Fable 5 at 0.1×), so
|
|
19
|
+
* a multiplier would misprice exactly the cache-heavy runs this table exists
|
|
20
|
+
* for. Each row carries its own provenance fields, so a consumer quoting a
|
|
21
|
+
* dollar figure can cite where it came from without reading this file.
|
|
23
22
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* `verified: false` would mark a PLACEHOLDER row — a best-effort stand-in,
|
|
24
|
+
* never a quoted price. No current row carries it; a consumer publishing a
|
|
25
|
+
* dollar figure must still surface the flag.
|
|
26
|
+
*
|
|
27
|
+
* ## Only exact ids resolve
|
|
28
|
+
*
|
|
29
|
+
* `priceFor()` accepts an exact table key or an explicit `MODEL_ALIASES` entry
|
|
30
|
+
* and nothing else. Until #1470 it fell back to the longest table key an id
|
|
31
|
+
* started with, which handed an unlisted generation the rates of its nearest
|
|
32
|
+
* neighbour, marked verified.
|
|
33
|
+
*
|
|
34
|
+
* ## Series break (#1470, 2026-09-29)
|
|
35
|
+
*
|
|
36
|
+
* Session reports written before this fix priced records through that
|
|
37
|
+
* fallback: `claude-opus-5-5` at `claude-opus-5` rates, `claude-sonnet-5-5` at
|
|
38
|
+
* `claude-sonnet-5` rates (identical numbers, by coincidence rather than
|
|
39
|
+
* evidence), and `claude-fable-5` / `claude-opus-4-8` not at all. Nothing
|
|
40
|
+
* rewrites that history — the metrics ledgers are append-only. Measured
|
|
41
|
+
* 2026-09-29: 0 of the 9 priced sessions.jsonl reports in this repo contained
|
|
42
|
+
* such records (the first one reached subagents.jsonl after the last priced
|
|
43
|
+
* report was written).
|
|
28
44
|
*
|
|
29
45
|
* ## Null means UNKNOWN MODEL, never zero
|
|
30
46
|
*
|
|
@@ -39,68 +55,93 @@
|
|
|
39
55
|
*/
|
|
40
56
|
|
|
41
57
|
/**
|
|
42
|
-
* The date the
|
|
43
|
-
* Any consumer quoting a dollar figure should quote this
|
|
44
|
-
* price without its measurement date ages silently.
|
|
58
|
+
* The date the rates in PRICING_TABLE were read off the price page. Every row's
|
|
59
|
+
* `checked_at` equals it. Any consumer quoting a dollar figure should quote this
|
|
60
|
+
* date beside it — a price without its measurement date ages silently.
|
|
45
61
|
* @type {string}
|
|
46
62
|
*/
|
|
47
|
-
export const PRICING_TABLE_DATE = '2026-09-
|
|
63
|
+
export const PRICING_TABLE_DATE = '2026-09-29';
|
|
48
64
|
|
|
49
65
|
/**
|
|
50
66
|
* @typedef {Object} PricingRow
|
|
51
67
|
* @property {number} input - USD per 1M uncached prompt tokens.
|
|
52
68
|
* @property {number} cache_read - USD per 1M tokens served from the prompt cache.
|
|
53
|
-
* @property {number} cache_creation - USD per 1M tokens written to the prompt cache
|
|
69
|
+
* @property {number} cache_creation - USD per 1M tokens written to the prompt cache
|
|
70
|
+
* (the `cache_write_ttl` rate).
|
|
54
71
|
* @property {number} output - USD per 1M completion tokens.
|
|
55
72
|
* @property {boolean} verified - false ⇒ the row is a labelled placeholder, not a quoted price.
|
|
56
73
|
* @property {'documented'|'derived-multiplier'} cache_source - provenance of the two cache rates.
|
|
74
|
+
* Every current row is 'documented'; 'derived-multiplier' is kept for a future row whose cache
|
|
75
|
+
* rates are multiplied out and must say so.
|
|
76
|
+
* @property {string} source_url - the page all four rates were read from.
|
|
77
|
+
* @property {string} checked_at - ISO date the rates were read; equals PRICING_TABLE_DATE.
|
|
78
|
+
* @property {'anthropic'} provider
|
|
79
|
+
* @property {'5m'} cache_write_ttl - which cache-write rate `cache_creation` is.
|
|
80
|
+
* @property {string} unit - 'USD per 1M tokens'.
|
|
81
|
+
* @property {string} surface - the billing surface and speed the rates apply to.
|
|
82
|
+
* @property {'standard'} tier - the service tier the rates apply to.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/** Provenance shared by every current row — all read off one page on one date. */
|
|
86
|
+
const PROVENANCE = Object.freeze({
|
|
87
|
+
verified: true,
|
|
88
|
+
cache_source: 'documented',
|
|
89
|
+
source_url: 'https://platform.claude.com/docs/en/about-claude/pricing',
|
|
90
|
+
checked_at: PRICING_TABLE_DATE,
|
|
91
|
+
provider: 'anthropic',
|
|
92
|
+
cache_write_ttl: '5m',
|
|
93
|
+
unit: 'USD per 1M tokens',
|
|
94
|
+
surface: 'Claude API (first-party), standard speed',
|
|
95
|
+
tier: 'standard',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One frozen documented row: four USD rates plus the shared provenance. The
|
|
100
|
+
* rates are spread last, so provenance can never overwrite a price.
|
|
101
|
+
* @param {{input: number, cache_read: number, cache_creation: number, output: number}} rates
|
|
102
|
+
* @returns {PricingRow}
|
|
57
103
|
*/
|
|
104
|
+
function usd(rates) {
|
|
105
|
+
return Object.freeze({ ...PROVENANCE, ...rates });
|
|
106
|
+
}
|
|
58
107
|
|
|
59
108
|
/**
|
|
60
109
|
* Per-model USD-per-million-token rates.
|
|
61
110
|
* @type {Readonly<Record<string, PricingRow>>}
|
|
62
111
|
*/
|
|
63
112
|
export const PRICING_TABLE = Object.freeze({
|
|
64
|
-
//
|
|
65
|
-
// (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}),
|
|
82
|
-
'claude-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}),
|
|
90
|
-
'claude-haiku-4-5': Object.freeze({
|
|
91
|
-
input: 1.0,
|
|
92
|
-
cache_read: 0.1,
|
|
93
|
-
cache_creation: 1.25,
|
|
94
|
-
output: 5.0,
|
|
95
|
-
verified: true,
|
|
96
|
-
cache_source: 'derived-multiplier',
|
|
97
|
-
}),
|
|
113
|
+
// Ceiling: only the 5-minute cache-write rate is priced; a 1-hour write costs 2× input
|
|
114
|
+
// (Opus 5.5: $8/MTok) and would be under-priced here. Measured 2026-09-29 over 803,788
|
|
115
|
+
// assistant usage blocks in 10,745 subagent transcripts: `usage.cache_creation
|
|
116
|
+
// .ephemeral_1h_input_tokens > 0` on 0 blocks. Revisit when the first such block appears.
|
|
117
|
+
//
|
|
118
|
+
// Ceiling: list price only — fast mode, `inference_geo: "us"` (1.1×) and batch discounts are
|
|
119
|
+
// not modelled. Measured 2026-09-29: `usage.speed` = "fast" on 0 of the 374,331 blocks that
|
|
120
|
+
// carry the field; `inference_geo` = "not_available" on 803,479 and no other value seen.
|
|
121
|
+
// Revisit when either field shows another value.
|
|
122
|
+
//
|
|
123
|
+
// Ceiling: a model absent from this table prices as null, never at a neighbour's rate, so the
|
|
124
|
+
// rollup reports `total_cost_usd: null` and `cost_records_priced < cost_records_total` — the
|
|
125
|
+
// intended way missing evidence shows up. That ratio also drops below 1 for records no row can
|
|
126
|
+
// fix: a token-less record (transcript over the hook's size cap, without usage turns, or
|
|
127
|
+
// unreadable) and a record with a token-bearing turn that names no model (#1475). Revisit when
|
|
128
|
+
// an unpriced record names a model id absent from this table: add that model as its own
|
|
129
|
+
// evidenced row, never as a family prefix or an alias to another generation.
|
|
130
|
+
'claude-fable-5-1': usd({ input: 10.0, cache_read: 0.25, cache_creation: 12.5, output: 50.0 }),
|
|
131
|
+
'claude-fable-5': usd({ input: 10.0, cache_read: 1.0, cache_creation: 12.5, output: 50.0 }),
|
|
132
|
+
'claude-opus-5-5': usd({ input: 4.0, cache_read: 0.2, cache_creation: 5.0, output: 20.0 }),
|
|
133
|
+
'claude-opus-5': usd({ input: 5.0, cache_read: 0.5, cache_creation: 6.25, output: 25.0 }),
|
|
134
|
+
'claude-opus-4-8': usd({ input: 5.0, cache_read: 0.5, cache_creation: 6.25, output: 25.0 }),
|
|
135
|
+
'claude-sonnet-5-5': usd({ input: 2.0, cache_read: 0.2, cache_creation: 2.5, output: 10.0 }),
|
|
136
|
+
'claude-sonnet-5': usd({ input: 2.0, cache_read: 0.2, cache_creation: 2.5, output: 10.0 }),
|
|
137
|
+
'claude-haiku-4-5': usd({ input: 1.0, cache_read: 0.1, cache_creation: 1.25, output: 5.0 }),
|
|
98
138
|
});
|
|
99
139
|
|
|
100
140
|
/**
|
|
101
141
|
* Explicit aliases for model ids that appear in transcripts but are not the
|
|
102
|
-
* canonical table key — dated
|
|
103
|
-
* the harness stamps on long-context sessions.
|
|
142
|
+
* canonical table key — a dated snapshot of the SAME model and the `[1m]`
|
|
143
|
+
* context-window suffix the harness stamps on long-context sessions. An alias
|
|
144
|
+
* never points at another generation.
|
|
104
145
|
* @type {Readonly<Record<string, string>>}
|
|
105
146
|
*/
|
|
106
147
|
export const MODEL_ALIASES = Object.freeze({
|
|
@@ -113,10 +154,19 @@ export const MODEL_ALIASES = Object.freeze({
|
|
|
113
154
|
/**
|
|
114
155
|
* Look up the rate row for a model id.
|
|
115
156
|
*
|
|
116
|
-
* Resolution
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* `claude-opus-5-
|
|
157
|
+
* Resolution: exact `PRICING_TABLE` key → explicit `MODEL_ALIASES` entry → `null`,
|
|
158
|
+
* after trimming surrounding whitespace.
|
|
159
|
+
* Nothing else resolves — no prefix, no pattern, no family fallback — so
|
|
160
|
+
* `claude-opus-5-20260401`, `claude-opus-5-99` and `claude-haiku-4-5-20251002` are
|
|
161
|
+
* all UNKNOWN even though each starts with a table key.
|
|
162
|
+
*
|
|
163
|
+
* Why no prefix: from the 4.6 generation on, Anthropic ships dateless pinned
|
|
164
|
+
* model ids (https://platform.claude.com/docs/en/about-claude/models/model-ids-and-versions,
|
|
165
|
+
* retrieved 2026-09-29). The ids that extend a current table key are therefore
|
|
166
|
+
* other generations, not snapshots of it — `claude-opus-5-5` extends
|
|
167
|
+
* `claude-opus-5` — and a prefix match priced them at another generation's
|
|
168
|
+
* rates, marked verified (#1470). A future generation gets its own evidenced
|
|
169
|
+
* row, never a family prefix.
|
|
120
170
|
*
|
|
121
171
|
* @param {string|null|undefined} modelId
|
|
122
172
|
* @returns {PricingRow|null} the row, or null when the model is UNKNOWN
|
|
@@ -129,14 +179,10 @@ export function priceFor(modelId) {
|
|
|
129
179
|
|
|
130
180
|
if (Object.hasOwn(PRICING_TABLE, id)) return PRICING_TABLE[id];
|
|
131
181
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
let best = null;
|
|
136
|
-
for (const key of Object.keys(PRICING_TABLE)) {
|
|
137
|
-
if (id.startsWith(key) && (best === null || key.length > best.length)) best = key;
|
|
182
|
+
if (Object.hasOwn(MODEL_ALIASES, id) && Object.hasOwn(PRICING_TABLE, MODEL_ALIASES[id])) {
|
|
183
|
+
return PRICING_TABLE[MODEL_ALIASES[id]];
|
|
138
184
|
}
|
|
139
|
-
return
|
|
185
|
+
return null;
|
|
140
186
|
}
|
|
141
187
|
|
|
142
188
|
/**
|
|
@@ -161,9 +207,10 @@ function num(n) {
|
|
|
161
207
|
/**
|
|
162
208
|
* Estimate the USD cost of one record's four token buckets.
|
|
163
209
|
*
|
|
164
|
-
* Each bucket is multiplied by ITS OWN rate —
|
|
165
|
-
*
|
|
166
|
-
* blended rate is not an approximation of
|
|
210
|
+
* Each bucket is multiplied by ITS OWN rate — depending on the row, a cache
|
|
211
|
+
* read costs a tenth to a fortieth of an uncached prompt token and a 5-minute
|
|
212
|
+
* cache write 1.25× as much, so a blended rate is not an approximation of
|
|
213
|
+
* this, it is a different number.
|
|
167
214
|
*
|
|
168
215
|
* @param {object} opts
|
|
169
216
|
* @param {string|null|undefined} opts.model - model id from the transcript
|
|
@@ -93,6 +93,7 @@ export const USAGE_PING_OPTIONAL_FIELDS = Object.freeze([
|
|
|
93
93
|
'fleet_self_declared',
|
|
94
94
|
'session_record',
|
|
95
95
|
'session_profile',
|
|
96
|
+
'ledger_complete',
|
|
96
97
|
]);
|
|
97
98
|
|
|
98
99
|
/**
|
|
@@ -117,6 +118,29 @@ const USAGE_PING_PROJECTED_FIELDS = Object.freeze([...USAGE_PING_FIELDS, ...USAG
|
|
|
117
118
|
*/
|
|
118
119
|
export const SESSION_RECORD_SOURCES = Object.freeze(['ledger', 'derived', 'absent']);
|
|
119
120
|
|
|
121
|
+
/*
|
|
122
|
+
* `ledger_complete` — a SECOND, ORTHOGONAL axis beside `session_record`
|
|
123
|
+
* (GitLab #1416), deliberately NOT a fourth provenance token.
|
|
124
|
+
*
|
|
125
|
+
* `deriveSessionFromEvents` (telemetry/sync.mjs) returns a three-state verdict on
|
|
126
|
+
* the events ledger it read FROM: `true` (every rotation archive present),
|
|
127
|
+
* `false` (at least one measured gap — a deleted/missing archive), and `null`
|
|
128
|
+
* (no verdict was produced at all: the read threw, or there was no source). That
|
|
129
|
+
* verdict decided nothing on the wire, so a derived record reconstructed from a
|
|
130
|
+
* holey ledger was indistinguishable from one reconstructed from a whole one.
|
|
131
|
+
*
|
|
132
|
+
* Why a boolean and not a `derived-partial` token: `SESSION_RECORD_SOURCES` is a
|
|
133
|
+
* frozen enum mirrored in prose and in the server's `raw_json` consumers, and
|
|
134
|
+
* completeness is a property of the SOURCE READ, not of WHICH source was used —
|
|
135
|
+
* widening the provenance enum would make the two facts inseparable and force
|
|
136
|
+
* every reader of `session_record === 'derived'` to learn a second spelling.
|
|
137
|
+
*
|
|
138
|
+
* THE THIRD STATE IS OMISSION: only a real boolean is written. `null` (nothing
|
|
139
|
+
* measured) omits the key, exactly like `session_profile` — "we did not look" is
|
|
140
|
+
* never sent as a measured `false`. On the `ledger` branch the field is not set
|
|
141
|
+
* at ALL: that path never reads the events ledger, so it has no verdict to give.
|
|
142
|
+
*/
|
|
143
|
+
|
|
120
144
|
/** Exact duration-bucket tokens (ASCII, stable wire values). */
|
|
121
145
|
export const DURATION_BUCKETS = Object.freeze(['<15m', '15-60m', '1-3h', '>3h']);
|
|
122
146
|
|
|
@@ -541,6 +565,7 @@ export function buildUsagePing({
|
|
|
541
565
|
consentState,
|
|
542
566
|
sessionRecordSource,
|
|
543
567
|
sessionProfile,
|
|
568
|
+
ledgerComplete,
|
|
544
569
|
} = {}) {
|
|
545
570
|
const session = isPlainObject(sessionRecord) ? sessionRecord : {};
|
|
546
571
|
const invocations = Array.isArray(skillInvocations) ? skillInvocations : [];
|
|
@@ -637,6 +662,11 @@ export function buildUsagePing({
|
|
|
637
662
|
session_record: SESSION_RECORD_SOURCES.includes(sessionRecordSource)
|
|
638
663
|
? sessionRecordSource
|
|
639
664
|
: 'absent',
|
|
665
|
+
// ONLY a real boolean travels (#1416). `null`/`undefined` — the read never
|
|
666
|
+
// happened, or this path never read the events ledger at all — OMITS the
|
|
667
|
+
// key, the same absent-is-not-empty contract `session_profile` uses above.
|
|
668
|
+
// Writing `false` there would turn "we did not look" into a measured gap.
|
|
669
|
+
...(typeof ledgerComplete === 'boolean' ? { ledger_complete: ledgerComplete } : {}),
|
|
640
670
|
skills: filterRosterNames(skillNames, rosterSkills),
|
|
641
671
|
commands: filterRosterNames(commandNames, rosterCommands),
|
|
642
672
|
};
|
|
@@ -41,6 +41,7 @@ import { ensureAnonId } from './anon-id.mjs';
|
|
|
41
41
|
import { peekAll, enqueue, clear, queueStats } from './queue.mjs';
|
|
42
42
|
import { loadOwnerConfig } from '../owner-yaml.mjs';
|
|
43
43
|
import { readJsonlFile } from '../io.mjs';
|
|
44
|
+
import { readEventsWithRotations } from '../events.mjs';
|
|
44
45
|
import { readCanonicalSessions } from '../sessions-canonical.mjs';
|
|
45
46
|
import { resolvePrivateConfigDir } from '../config/private-config-dir.mjs';
|
|
46
47
|
import { readSessionProfile } from '../state-md.mjs';
|
|
@@ -338,12 +339,54 @@ export function readSessionProfileForMetricsDir(metricsDir) {
|
|
|
338
339
|
* Never throws.
|
|
339
340
|
*
|
|
340
341
|
* @param {string} metricsDir
|
|
341
|
-
* @returns {{ session: object, source: 'derived'|'absent'
|
|
342
|
+
* @returns {{ session: object, source: 'derived'|'absent',
|
|
343
|
+
* ledger_complete: boolean|null, ledger_gaps: object[] }}
|
|
344
|
+
* `ledger_complete` is `null` — never `false` — when the read produced no
|
|
345
|
+
* verdict at all: an undetermined ledger is not a proven gap. TWO paths reach
|
|
346
|
+
* that third state (#1423): this function throwing, and the reader finding NO
|
|
347
|
+
* source (no active file, no archive, no legacy ring) — in the latter case
|
|
348
|
+
* `events: []` is the absence of a ledger, not a measured quiet window.
|
|
349
|
+
* `ledger_complete === false` additionally writes ONE stderr line naming the
|
|
350
|
+
* gap kinds, because a verdict nothing surfaces is a verdict nobody acts on.
|
|
342
351
|
*/
|
|
343
352
|
export function deriveSessionFromEvents(metricsDir) {
|
|
344
353
|
try {
|
|
345
|
-
|
|
346
|
-
|
|
354
|
+
// #1407: read ACROSS rotation boundaries. This reconstruction is the ONLY
|
|
355
|
+
// source of session facts whenever `sessions.jsonl` has no usable record,
|
|
356
|
+
// and its sibling window is `DAILY_FLUSH_MS` (24 h) — so a rotation between
|
|
357
|
+
// a session's start and its flush silently truncated it.
|
|
358
|
+
//
|
|
359
|
+
// CEILING (BV-004): the reader loads the active file AND every archive
|
|
360
|
+
// fully into memory — up to ~60 MB transient at the default
|
|
361
|
+
// `max-size-mb: 10` / `max-backups: 5`. Measured 2026-09-20 A/B in ONE
|
|
362
|
+
// process on exactly that worst case (60 MB / 6 sources / 293,994 records,
|
|
363
|
+
// host loadavg 11.2): median 25.5 ms → 215.8 ms, i.e. +190 ms, which is
|
|
364
|
+
// 6.4 % of this path's `POST_TIMEOUT_MS` (3000). Acceptable for a
|
|
365
|
+
// once-per-flush cold read. Revisit if `max-size-mb` is raised past ~100,
|
|
366
|
+
// if `max-backups` grows, or if the flush budget drops below ~1 s.
|
|
367
|
+
const ledger = readEventsWithRotations(undefined, {
|
|
368
|
+
filePath: path.join(metricsDir, 'events.jsonl'),
|
|
369
|
+
});
|
|
370
|
+
const events = ledger.events;
|
|
371
|
+
// A missing archive is a GAP, never an empty window (#1407 AC-3). Carried
|
|
372
|
+
// on EVERY return below, including the empty ones — that is precisely where
|
|
373
|
+
// a deleted archive and a quiet period are otherwise indistinguishable.
|
|
374
|
+
const ledgerVerdict = { ledger_complete: ledger.complete, ledger_gaps: ledger.gaps };
|
|
375
|
+
// Visibility (#1423): the field had ZERO production consumers — a measured
|
|
376
|
+
// gap reached nobody. ONE stderr line, at the single point the verdict is
|
|
377
|
+
// formed, so every return below is covered by construction. Diagnostics on
|
|
378
|
+
// stderr, never stdout (cli-design.md); no new event, because the gap is a
|
|
379
|
+
// property of the LEDGER and an event about it would land in the same file.
|
|
380
|
+
if (ledger.complete === false) {
|
|
381
|
+
const kinds = [...new Set((ledger.gaps ?? []).map((g) => g?.kind ?? 'unknown'))].sort();
|
|
382
|
+
process.stderr.write(
|
|
383
|
+
`⚠ telemetry: events ledger INCOMPLETE — ${ledger.gaps.length} gap(s) ` +
|
|
384
|
+
`[${kinds.join(', ')}]; the session facts below are reconstructed from a PARTIAL read\n`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
388
|
+
return { session: {}, source: 'absent', ...ledgerVerdict };
|
|
389
|
+
}
|
|
347
390
|
|
|
348
391
|
let startedAt = null;
|
|
349
392
|
let sessionType = null;
|
|
@@ -412,7 +455,9 @@ export function deriveSessionFromEvents(metricsDir) {
|
|
|
412
455
|
profile = undatedShapeProfile;
|
|
413
456
|
}
|
|
414
457
|
|
|
415
|
-
if (startedAt === null && sessionType === null && profile === null)
|
|
458
|
+
if (startedAt === null && sessionType === null && profile === null) {
|
|
459
|
+
return { session: {}, source: 'absent', ...ledgerVerdict };
|
|
460
|
+
}
|
|
416
461
|
|
|
417
462
|
const session = {};
|
|
418
463
|
if (sessionType !== null) session.session_type = sessionType;
|
|
@@ -422,9 +467,11 @@ export function deriveSessionFromEvents(metricsDir) {
|
|
|
422
467
|
// omit-never-fabricate contract session-close-backfill.mjs uses (#914 R1).
|
|
423
468
|
if (lastTs !== null && startedAt !== null && lastTs >= startedAt) session.completed_at = lastTs;
|
|
424
469
|
|
|
425
|
-
return { session, source: 'derived' };
|
|
470
|
+
return { session, source: 'derived', ...ledgerVerdict };
|
|
426
471
|
} catch {
|
|
427
|
-
|
|
472
|
+
// No verdict was ever produced — `null`, not `false`: a measurement that
|
|
473
|
+
// did not happen is not a measured zero.
|
|
474
|
+
return { session: {}, source: 'absent', ledger_complete: null, ledger_gaps: [] };
|
|
428
475
|
}
|
|
429
476
|
}
|
|
430
477
|
|
|
@@ -483,6 +530,12 @@ export function buildBatch({
|
|
|
483
530
|
let sessionForPing;
|
|
484
531
|
/** @type {'ledger'|'derived'|'absent'} */
|
|
485
532
|
let sessionRecordSource = 'ledger';
|
|
533
|
+
// #1416: the events-ledger completeness verdict, carried to the ping as its
|
|
534
|
+
// own optional boolean. Stays `null` on the LEDGER branch by construction —
|
|
535
|
+
// that branch never reads events.jsonl, so it has nothing to report, and
|
|
536
|
+
// `buildUsagePing` omits the key for anything that is not a real boolean.
|
|
537
|
+
/** @type {boolean|null} */
|
|
538
|
+
let ledgerComplete = null;
|
|
486
539
|
if (sessionRecord && typeof sessionRecord.started_at === 'string' && !Number.isNaN(Date.parse(sessionRecord.started_at))) {
|
|
487
540
|
const startMs = Date.parse(sessionRecord.started_at);
|
|
488
541
|
windowInvocations = invocations.filter((rec) => {
|
|
@@ -500,6 +553,7 @@ export function buildBatch({
|
|
|
500
553
|
const derived = deriveSessionFromEvents(dir);
|
|
501
554
|
sessionForPing = derived.session;
|
|
502
555
|
sessionRecordSource = derived.source;
|
|
556
|
+
ledgerComplete = derived.ledger_complete;
|
|
503
557
|
|
|
504
558
|
const derivedStartMs = Date.parse(sessionForPing.started_at);
|
|
505
559
|
const cutoff = (Number.isNaN(Date.parse(nowIso)) ? Date.now() : Date.parse(nowIso)) - DAILY_FLUSH_MS;
|
|
@@ -522,6 +576,7 @@ export function buildBatch({
|
|
|
522
576
|
consentState,
|
|
523
577
|
sessionRecordSource,
|
|
524
578
|
sessionProfile: readSessionProfileForMetricsDir(dir),
|
|
579
|
+
ledgerComplete,
|
|
525
580
|
});
|
|
526
581
|
|
|
527
582
|
const target = statePath || TELEMETRY_JSON_PATH;
|
|
@@ -31,9 +31,11 @@
|
|
|
31
31
|
* @module scripts/lib/telemetry-flush-health-banner
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import { existsSync
|
|
34
|
+
import { existsSync } from 'node:fs';
|
|
35
35
|
import path from 'node:path';
|
|
36
36
|
|
|
37
|
+
import { readTailWindow } from './tail-window.mjs';
|
|
38
|
+
|
|
37
39
|
/**
|
|
38
40
|
* Bytes of `events.jsonl` read from the END of the file.
|
|
39
41
|
*
|
|
@@ -82,31 +84,11 @@ function sanitizeReason(reason) {
|
|
|
82
84
|
* @returns {{text: string} | {missing: true} | {error: string}}
|
|
83
85
|
*/
|
|
84
86
|
function readTail(file) {
|
|
85
|
-
let fd;
|
|
86
87
|
try {
|
|
87
|
-
|
|
88
|
-
const { size } = fstatSync(fd);
|
|
89
|
-
const length = Math.min(size, TAIL_BYTES);
|
|
90
|
-
const start = size - length;
|
|
91
|
-
const buf = Buffer.allocUnsafe(length);
|
|
92
|
-
let read = 0;
|
|
93
|
-
while (read < length) {
|
|
94
|
-
const n = readSync(fd, buf, read, length - read, start + read);
|
|
95
|
-
if (n <= 0) break;
|
|
96
|
-
read += n;
|
|
97
|
-
}
|
|
98
|
-
return { text: buf.subarray(0, read).toString('utf8') };
|
|
88
|
+
return { text: readTailWindow(file, TAIL_BYTES).text };
|
|
99
89
|
} catch (err) {
|
|
100
90
|
if (err?.code === 'ENOENT') return { missing: true };
|
|
101
91
|
return { error: typeof err?.code === 'string' ? err.code : 'EUNKNOWN' };
|
|
102
|
-
} finally {
|
|
103
|
-
if (fd !== undefined) {
|
|
104
|
-
try {
|
|
105
|
-
closeSync(fd);
|
|
106
|
-
} catch {
|
|
107
|
-
/* best effort */
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
92
|
}
|
|
111
93
|
}
|
|
112
94
|
|
|
@@ -22,10 +22,17 @@
|
|
|
22
22
|
* triageDecision(finding, candidates)
|
|
23
23
|
* → {action: 'create'|'update'|'ignore', target?, reason: string, confidence: number}
|
|
24
24
|
*
|
|
25
|
+
* buildIssueBody(finding, fp) → string
|
|
26
|
+
*
|
|
27
|
+
* neutralizeFingerprintSentinel(text) → string
|
|
28
|
+
* fingerprintSentinelLine(fp) → string
|
|
29
|
+
* The one shared sentinel writer + neutraliser, also used by
|
|
30
|
+
* ux-grill/reconcile.mjs (#1339 P6).
|
|
31
|
+
*
|
|
25
32
|
* ReconcileError
|
|
26
33
|
*
|
|
27
34
|
* Security:
|
|
28
|
-
* - #388 (SEC-IR-MED-1): sentinel injection hardening —
|
|
35
|
+
* - #388 (SEC-IR-MED-1): sentinel injection hardening — neutralizeFingerprintSentinel()
|
|
29
36
|
* strips **Fingerprint:** literals from free-text fields before they are
|
|
30
37
|
* embedded in the body (before the authoritative sentinel line is appended).
|
|
31
38
|
* - #389 (SEC-IR-LOW-1): maxBuffer set to 4 MB on every execFile call;
|
|
@@ -126,39 +133,64 @@ function validateFinding(finding) {
|
|
|
126
133
|
// ---------------------------------------------------------------------------
|
|
127
134
|
|
|
128
135
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
136
|
+
* Neutralise every `**Fingerprint:**` literal in untrusted text by rewriting it
|
|
137
|
+
* to `__Fingerprint__`. The ONE copy of this neutralisation: this module's
|
|
138
|
+
* {@link buildIssueBody} and `ux-grill/reconcile.mjs` both route free text
|
|
139
|
+
* through it (#1339 P6), so a hardening added here reaches both producers.
|
|
133
140
|
*
|
|
134
|
-
* Context: glab parses issue bodies as Markdown. If
|
|
141
|
+
* Context: glab parses issue bodies as Markdown. If a free-text field contained
|
|
135
142
|
* `**Fingerprint:** <attacker-hash>`, a grep for the fingerprint sentinel in
|
|
136
143
|
* listExistingFindings() would match the spoofed value instead of the real one,
|
|
137
144
|
* bypassing dedup. Replacing the literal prevents the attack without altering
|
|
138
|
-
* the semantic meaning of the
|
|
145
|
+
* the semantic meaning of the text.
|
|
146
|
+
*
|
|
147
|
+
* @param {unknown} text - coerced with `String(text ?? '')`
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
export function neutralizeFingerprintSentinel(text) {
|
|
151
|
+
// gi flag: case-insensitive match so **fingerprint:** and **FINGERPRINT:** variants
|
|
152
|
+
// are also neutralized. Sanitizer is intentionally broader than the case-sensitive
|
|
153
|
+
// extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
|
|
154
|
+
return String(text ?? '').replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The authoritative dedup sentinel line, in the exact shape
|
|
159
|
+
* {@link extractFingerprintFromBody} reads. The caller passes an
|
|
160
|
+
* already-trusted value; this function does not sanitise it.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} fp - 16-char hex fingerprint
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
export function fingerprintSentinelLine(fp) {
|
|
166
|
+
return `**Fingerprint:** \`${fp}\``;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Sanitize free-text recommendation fields before embedding in the issue body
|
|
171
|
+
* (#388 SEC-IR-MED-1) — {@link neutralizeFingerprintSentinel} for a non-empty
|
|
172
|
+
* string, anything else returned unchanged (a `null` description is dropped by
|
|
173
|
+
* buildIssueBody's line filter rather than rendered as an empty line).
|
|
139
174
|
*
|
|
140
175
|
* @param {string|undefined|null} text
|
|
141
176
|
* @returns {string|undefined|null}
|
|
142
177
|
*/
|
|
143
178
|
function sanitizeRecommendation(text) {
|
|
144
179
|
if (!text || typeof text !== 'string') return text;
|
|
145
|
-
|
|
146
|
-
// are also neutralized. Sanitizer is intentionally broader than the case-sensitive
|
|
147
|
-
// extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
|
|
148
|
-
return text.replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
|
|
180
|
+
return neutralizeFingerprintSentinel(text);
|
|
149
181
|
}
|
|
150
182
|
|
|
151
183
|
/**
|
|
152
184
|
* Render untrusted text as the content of ONE inline Markdown code span:
|
|
153
|
-
* sentinel literals neutralised
|
|
154
|
-
*
|
|
155
|
-
*
|
|
185
|
+
* sentinel literals neutralised, line breaks and NUL folded to a space,
|
|
186
|
+
* backticks turned into `'` — so a page-controlled locator can neither close
|
|
187
|
+
* its code span nor start a new body line.
|
|
156
188
|
*
|
|
157
189
|
* @param {unknown} text
|
|
158
190
|
* @returns {string}
|
|
159
191
|
*/
|
|
160
192
|
function codeSpanContent(text) {
|
|
161
|
-
return
|
|
193
|
+
return neutralizeFingerprintSentinel(text)
|
|
162
194
|
.replace(/[\r\n\0]+/g, ' ')
|
|
163
195
|
.replace(/`/g, "'");
|
|
164
196
|
}
|
|
@@ -218,7 +250,7 @@ export function buildIssueBody(finding, fp) {
|
|
|
218
250
|
'',
|
|
219
251
|
safeRecommendation ? `**Recommendation:** ${safeRecommendation}` : null,
|
|
220
252
|
'',
|
|
221
|
-
|
|
253
|
+
fingerprintSentinelLine(fp),
|
|
222
254
|
`**Severity:** ${finding.severity}`,
|
|
223
255
|
`**Check:** ${finding.checkId}`,
|
|
224
256
|
`**Locator:** \`${codeSpanContent(finding.locator)}\``,
|