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
|
@@ -531,15 +531,23 @@ function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'aban
|
|
|
531
531
|
const BACKFILL_STUB_STATUSES = new Set(['abandoned']);
|
|
532
532
|
|
|
533
533
|
/**
|
|
534
|
-
* True when a sessions.jsonl record is a backfilled
|
|
535
|
-
* provenance (`_backfill_source`) AND a stub status.
|
|
536
|
-
* hand-written `abandoned` record with no backfill
|
|
537
|
-
* deliberate statement and is never superseded on our
|
|
534
|
+
* True when a sessions.jsonl record is a backfilled stub this module may
|
|
535
|
+
* SUPERSEDE — reconstructed provenance (`_backfill_source`) AND a stub status.
|
|
536
|
+
* Both are required: a hand-written `abandoned` record with no backfill
|
|
537
|
+
* provenance is somebody's deliberate statement and is never superseded on our
|
|
538
|
+
* own initiative.
|
|
539
|
+
*
|
|
540
|
+
* Named for the decision it makes, not for the record class, because
|
|
541
|
+
* `sessions-staleness-banner.mjs` asks a DIFFERENT question about the same
|
|
542
|
+
* records — `isNonAnchorStub()` there accepts EITHER marker, since a record
|
|
543
|
+
* only needs to be doubtful to be disqualified as a time anchor, where this
|
|
544
|
+
* one must be proven reconstructed before it may be overwritten. Both are
|
|
545
|
+
* module-local on purpose; they must not be shared.
|
|
538
546
|
*
|
|
539
547
|
* @param {unknown} record
|
|
540
548
|
* @returns {boolean}
|
|
541
549
|
*/
|
|
542
|
-
function
|
|
550
|
+
function isSupersedableStub(record) {
|
|
543
551
|
if (!record || typeof record !== 'object' || Array.isArray(record)) return false;
|
|
544
552
|
if (typeof record._backfill_source !== 'string') return false;
|
|
545
553
|
return BACKFILL_STUB_STATUSES.has(record.status);
|
|
@@ -636,7 +644,7 @@ function classifyExisting(readFileSync, sessionsPath, { recordId, sessionId, sem
|
|
|
636
644
|
const sessionRecords = readJsonlSafe(readFileSync, sessionsPath);
|
|
637
645
|
const newest = findRecordedSession(sessionRecords, { sessionId, semanticSessionId, startedAt });
|
|
638
646
|
if (!newest) return { kind: 'absent' };
|
|
639
|
-
if (
|
|
647
|
+
if (isSupersedableStub(newest)) {
|
|
640
648
|
return { kind: 'stub', matchedId: newest.session_id, stubId: newest.session_id };
|
|
641
649
|
}
|
|
642
650
|
return { kind: 'canonical', matchedId: newest.session_id };
|
|
@@ -1034,6 +1042,9 @@ export async function backfillAbandonedSession({
|
|
|
1034
1042
|
* { action: 'skipped-not-completed', status } — STATE.md status isn't 'completed'
|
|
1035
1043
|
* { action: 'skipped-no-session-id' } — completed but no `session:` field
|
|
1036
1044
|
* { action: 'skipped-already-recorded', sessionId } — sessions.jsonl already has it
|
|
1045
|
+
* { action: 'skipped-key-occupied', sessionId } — identity absent, but a canonical
|
|
1046
|
+
* non-stub record of a DIFFERENT session
|
|
1047
|
+
* already holds this key (#1388 P8)
|
|
1037
1048
|
* { action: 'skipped-marker-exists', sessionId } — lost the TOCTOU claim
|
|
1038
1049
|
* { action: 'error', error, sessionId? } — any failure, swallowed
|
|
1039
1050
|
*
|
|
@@ -1118,6 +1129,22 @@ export async function backfillCompletedFromStateMd({
|
|
|
1118
1129
|
if (existing.kind === 'canonical') {
|
|
1119
1130
|
return { action: 'skipped-already-recorded', sessionId: existing.matchedId };
|
|
1120
1131
|
}
|
|
1132
|
+
// Key-occupancy guard (#1380 P6) — the counterpart of the abandoned
|
|
1133
|
+
// path's occupied-key check. Identity can say `absent` (label drift
|
|
1134
|
+
// veto, conflicting native join) while a canonical, authoritative record
|
|
1135
|
+
// already holds `recordId`; canonical readers collapse by session_id
|
|
1136
|
+
// newest-wins, so appending here would displace that record with this
|
|
1137
|
+
// zero-counter one. `recordId` is already the native id whenever STATE.md
|
|
1138
|
+
// carries one, so there is no alternate key to fall back to: skip.
|
|
1139
|
+
if (existing.kind === 'absent'
|
|
1140
|
+
&& canonicalizeSessions(readJsonlSafe(readFileSync, sessionsPath))
|
|
1141
|
+
.some((record) => record?.session_id === recordId && !isSupersedableStub(record))) {
|
|
1142
|
+
// Distinct action (#1388 P8): `skipped-already-recorded` above means
|
|
1143
|
+
// "this very session is already on file"; here identity said ABSENT and
|
|
1144
|
+
// a DIFFERENT session's record occupies the key. Sharing one string made
|
|
1145
|
+
// the two causes indistinguishable in the backfill_completed telemetry.
|
|
1146
|
+
return { action: 'skipped-key-occupied', sessionId: recordId };
|
|
1147
|
+
}
|
|
1121
1148
|
const supersedes = existing.kind === 'stub' ? existing.stubId : null;
|
|
1122
1149
|
// Preserve a matched stub's key for its append-only replacement. Without
|
|
1123
1150
|
// event timestamps or a raw join on a legacy stub, a new UUID key would
|
|
@@ -39,10 +39,11 @@
|
|
|
39
39
|
* - Production code is silent: no console.log, no console.warn.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
import {
|
|
42
|
+
import { readFile } from 'node:fs/promises';
|
|
43
43
|
import path from 'node:path';
|
|
44
44
|
|
|
45
45
|
import { readCanonicalSessions } from './sessions-canonical.mjs';
|
|
46
|
+
import { readTailWindow } from './tail-window.mjs';
|
|
46
47
|
import { withStateMdLock } from './session-lock.mjs';
|
|
47
48
|
import { resolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
|
|
48
49
|
import { parseStateMd } from './state-md/yaml-parser.mjs';
|
|
@@ -208,31 +209,19 @@ const EVENTS_MAX_AGE_MS = 2 * 24 * 60 * 60 * 1000;
|
|
|
208
209
|
async function readSessionIdsFromEvents(repoRoot) {
|
|
209
210
|
const filePath = path.join(repoRoot, '.orchestrator', 'metrics', 'events.jsonl');
|
|
210
211
|
|
|
211
|
-
let
|
|
212
|
-
// Assigned in the try below; every path that reaches its read site has
|
|
213
|
-
// passed through that assignment (the catch returns early).
|
|
214
|
-
let windowIsPartial;
|
|
215
|
-
let handle = null;
|
|
212
|
+
let window;
|
|
216
213
|
try {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const length = size - start;
|
|
222
|
-
if (length > 0) {
|
|
223
|
-
const buf = Buffer.alloc(length);
|
|
224
|
-
const { bytesRead } = await handle.read(buf, 0, length, start);
|
|
225
|
-
text = buf.subarray(0, bytesRead).toString('utf8');
|
|
226
|
-
}
|
|
214
|
+
// Sync read inside an async function: 1 MiB ceiling, one call per n-claim —
|
|
215
|
+
// the shared primitive (tail-window.mjs) owns the byte window, this reader
|
|
216
|
+
// keeps its own fail-open mapping.
|
|
217
|
+
window = readTailWindow(filePath, EVENTS_TAIL_BYTES);
|
|
227
218
|
} catch {
|
|
228
219
|
return [];
|
|
229
|
-
} finally {
|
|
230
|
-
if (handle !== null) await handle.close().catch(() => {});
|
|
231
220
|
}
|
|
232
221
|
|
|
233
|
-
const lines = text.split(/\r?\n/);
|
|
222
|
+
const lines = window.text.split(/\r?\n/);
|
|
234
223
|
// The window started mid-file, so line 0 may be a truncated record.
|
|
235
|
-
if (
|
|
224
|
+
if (window.cut) lines.shift();
|
|
236
225
|
|
|
237
226
|
const cutoff = Date.now() - EVENTS_MAX_AGE_MS;
|
|
238
227
|
const ids = [];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the leading /session mode independently of the user's task context.
|
|
3
|
+
* This is a text boundary, not a shell/option parser: quotes and command syntax
|
|
4
|
+
* in the context remain data. `unknown` is a ledger value, never a live mode.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} [argumentsText] Complete text following /session.
|
|
7
|
+
* @returns {{sessionType: string, context: string, profile?: string, invalidMode?: string}}
|
|
8
|
+
*/
|
|
9
|
+
export function resolveSessionInvocation(argumentsText = '') {
|
|
10
|
+
if (typeof argumentsText !== 'string') {
|
|
11
|
+
throw new TypeError('session invocation arguments must be a string');
|
|
12
|
+
}
|
|
13
|
+
const [_, mode, context = ''] = argumentsText.trimStart().match(/^(\S+)(?:\s+([\s\S]*))?$/u) ?? [];
|
|
14
|
+
if (mode === undefined) return { sessionType: 'deep', context: '' };
|
|
15
|
+
if (mode === 'ultradeep') return { sessionType: 'deep', profile: 'ultradeep', context };
|
|
16
|
+
if (['housekeeping', 'feature', 'deep'].includes(mode)) {
|
|
17
|
+
return { sessionType: mode, context };
|
|
18
|
+
}
|
|
19
|
+
return { sessionType: 'deep', context, invalidMode: mode };
|
|
20
|
+
}
|
|
@@ -36,11 +36,17 @@
|
|
|
36
36
|
* no key-order churn.
|
|
37
37
|
*
|
|
38
38
|
* ── PROVENANCE ───────────────────────────────────────────────────────────────
|
|
39
|
-
* Every repaired record carries `
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
39
|
+
* Every repaired record carries `_repair_source` (`REPAIR_SOURCE`) +
|
|
40
|
+
* `_backfill_incomplete_fields`; `_backfill_source` is set to `REPAIR_SOURCE`
|
|
41
|
+
* only when the record had no origin of its own. An existing origin
|
|
42
|
+
* (`state-md-completed`, `events-jsonl`, `abandoned`) is KEPT, because
|
|
43
|
+
* `sessions-canonical.mjs`'s stub guard reads it: overwriting it promoted a
|
|
44
|
+
* repaired zero-work stub to a real record that shadowed the real one (#1443
|
|
45
|
+
* F2). `_backfill_*` is schema-accepted by `_validateOptionalFields`, and
|
|
46
|
+
* `validateSession` ignores undeclared keys (as it does the `_<field>_raw`
|
|
47
|
+
* sidecars), so no schema bump is needed. The incomplete-fields list is the
|
|
48
|
+
* EXACT set of fields this record had defaulted, which is what lets a
|
|
49
|
+
* downstream consumer tell a measured zero apart from a repaired-to-zero.
|
|
44
50
|
*
|
|
45
51
|
* It also never DISCARDS what it replaces: any field whose present value is
|
|
46
52
|
* overwritten by a default is first copied to a `_<field>_raw` sidecar
|
|
@@ -70,7 +76,10 @@ import { checkSessionsIntegrity as defaultCheckIntegrity } from './sessions-inte
|
|
|
70
76
|
// Constants
|
|
71
77
|
// ---------------------------------------------------------------------------
|
|
72
78
|
|
|
73
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Value written to `_repair_source` on every record this module repairs, and to
|
|
81
|
+
* `_backfill_source` only on a record that had no origin there (#1443 F2).
|
|
82
|
+
*/
|
|
74
83
|
export const REPAIR_SOURCE = 'repair-invalid-sessions/1004';
|
|
75
84
|
|
|
76
85
|
/** Ledger path (relative to repoRoot) that `checkSessionsIntegrity` inspects. */
|
|
@@ -98,6 +107,28 @@ const INCOMPLETE_FIELD_ORDER = Object.freeze([
|
|
|
98
107
|
'total_agents',
|
|
99
108
|
'total_files_changed',
|
|
100
109
|
]);
|
|
110
|
+
// `timestamp_precision` (#1442) deliberately adds NOTHING to the list above: a
|
|
111
|
+
// truncated timestamp is still a measurement (exact to the millisecond), not a
|
|
112
|
+
// default. Listing it would make consumers discard a real value —
|
|
113
|
+
// `sessions-staleness-banner.mjs` returns null for any timestamp named in
|
|
114
|
+
// `_backfill_incomplete_fields`, `session-close-backfill.mjs` keys on
|
|
115
|
+
// `includes('started_at')`. The sub-millisecond original survives in the
|
|
116
|
+
// `_<field>_raw` sidecar instead.
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Timestamp fields `validateSession` holds to `ISO_8601_UTC_MS_RE`
|
|
120
|
+
* (`session-schema/validator.mjs`, not exported): exactly three fractional
|
|
121
|
+
* digits or none.
|
|
122
|
+
*/
|
|
123
|
+
const MS_PRECISION_FIELDS = Object.freeze(['started_at', 'completed_at', 'lease_acquired_at']);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* ISO-8601 UTC with MORE than three fractional digits (`.994600Z`) — the one
|
|
127
|
+
* rejected shape that has an unambiguous canonical form. Group 1 is the value
|
|
128
|
+
* cut at the millisecond. Offsets, `.3Z` and non-ISO strings do not match and
|
|
129
|
+
* stay for the validator to reject: there is no defensible default for them.
|
|
130
|
+
*/
|
|
131
|
+
const ISO_UTC_SUB_MS_RE = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3})\d+Z$/;
|
|
101
132
|
|
|
102
133
|
// ---------------------------------------------------------------------------
|
|
103
134
|
// Helpers
|
|
@@ -112,6 +143,20 @@ function isCount(v) {
|
|
|
112
143
|
return typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
113
144
|
}
|
|
114
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Cut a sub-millisecond ISO-8601 UTC timestamp to milliseconds. TRUNCATES,
|
|
148
|
+
* never rounds: `.9999Z` must not roll into the next second, and truncation is
|
|
149
|
+
* monotone, so `completed_at >= started_at` survives on every pair it held on.
|
|
150
|
+
*
|
|
151
|
+
* @param {unknown} value
|
|
152
|
+
* @returns {string|null} the truncated value, or null when there is nothing to cut
|
|
153
|
+
*/
|
|
154
|
+
function truncateToMs(value) {
|
|
155
|
+
if (typeof value !== 'string') return null;
|
|
156
|
+
const m = ISO_UTC_SUB_MS_RE.exec(value);
|
|
157
|
+
return m ? `${m[1]}Z` : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
115
160
|
function orderIncompleteFields(fields) {
|
|
116
161
|
const rank = (f) => {
|
|
117
162
|
const i = INCOMPLETE_FIELD_ORDER.indexOf(f);
|
|
@@ -321,6 +366,19 @@ export function repairRecord(record) {
|
|
|
321
366
|
incomplete.add('total_files_changed');
|
|
322
367
|
}
|
|
323
368
|
|
|
369
|
+
// -- timestamp precision (#1442) -------------------------------------------
|
|
370
|
+
// Runs BEFORE the completed_at default below, because that default copies
|
|
371
|
+
// `started_at` — a microsecond `started_at` would otherwise be copied into
|
|
372
|
+
// `completed_at` and spread the defect instead of fixing it.
|
|
373
|
+
let precisionCut = false;
|
|
374
|
+
for (const key of MS_PRECISION_FIELDS) {
|
|
375
|
+
const truncated = truncateToMs(out[key]);
|
|
376
|
+
if (truncated === null) continue;
|
|
377
|
+
preserveRaw(out, key, record[key], rescued);
|
|
378
|
+
out[key] = truncated;
|
|
379
|
+
precisionCut = true;
|
|
380
|
+
}
|
|
381
|
+
|
|
324
382
|
// -- completed_at ---------------------------------------------------------
|
|
325
383
|
// Legacy `ended_at` is the same fact under its old name (emit-session.mjs
|
|
326
384
|
// aliasLegacyEndedAt precedent) — prefer it when parseable and monotonic,
|
|
@@ -332,13 +390,19 @@ export function repairRecord(record) {
|
|
|
332
390
|
preserveRaw(out, 'completed_at', record.completed_at, rescued);
|
|
333
391
|
const endedMs = typeof out.ended_at === 'string' ? Date.parse(out.ended_at) : NaN;
|
|
334
392
|
const startedMs = Date.parse(out.started_at);
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
393
|
+
if (Number.isFinite(endedMs) && Number.isFinite(startedMs) && endedMs >= startedMs) {
|
|
394
|
+
// The COPY is cut to milliseconds; `ended_at` itself is not a validated
|
|
395
|
+
// field and keeps its original bytes, so nothing needs a sidecar here.
|
|
396
|
+
const endedTruncated = truncateToMs(out.ended_at);
|
|
397
|
+
if (endedTruncated !== null) precisionCut = true;
|
|
398
|
+
out.completed_at = endedTruncated ?? out.ended_at;
|
|
399
|
+
} else {
|
|
400
|
+
out.completed_at = out.started_at;
|
|
401
|
+
}
|
|
339
402
|
defects.push('completed_at_missing');
|
|
340
403
|
incomplete.add('completed_at');
|
|
341
404
|
}
|
|
405
|
+
if (precisionCut) defects.push('timestamp_precision');
|
|
342
406
|
|
|
343
407
|
if (defects.length === 0) {
|
|
344
408
|
return { record, defects: [], incompleteFields: [], changed: false };
|
|
@@ -349,7 +413,14 @@ export function repairRecord(record) {
|
|
|
349
413
|
? record._backfill_incomplete_fields.filter((f) => typeof f === 'string')
|
|
350
414
|
: [];
|
|
351
415
|
const merged = orderIncompleteFields(incomplete);
|
|
352
|
-
|
|
416
|
+
// The repair is recorded in its own field; `_backfill_source` keeps the
|
|
417
|
+
// record's ORIGIN. Overwriting a synthesised origin ('state-md-completed',
|
|
418
|
+
// 'events-jsonl', 'abandoned') made a repaired zero-work stub read as a real
|
|
419
|
+
// record, and it shadowed the real one for its id (#1443 F2).
|
|
420
|
+
out._repair_source = REPAIR_SOURCE;
|
|
421
|
+
if (typeof record._backfill_source !== 'string' || record._backfill_source.length === 0) {
|
|
422
|
+
out._backfill_source = REPAIR_SOURCE;
|
|
423
|
+
}
|
|
353
424
|
out._backfill_incomplete_fields = [...new Set([...prior, ...merged])];
|
|
354
425
|
|
|
355
426
|
return { record: out, defects, incompleteFields: merged, changed: true };
|
|
@@ -487,6 +558,11 @@ export function repairText(raw, deps = {}) {
|
|
|
487
558
|
repaired,
|
|
488
559
|
// Projected: every line that was invalid and did NOT get repaired.
|
|
489
560
|
invalid_after: invalidBefore - repaired,
|
|
561
|
+
// Lines with no defensible repair (`repairLine` status `error`). They are
|
|
562
|
+
// emitted byte-identically and are EXPECTED to stay invalid after a
|
|
563
|
+
// write — `verifyWritten` tolerates exactly these line numbers (#1442).
|
|
564
|
+
// Same population as `errors`, which carries the per-line detail.
|
|
565
|
+
unrepairable: errors.length,
|
|
490
566
|
duplicate_ids_observed: duplicates,
|
|
491
567
|
defects_by_class: defectsByClass,
|
|
492
568
|
errors,
|
|
@@ -507,9 +583,32 @@ export function repairText(raw, deps = {}) {
|
|
|
507
583
|
* any other target it is reported as skipped rather than quietly measuring the
|
|
508
584
|
* wrong file.
|
|
509
585
|
*
|
|
510
|
-
*
|
|
586
|
+
* ── WHAT "OK" MEANS (#1442) ──────────────────────────────────────────────────
|
|
587
|
+
* Not "zero invalid lines". A ledger that carries lines this module has no
|
|
588
|
+
* defensible repair for could never pass that bar, so one unrepairable record
|
|
589
|
+
* rolled back every repair beside it (all-or-nothing). `ok` now means NO
|
|
590
|
+
* WORSENING and ALL REPAIRABLE VALID:
|
|
591
|
+
* - every invalid line in the written file is one of `expected.unrepairableLines`
|
|
592
|
+
* (so every repaired AND every already-valid line validates), and
|
|
593
|
+
* - `invalid_after <= expected.invalidBefore`, and
|
|
594
|
+
* - the integrity probe flags no line it did not already flag in
|
|
595
|
+
* `expected.integrityBefore` (compared by line number — the repair never
|
|
596
|
+
* adds or removes lines, so positions are stable across the write).
|
|
597
|
+
* With no `expected`, the defaults reproduce the old strict bar: no invalid
|
|
598
|
+
* line tolerated, and a clean pre-write integrity baseline.
|
|
599
|
+
*
|
|
600
|
+
* NAMED CEILING (BV-004): integrity is compared per line, not per reason — a
|
|
601
|
+
* line flagged before AND after for a DIFFERENT reason is not "worse". The
|
|
602
|
+
* per-line `validateSession` pass above still catches it for the schema half.
|
|
603
|
+
*
|
|
604
|
+
* @param {object} args
|
|
605
|
+
* @param {object} [args.expected]
|
|
606
|
+
* @param {number[]} [args.expected.unrepairableLines] 1-based lines allowed to stay invalid
|
|
607
|
+
* @param {number} [args.expected.invalidBefore] invalid count before the write
|
|
608
|
+
* @param {object|null} [args.expected.integrityBefore] pre-write `checkIntegrity` banner (null = clean)
|
|
609
|
+
* @returns {{ok: boolean, invalid_after: number, invalid_lines: object[], unexpected_invalid: object[], integrity: string|object, integrity_worsened: boolean}}
|
|
511
610
|
*/
|
|
512
|
-
export function verifyWritten({ file, repoRoot, deps = {} }) {
|
|
611
|
+
export function verifyWritten({ file, repoRoot, deps = {}, expected = {} }) {
|
|
513
612
|
const {
|
|
514
613
|
readFileSync = fs.readFileSync,
|
|
515
614
|
validateSession = defaultValidateSession,
|
|
@@ -539,17 +638,50 @@ export function verifyWritten({ file, repoRoot, deps = {} }) {
|
|
|
539
638
|
}
|
|
540
639
|
}
|
|
541
640
|
|
|
641
|
+
const { unrepairableLines = [], invalidBefore = 0, integrityBefore = null } = expected;
|
|
642
|
+
const tolerated = new Set(unrepairableLines);
|
|
643
|
+
const unexpectedInvalid = invalidLines.filter((l) => !tolerated.has(l.line));
|
|
644
|
+
|
|
542
645
|
let integrity = 'skipped-not-canonical-path';
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
646
|
+
let worsened = false;
|
|
647
|
+
if (isCanonicalLedger(file, repoRoot)) {
|
|
648
|
+
const banner = checkIntegrity({ repoRoot });
|
|
649
|
+
integrity = banner === null ? 'clean' : banner;
|
|
650
|
+
worsened = integrityWorsened(integrityBefore, banner);
|
|
549
651
|
}
|
|
550
652
|
|
|
551
|
-
const ok =
|
|
552
|
-
return {
|
|
653
|
+
const ok = unexpectedInvalid.length === 0 && invalidLines.length <= invalidBefore && !worsened;
|
|
654
|
+
return {
|
|
655
|
+
ok,
|
|
656
|
+
invalid_after: invalidLines.length,
|
|
657
|
+
invalid_lines: invalidLines,
|
|
658
|
+
unexpected_invalid: unexpectedInvalid,
|
|
659
|
+
integrity,
|
|
660
|
+
integrity_worsened: worsened,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** True when `file` IS `<repoRoot>/.orchestrator/metrics/sessions.jsonl` — the only file `checkIntegrity` reads. */
|
|
665
|
+
function isCanonicalLedger(file, repoRoot) {
|
|
666
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0) return false;
|
|
667
|
+
return path.resolve(file) === path.resolve(path.join(repoRoot, CANONICAL_LEDGER_REL));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Did the integrity probe flag a line after the write that it did not flag
|
|
672
|
+
* before? Fail-safe on an unreadable banner shape: a non-null banner without
|
|
673
|
+
* its `schemaInvalid` / `mirrorSkipped` arrays counts as worse.
|
|
674
|
+
*/
|
|
675
|
+
function integrityWorsened(before, after) {
|
|
676
|
+
if (after === null || after === undefined) return false;
|
|
677
|
+
for (const key of ['schemaInvalid', 'mirrorSkipped']) {
|
|
678
|
+
const now = after && Array.isArray(after[key]) ? after[key] : null;
|
|
679
|
+
if (now === null) return true;
|
|
680
|
+
const prior = before && Array.isArray(before[key]) ? before[key] : [];
|
|
681
|
+
const priorLines = new Set(prior.map((e) => e?.line));
|
|
682
|
+
if (now.some((e) => !priorLines.has(e?.line))) return true;
|
|
683
|
+
}
|
|
684
|
+
return false;
|
|
553
685
|
}
|
|
554
686
|
|
|
555
687
|
/**
|
|
@@ -560,7 +692,10 @@ export function verifyWritten({ file, repoRoot, deps = {} }) {
|
|
|
560
692
|
* `<file>.tmp-<pid>` in the same directory and renames it over the target
|
|
561
693
|
* (atomic within a filesystem), then re-verifies the written file. A failed
|
|
562
694
|
* verification restores the backup byte-identically and reports `ok: false` —
|
|
563
|
-
* the caller maps that to exit 3.
|
|
695
|
+
* the caller maps that to exit 3. Lines with no defensible repair are kept
|
|
696
|
+
* byte-identical and counted in `summary.unrepairable`; they are NOT a
|
|
697
|
+
* verification failure (see `verifyWritten` § WHAT "OK" MEANS) — the caller
|
|
698
|
+
* maps an ok run with an unrepairable remainder to exit 4.
|
|
564
699
|
*
|
|
565
700
|
* Never throws for a defective RECORD (those land in `summary.errors`); a
|
|
566
701
|
* genuine I/O failure DOES throw and is the caller's exit-2 case.
|
|
@@ -584,6 +719,8 @@ export function repairLedger({ file, repoRoot = null, apply = false, backup = tr
|
|
|
584
719
|
existsSync = fs.existsSync,
|
|
585
720
|
} = deps;
|
|
586
721
|
|
|
722
|
+
const { checkIntegrity = defaultCheckIntegrity } = deps;
|
|
723
|
+
|
|
587
724
|
const raw = readFileSync(file, 'utf8');
|
|
588
725
|
const { text, summary: base } = repairText(raw, deps);
|
|
589
726
|
|
|
@@ -594,10 +731,17 @@ export function repairLedger({ file, repoRoot = null, apply = false, backup = tr
|
|
|
594
731
|
backup_path: null,
|
|
595
732
|
post_verify: null,
|
|
596
733
|
ok: true,
|
|
734
|
+
restored: false,
|
|
597
735
|
};
|
|
598
736
|
|
|
599
737
|
if (!apply) return summary;
|
|
600
738
|
|
|
739
|
+
// -- integrity baseline (#1442) --------------------------------------------
|
|
740
|
+
// Measured on the ORIGINAL bytes, before anything is written, so the
|
|
741
|
+
// post-write probe can be judged "not worse" instead of "perfectly clean" —
|
|
742
|
+
// unrepairable lines keep the banner non-null on both sides by construction.
|
|
743
|
+
const integrityBefore = isCanonicalLedger(file, repoRoot) ? checkIntegrity({ repoRoot }) : null;
|
|
744
|
+
|
|
601
745
|
// -- backup FIRST (unconditional unless explicitly opted out) --------------
|
|
602
746
|
let backupPath = null;
|
|
603
747
|
if (backup) {
|
|
@@ -621,14 +765,27 @@ export function repairLedger({ file, repoRoot = null, apply = false, backup = tr
|
|
|
621
765
|
}
|
|
622
766
|
|
|
623
767
|
// -- post-verification ----------------------------------------------------
|
|
624
|
-
const verdict = verifyWritten({
|
|
768
|
+
const verdict = verifyWritten({
|
|
769
|
+
file,
|
|
770
|
+
repoRoot,
|
|
771
|
+
deps,
|
|
772
|
+
expected: {
|
|
773
|
+
unrepairableLines: base.errors.map((e) => e.line),
|
|
774
|
+
invalidBefore: base.invalid_before,
|
|
775
|
+
integrityBefore,
|
|
776
|
+
},
|
|
777
|
+
});
|
|
625
778
|
summary.post_verify = verdict;
|
|
626
779
|
summary.invalid_after = verdict.invalid_after;
|
|
627
780
|
|
|
628
781
|
if (!verdict.ok) {
|
|
629
|
-
// Restore byte-identically
|
|
630
|
-
//
|
|
631
|
-
//
|
|
782
|
+
// Restore byte-identically — ONLY for a repaired (or previously valid) line
|
|
783
|
+
// that fails verification, or an integrity probe that got worse. An
|
|
784
|
+
// unrepairable remainder alone is not a failure (#1442): those lines were
|
|
785
|
+
// emitted byte-identically and keep the ledger exactly as broken as it was.
|
|
786
|
+
// The backup is preferred (it is the on-disk artefact an operator can
|
|
787
|
+
// inspect); `raw` is the in-memory fallback for `--no-backup`, and both are
|
|
788
|
+
// the same bytes.
|
|
632
789
|
if (backupPath) {
|
|
633
790
|
copyFileSync(backupPath, file);
|
|
634
791
|
} else {
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* {
|
|
18
18
|
* session_id, pid, platform, repo_path_hash, repo_name, branch,
|
|
19
19
|
* started_at, last_heartbeat, status ('active'|'wave'|'idle'),
|
|
20
|
-
* current_wave, host_class, mode (Epic #583 W2-I3)
|
|
20
|
+
* current_wave, host_class, mode (Epic #583 W2-I3),
|
|
21
|
+
* role ('navigator'|null, optional, #1462 — display only, never ownership;
|
|
22
|
+
* set by heartbeat(patch.role), NOT by registerSelf)
|
|
21
23
|
* }
|
|
22
24
|
*
|
|
23
25
|
* Schema v2 (Epic #583, W2-I3): `mode` field added so the exclusivity-matrix
|
|
@@ -142,6 +144,14 @@ function _validEntry(obj) {
|
|
|
142
144
|
if ('mode' in obj && obj.mode !== null && typeof obj.mode !== 'string') {
|
|
143
145
|
return false;
|
|
144
146
|
}
|
|
147
|
+
// #1462: `role` is optional and a DISPLAY field only — the registry shows who
|
|
148
|
+
// claims to be navigator, it never decides it. The authoritative claim is the
|
|
149
|
+
// navigator lease under `~/.config/navigator/leases/navigator.json`, and
|
|
150
|
+
// classifyMode ignores `role`. When present it MUST be null or a string (no
|
|
151
|
+
// number / object smuggling); absent is accepted like a v1 `mode`.
|
|
152
|
+
if ('role' in obj && obj.role !== null && typeof obj.role !== 'string') {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
145
155
|
return true;
|
|
146
156
|
}
|
|
147
157
|
|
|
@@ -236,11 +246,25 @@ export async function registerSelf(opts) {
|
|
|
236
246
|
return entry;
|
|
237
247
|
}
|
|
238
248
|
|
|
249
|
+
/** Roles `heartbeat(id, { role })` accepts besides `null` (#1462). */
|
|
250
|
+
export const REGISTRY_ROLES = Object.freeze(['navigator']);
|
|
251
|
+
|
|
239
252
|
/**
|
|
240
253
|
* Refresh last_heartbeat on the existing entry (and optionally status /
|
|
241
|
-
* current_wave). No-op if the entry is missing.
|
|
254
|
+
* current_wave / role). No-op if the entry is missing.
|
|
255
|
+
*
|
|
256
|
+
* `patch.role`: `undefined` keeps the stored role, `null` clears it, a member
|
|
257
|
+
* of `REGISTRY_ROLES` sets it. Anything else throws before the file is touched.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} sessionId
|
|
260
|
+
* @param {{ status?: string, currentWave?: number, role?: string|null }} [patch]
|
|
261
|
+
* @returns {Promise<object|null>} the updated entry, or null if none exists.
|
|
262
|
+
* @throws {TypeError} when `patch.role` is neither undefined, null, nor a REGISTRY_ROLES member.
|
|
242
263
|
*/
|
|
243
264
|
export async function heartbeat(sessionId, patch = {}) {
|
|
265
|
+
if (patch.role !== undefined && patch.role !== null && !REGISTRY_ROLES.includes(patch.role)) {
|
|
266
|
+
throw new TypeError('heartbeat: role must be "navigator" or null');
|
|
267
|
+
}
|
|
244
268
|
const file = entryPath(sessionId);
|
|
245
269
|
const existing = await _readJsonSafe(file);
|
|
246
270
|
if (!_validEntry(existing)) return null;
|
|
@@ -249,6 +273,7 @@ export async function heartbeat(sessionId, patch = {}) {
|
|
|
249
273
|
last_heartbeat: utcTimestamp(),
|
|
250
274
|
...(patch.status !== undefined ? { status: patch.status } : {}),
|
|
251
275
|
...(patch.currentWave !== undefined ? { current_wave: patch.currentWave } : {}),
|
|
276
|
+
...(patch.role !== undefined ? { role: patch.role } : {}),
|
|
252
277
|
};
|
|
253
278
|
await _writeJsonAtomic(file, updated);
|
|
254
279
|
return updated;
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Leaf module — no imports from siblings or parent.
|
|
6
6
|
*
|
|
7
7
|
* Exports: CURRENT_SESSION_SCHEMA_VERSION, SESSION_KEY_ALIASES,
|
|
8
|
-
* VALID_SESSION_TYPES, VALID_SESSION_PROFILES,
|
|
9
|
-
* AGENT_SUMMARY_FIELDS
|
|
8
|
+
* WAVE_KEY_ALIASES, VALID_SESSION_TYPES, VALID_SESSION_PROFILES,
|
|
9
|
+
* REQUIRED_FIELDS, AGENT_SUMMARY_FIELDS, OPTIONAL_FIELDS
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -53,6 +53,24 @@ export const SESSION_KEY_ALIASES = Object.freeze({
|
|
|
53
53
|
isolation_override: 'isolation',
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Safe per-wave key aliases (legacy -> canonical) — the `waves[]` counterpart of
|
|
58
|
+
* SESSION_KEY_ALIASES, same-shape renames only. Applied by normalizeWaveKeys
|
|
59
|
+
* (normalizer.mjs) on read AND by scripts/emit-session.mjs on write (#1390 P1).
|
|
60
|
+
*
|
|
61
|
+
* The canonical spelling is `agent_count_*` (skills/session-end/
|
|
62
|
+
* metrics-collection.md). The ledger carried 23 sessions in that spelling and
|
|
63
|
+
* exactly one, `main-2026-09-18-session-1` (5 waves), in the de-underscored
|
|
64
|
+
* `agents_*` spelling (measured 2026-09-19 @ 8f6ac022 over
|
|
65
|
+
* .orchestrator/metrics/sessions.jsonl) — a consumer reading
|
|
66
|
+
* `w.agent_count_completed` got `undefined` for every one of its waves.
|
|
67
|
+
*/
|
|
68
|
+
export const WAVE_KEY_ALIASES = Object.freeze({
|
|
69
|
+
agents_planned: 'agent_count_planned',
|
|
70
|
+
agents_started: 'agent_count_started',
|
|
71
|
+
agents_completed: 'agent_count_completed',
|
|
72
|
+
});
|
|
73
|
+
|
|
56
74
|
// ---------------------------------------------------------------------------
|
|
57
75
|
// Enums / required field lists
|
|
58
76
|
// ---------------------------------------------------------------------------
|
|
@@ -198,4 +216,40 @@ export const OPTIONAL_FIELDS = Object.freeze([
|
|
|
198
216
|
// lacking the field validates unchanged. Value set + the full rationale for
|
|
199
217
|
// why it is NOT a VALID_SESSION_TYPES member: VALID_SESSION_PROFILES above.
|
|
200
218
|
'session_profile',
|
|
219
|
+
// #1339 P8 — HEAD sha at session start. Gives `/evolve analyze` an
|
|
220
|
+
// attributable commit range instead of a time window that also catches
|
|
221
|
+
// parallel sessions' commits. Absent = not measured. NOT guaranteed to be a
|
|
222
|
+
// full sha: scripts/emit-session.mjs now writes only a full 40/64-hex sha or
|
|
223
|
+
// an explicit `null` (a derived ref is adopted only in full form, and an
|
|
224
|
+
// explicit short ref on the record is dropped with a WARN), but every record
|
|
225
|
+
// before that derivation was hand-composed — 19 with a 40-char and 9 with a 7-char
|
|
226
|
+
// value (measured 2026-09-19 @ 8f6ac022, jq over sessions.jsonl).
|
|
227
|
+
// Consumers must accept a short sha or `null`, never assume a full one.
|
|
228
|
+
'session_start_ref',
|
|
229
|
+
// #1457 point 7 — four fields that were written and (two of them) shape-
|
|
230
|
+
// checked while appearing in NEITHER list, the same "status only inferable
|
|
231
|
+
// from an `if`" class as `effectiveness` above (#964). Each one has a live
|
|
232
|
+
// writer:
|
|
233
|
+
// `total_tokens`, `matched_records` — merged from the #1436 subagents.jsonl
|
|
234
|
+
// rollup by scripts/emit-session.mjs (ROLLUP_KEYS) and produced by
|
|
235
|
+
// scripts/lib/session-token-rollup.mjs; validator.mjs checks both as
|
|
236
|
+
// non-negative integers.
|
|
237
|
+
// `raw_session_id` — the native harness uuid beside the semantic
|
|
238
|
+
// `session_id`, written by scripts/emit-session.mjs and by the SessionEnd
|
|
239
|
+
// backfill in scripts/lib/session-close-backfill.mjs.
|
|
240
|
+
// `_repair_source` — provenance marker every record repaired by
|
|
241
|
+
// scripts/lib/session-record-repair.mjs carries (#1443 F2), distinct from
|
|
242
|
+
// `_backfill_source`, which keeps the record's origin.
|
|
243
|
+
'total_tokens',
|
|
244
|
+
'matched_records',
|
|
245
|
+
'raw_session_id',
|
|
246
|
+
'_repair_source',
|
|
247
|
+
// #1475 — how many of the session's cost-relevant subagent records were
|
|
248
|
+
// priced, out of how many. Merged from the rollup by scripts/emit-session.mjs;
|
|
249
|
+
// non-negative integers, priced ≤ total. `total_cost_usd` is omitted when
|
|
250
|
+
// priced < total (some agent's cost is unknown) AND when total is 0 (nothing
|
|
251
|
+
// to price — 1223 of 2093 ledger×session pairs, 31 fleet ledgers,
|
|
252
|
+
// 2026-10-01), so the counters say which of the two it is.
|
|
253
|
+
'cost_records_priced',
|
|
254
|
+
'cost_records_total',
|
|
201
255
|
]);
|