session-orchestrator 3.23.0 → 4.0.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 +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1401 -0
- package/NOTICE +11 -6
- package/README.md +127 -92
- package/agents/db-specialist.md +0 -1
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +249 -48
- package/docs/codex-setup.md +66 -22
- package/docs/components.md +37 -16
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +51 -10
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +8 -8
- package/docs/session-config-reference.md +120 -61
- package/docs/session-config-template.md +40 -33
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +187 -4
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +11 -31
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/enforce-scope.mjs +35 -6
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +280 -14
- package/hooks/on-session-start.mjs +153 -4
- package/hooks/on-stop.mjs +371 -17
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +189 -4
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +278 -392
- package/hooks/post-tool-batch-wave-signal.mjs +272 -44
- package/hooks/post-tool-failure-corrective-context.mjs +11 -34
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +24 -30
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/autopilot.mjs +26 -12
- package/scripts/backfill-abandoned-sessions.mjs +130 -15
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
- package/scripts/lib/build-live-signals.mjs +25 -22
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +63 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +9 -3
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +388 -0
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +234 -0
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/config.mjs +31 -3
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +256 -7
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-banner.mjs +20 -8
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +249 -9
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/scope-gate.mjs +36 -0
- package/scripts/lib/session-close-backfill.mjs +125 -18
- package/scripts/lib/session-discovery.mjs +57 -3
- package/scripts/lib/session-end/phase-skip.mjs +2 -2
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +187 -11
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/session-transition.mjs +1 -1
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +255 -17
- package/scripts/lib/telemetry/sync.mjs +417 -24
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +0 -9
- package/scripts/lib/validate/check-validator-registration.mjs +254 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +174 -135
- package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
- package/scripts/lib/wave-resource-gate.mjs +133 -7
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +142 -8
- package/scripts/materialize-wave-scope.mjs +32 -9
- package/scripts/memory-propose.mjs +146 -8
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +187 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +111 -26
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +18 -4
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +87 -11
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +38 -2
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +19 -9
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +41 -900
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +16 -2
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1271
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -269
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sessions-canonical.mjs — one record per physical session (#1167).
|
|
3
|
+
*
|
|
4
|
+
* `.orchestrator/metrics/sessions.jsonl` is APPEND-ONLY by design: nothing is
|
|
5
|
+
* ever rewritten in place, so the same physical session can appear more than
|
|
6
|
+
* once. A consumer that treats "one line = one session" therefore over-counts,
|
|
7
|
+
* and every duration/effectiveness aggregate computed from the raw file is
|
|
8
|
+
* silently wrong by however many duplicates happen to sit in its window.
|
|
9
|
+
*
|
|
10
|
+
* This module is the READ-side collapse. It never repairs the file (see § 3).
|
|
11
|
+
*
|
|
12
|
+
* ── THE THREE RULES, AND WHAT MEASURED THEM ─────────────────────────────────
|
|
13
|
+
*
|
|
14
|
+
* (1) NEWEST-WINS PER `session_id`.
|
|
15
|
+
* File order is chronological, so the LAST record carrying an id is the
|
|
16
|
+
* current one. This is the same reading rule
|
|
17
|
+
* `session-close-backfill.mjs::classifyExisting()` already applies (it
|
|
18
|
+
* takes `matches[matches.length - 1]`); this module generalises it to the
|
|
19
|
+
* whole file. Measured 2026-09-02 @ c3ab480 over 286 records: one id
|
|
20
|
+
* (2026-05-10) carries a byte-identical duplicate LINE — an older
|
|
21
|
+
* collision class than (3), and rule (1) alone resolves it.
|
|
22
|
+
*
|
|
23
|
+
* (2) NARROW COLLAPSE OF THE SYSTEMIC DOUBLE-STUB CLASS.
|
|
24
|
+
* Two `abandoned` records with an EXACT `started_at` + `completed_at`
|
|
25
|
+
* tuple match are one physical session recorded twice by the two backfill
|
|
26
|
+
* writers: `hooks/on-session-end.mjs` resolves the semantic id from
|
|
27
|
+
* `current-session.json` and writes `main-YYYY-MM-DD-session-N`, while
|
|
28
|
+
* `scripts/backfill-abandoned-sessions.mjs` could resolve a semantic id
|
|
29
|
+
* ONLY via `orchestrator.session.lock.acquired` — a session that lost the
|
|
30
|
+
* lock-acquire race has no such event, so it fell through to the synthetic
|
|
31
|
+
* mint (`<branch>-<date>-abandoned-<sha8>`, `_synthetic_session_id: true`)
|
|
32
|
+
* and wrote a SECOND stub for the same session. The join-back was
|
|
33
|
+
* impossible because `raw_session_id` is null on 286/286 records
|
|
34
|
+
* (`jq -s '[.[]|select(.raw_session_id != null)]|length'` → 0, measured
|
|
35
|
+
* 2026-09-02 @ c3ab480), so the two records share no key at all — only
|
|
36
|
+
* their millisecond-identical timestamps.
|
|
37
|
+
* Measured population: 8 such pairs over 6 weeks (16 records), via
|
|
38
|
+
* `jq -r '[.started_at,.completed_at,.status]|@tsv' … | sort | uniq -d`.
|
|
39
|
+
* The NON-synthetic record survives; the synthetic mint is the artefact.
|
|
40
|
+
*
|
|
41
|
+
* Deliberately narrow. The collapse requires BOTH records to be
|
|
42
|
+
* `status: 'abandoned'` and BOTH timestamps to be present and equal.
|
|
43
|
+
* `started_at` alone is NOT enough (two real sessions can start in the
|
|
44
|
+
* same millisecond of a re-fire), and `completed` records are never
|
|
45
|
+
* collapsed (an authoritative record is a truth claim about itself, never
|
|
46
|
+
* an artefact of a second writer).
|
|
47
|
+
*
|
|
48
|
+
* (3) AN ATTESTABLE `supersedes: X` REMOVES record X.
|
|
49
|
+
* The #1068 AC3/AC4 supersede path appends an authoritative `completed`
|
|
50
|
+
* record carrying a forward pointer to the backfilled `abandoned` stub it
|
|
51
|
+
* refutes. The stub is kept on disk verbatim (AC4 — forensic provenance);
|
|
52
|
+
* a canonical READER must drop it, or the same session is counted as both
|
|
53
|
+
* abandoned and completed.
|
|
54
|
+
*
|
|
55
|
+
* Two constraints, both measured defects of the first implementation:
|
|
56
|
+
* - ORDER-INDEPENDENT. A record is dropped iff some SURVIVING record
|
|
57
|
+
* supersedes it (a fixpoint over the supersede graph; cycles broken by
|
|
58
|
+
* keeping the newest member). Deleting in file order made a chain
|
|
59
|
+
* `C → B → A` resolve to `{C, A}` or `{C}` depending on the
|
|
60
|
+
* permutation the appends happened to land in, and a mutual pair
|
|
61
|
+
* resolved by insertion order.
|
|
62
|
+
* - ATTESTABLE ONLY. The marker is honoured only when the target is not
|
|
63
|
+
* authoritative (`status` `abandoned`, or absent on a legacy stub —
|
|
64
|
+
* never `completed`) AND the two records share a join key (equal
|
|
65
|
+
* `raw_session_id`, or byte-equal `started_at` — the shape
|
|
66
|
+
* `session-close-backfill.mjs::synthesizeRecord()` emits, since stub
|
|
67
|
+
* and superseder are synthesized from the same gathered events).
|
|
68
|
+
* Without that constraint ONE appended line could delete ANY id from
|
|
69
|
+
* EVERY reader of this module, the armed autonomy verdict included. A
|
|
70
|
+
* refused marker keeps both records and is reported (never logged)
|
|
71
|
+
* via `canonicalizeSessionsDetailed().ignoredSupersedes`.
|
|
72
|
+
*
|
|
73
|
+
* RULE ORDER: (1) → (2) → (3). The double-stub collapse must run BEFORE
|
|
74
|
+
* supersede removal: with the reverse order a `supersedes` append deleted the
|
|
75
|
+
* authentic stub first, shrank the tuple group to a single member, and the
|
|
76
|
+
* synthetic phantom then survived the very session that refuted it.
|
|
77
|
+
*
|
|
78
|
+
* ── WHAT THIS MODULE DOES NOT DO ────────────────────────────────────────────
|
|
79
|
+
* - It never writes. The 8 historical pairs stay on disk; the ledger is
|
|
80
|
+
* append-only and the duplicates are their own provenance.
|
|
81
|
+
* - It is not a phantom filter. `status: 'abandoned'` records SURVIVE here —
|
|
82
|
+
* dropping them is `session-schema/filters.mjs`'s job
|
|
83
|
+
* (`isRealSession` / `filterRealSessions` / `tailRealSessions`), and the
|
|
84
|
+
* two compose: canonicalize first, then filter.
|
|
85
|
+
*
|
|
86
|
+
* Plain Node ESM. Named exports. `canonicalizeSessions` is pure; only
|
|
87
|
+
* `readCanonicalSessions` touches the filesystem (sync, `readFileSync`).
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
import fs from 'node:fs';
|
|
91
|
+
import path from 'node:path';
|
|
92
|
+
|
|
93
|
+
const SESSIONS_REL = ['.orchestrator', 'metrics', 'sessions.jsonl'];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* True when the value is a usable record object (not null, not an array).
|
|
97
|
+
* @param {unknown} v
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
function isRecordObject(v) {
|
|
101
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Non-empty string guard — `''` is never a usable id or timestamp. */
|
|
105
|
+
function isNonEmptyString(v) {
|
|
106
|
+
return typeof v === 'string' && v.length > 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The timestamp a record is ordered by when a supersede CYCLE has to be broken:
|
|
111
|
+
* `completed_at` when present, else `started_at`, else `''` (sorts last).
|
|
112
|
+
* ISO-8601 strings compare lexicographically, so no Date parsing is needed.
|
|
113
|
+
* @param {object} rec
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
function cycleOrderTimestamp(rec) {
|
|
117
|
+
if (isNonEmptyString(rec.completed_at)) return rec.completed_at;
|
|
118
|
+
if (isNonEmptyString(rec.started_at)) return rec.started_at;
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* True when `superseder`'s `supersedes` marker is ATTESTABLE against `target`.
|
|
124
|
+
*
|
|
125
|
+
* `supersedes` is a forward pointer inside an append-only file that anyone (or
|
|
126
|
+
* any buggy writer) can append a line to, and a reader that obeys it blindly
|
|
127
|
+
* lets a single appended line delete ANY id from EVERY consumer — including the
|
|
128
|
+
* armed autonomy verdict. So the marker is honoured only for the shape the
|
|
129
|
+
* #1068 writer actually produces: the stub it refutes is never an AUTHORITATIVE
|
|
130
|
+
* record — its `status` is `abandoned`, or absent/null on a legacy stub, but
|
|
131
|
+
* never any other declared status (a `completed` record is a truth claim about
|
|
132
|
+
* itself and can never be deleted by an appended pointer) — and both records
|
|
133
|
+
* were synthesized from the SAME gathered events, hence share an attestable
|
|
134
|
+
* join key —
|
|
135
|
+
* - equal non-empty `raw_session_id` (the #1167 harness-uuid join), or
|
|
136
|
+
* - byte-equal non-empty `started_at` (`session-close-backfill.mjs`
|
|
137
|
+
* `synthesizeRecord()` derives `startedIso` from the same event set for the
|
|
138
|
+
* stub and for the record that supersedes it).
|
|
139
|
+
* Anything else is a data-integrity anomaly: BOTH records are kept and the
|
|
140
|
+
* marker is reported via `canonicalizeSessionsDetailed().ignoredSupersedes`.
|
|
141
|
+
*
|
|
142
|
+
* @param {object} superseder
|
|
143
|
+
* @param {object} target
|
|
144
|
+
* @returns {string|null} null when the marker is valid, else the reject reason
|
|
145
|
+
*/
|
|
146
|
+
function supersedeRejectReason(superseder, target) {
|
|
147
|
+
// Absent/null `status` is a legacy stub, not an authoritative record; any
|
|
148
|
+
// OTHER declared status (`completed` above all) is untouchable.
|
|
149
|
+
if (isNonEmptyString(target.status) && target.status !== 'abandoned') {
|
|
150
|
+
return 'target-not-abandoned';
|
|
151
|
+
}
|
|
152
|
+
const a = superseder.raw_session_id;
|
|
153
|
+
const b = target.raw_session_id;
|
|
154
|
+
if (isNonEmptyString(a) && isNonEmptyString(b) && a === b) return null;
|
|
155
|
+
if (isNonEmptyString(superseder.started_at) && superseder.started_at === target.started_at) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return 'no-shared-join-key';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Rule (3) — collapse the systemic two-writer double stub. Mutates `byId` by
|
|
163
|
+
* deleting the synthetic twin of each qualifying pair.
|
|
164
|
+
* @param {Map<string, object>} byId
|
|
165
|
+
* @returns {void}
|
|
166
|
+
*/
|
|
167
|
+
function collapseAbandonedTuples(byId) {
|
|
168
|
+
// Group ONLY the records eligible for the systemic double-stub class; every
|
|
169
|
+
// other record bypasses this pass entirely and can never be dropped by it.
|
|
170
|
+
const byTuple = new Map();
|
|
171
|
+
for (const rec of byId.values()) {
|
|
172
|
+
if (rec.status !== 'abandoned') continue;
|
|
173
|
+
if (!isNonEmptyString(rec.started_at) || !isNonEmptyString(rec.completed_at)) continue;
|
|
174
|
+
const key = `${rec.started_at} ${rec.completed_at}`;
|
|
175
|
+
const group = byTuple.get(key);
|
|
176
|
+
if (group) group.push(rec);
|
|
177
|
+
else byTuple.set(key, [rec]);
|
|
178
|
+
}
|
|
179
|
+
for (const group of byTuple.values()) {
|
|
180
|
+
if (group.length < 2) continue;
|
|
181
|
+
const authentic = group.filter((r) => r._synthetic_session_id !== true);
|
|
182
|
+
// All-synthetic (or all-authentic) groups are left intact: with no
|
|
183
|
+
// non-synthetic record to prefer there is no evidence about WHICH one is
|
|
184
|
+
// the artefact, and guessing would delete a session nobody can recover.
|
|
185
|
+
if (authentic.length === 0 || authentic.length === group.length) continue;
|
|
186
|
+
for (const rec of group) {
|
|
187
|
+
if (rec._synthetic_session_id === true) byId.delete(rec.session_id);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Rule (2) — order-independent supersede resolution. Mutates `byId` by deleting
|
|
194
|
+
* every record that a SURVIVING record supersedes, and appends every rejected
|
|
195
|
+
* marker to `ignored`.
|
|
196
|
+
*
|
|
197
|
+
* A record is dropped iff some record that itself survives supersedes it; the
|
|
198
|
+
* marking is a fixpoint over the supersede graph, so it depends on the EDGES
|
|
199
|
+
* only, never on the order the records appear in the file. A chain
|
|
200
|
+
* `C → B → A` therefore always resolves to `{C, A}` (B is dropped by the
|
|
201
|
+
* surviving C, so B's own marker no longer removes A).
|
|
202
|
+
*
|
|
203
|
+
* A cycle (`X → Y`, `Y → X`) has no fixpoint; it is broken deterministically by
|
|
204
|
+
* keeping the NEWEST member (`completed_at ?? started_at`, ties by ascending
|
|
205
|
+
* `session_id`) and re-running the propagation.
|
|
206
|
+
*
|
|
207
|
+
* @param {Map<string, object>} byId
|
|
208
|
+
* @param {Array<{by: string, target: string, reason: string}>} ignored
|
|
209
|
+
* @returns {void}
|
|
210
|
+
*/
|
|
211
|
+
function resolveSupersedes(byId, ignored) {
|
|
212
|
+
/** targetId → Set of ids of records that validly supersede it. */
|
|
213
|
+
const supersededBy = new Map();
|
|
214
|
+
for (const rec of byId.values()) {
|
|
215
|
+
const target = rec.supersedes;
|
|
216
|
+
if (!isNonEmptyString(target) || target === rec.session_id) continue;
|
|
217
|
+
const targetRec = byId.get(target);
|
|
218
|
+
// A marker pointing at an id that is not present removes nothing; it is not
|
|
219
|
+
// an anomaly either (the target may legitimately have been collapsed by
|
|
220
|
+
// rule 3 first, or simply predate this window of the ledger).
|
|
221
|
+
if (!targetRec) continue;
|
|
222
|
+
const reason = supersedeRejectReason(rec, targetRec);
|
|
223
|
+
if (reason !== null) {
|
|
224
|
+
ignored.push({ by: rec.session_id, target, reason });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const set = supersededBy.get(target);
|
|
228
|
+
if (set) set.add(rec.session_id);
|
|
229
|
+
else supersededBy.set(target, new Set([rec.session_id]));
|
|
230
|
+
}
|
|
231
|
+
if (supersededBy.size === 0) return;
|
|
232
|
+
|
|
233
|
+
const ids = [...byId.keys()];
|
|
234
|
+
/** id → 'alive' | 'dead'; absent = not yet decided. */
|
|
235
|
+
const state = new Map();
|
|
236
|
+
for (;;) {
|
|
237
|
+
let changed = true;
|
|
238
|
+
while (changed) {
|
|
239
|
+
changed = false;
|
|
240
|
+
for (const id of ids) {
|
|
241
|
+
if (state.has(id)) continue;
|
|
242
|
+
const sup = supersededBy.get(id);
|
|
243
|
+
if (!sup || sup.size === 0) {
|
|
244
|
+
state.set(id, 'alive');
|
|
245
|
+
changed = true;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
let anyAlive = false;
|
|
249
|
+
let allDead = true;
|
|
250
|
+
for (const s of sup) {
|
|
251
|
+
const st = state.get(s);
|
|
252
|
+
if (st === 'alive') anyAlive = true;
|
|
253
|
+
if (st !== 'dead') allDead = false;
|
|
254
|
+
}
|
|
255
|
+
if (anyAlive) {
|
|
256
|
+
state.set(id, 'dead');
|
|
257
|
+
changed = true;
|
|
258
|
+
} else if (allDead) {
|
|
259
|
+
state.set(id, 'alive');
|
|
260
|
+
changed = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const undecided = ids.filter((id) => !state.has(id));
|
|
265
|
+
if (undecided.length === 0) break;
|
|
266
|
+
// Cycle: keep the newest member, then let propagation settle the rest.
|
|
267
|
+
undecided.sort((a, b) => {
|
|
268
|
+
const ta = cycleOrderTimestamp(byId.get(a));
|
|
269
|
+
const tb = cycleOrderTimestamp(byId.get(b));
|
|
270
|
+
if (ta !== tb) return ta < tb ? 1 : -1;
|
|
271
|
+
return a < b ? -1 : 1;
|
|
272
|
+
});
|
|
273
|
+
state.set(undecided[0], 'alive');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const [id, st] of state) {
|
|
277
|
+
if (st === 'dead') byId.delete(id);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Collapse a raw sessions.jsonl record array to one record per physical
|
|
283
|
+
* session AND report the supersede markers that were refused. Pure — the input
|
|
284
|
+
* array is never mutated.
|
|
285
|
+
*
|
|
286
|
+
* Rules, applied in this order (see the module header for the measured
|
|
287
|
+
* justification of each):
|
|
288
|
+
* 1. newest-wins per `session_id` (file order is chronological);
|
|
289
|
+
* 2. two `abandoned` records with an exact, both-present
|
|
290
|
+
* `started_at` + `completed_at` tuple collapse to the non-synthetic one;
|
|
291
|
+
* 3. a surviving record's ATTESTABLE `supersedes: X` removes record `X`.
|
|
292
|
+
*
|
|
293
|
+
* The double-stub collapse runs BEFORE supersede removal so that a stub which
|
|
294
|
+
* is itself about to be superseded still shadows its synthetic twin — with the
|
|
295
|
+
* old order the twin outlived the record it duplicated (a `supersedes` append
|
|
296
|
+
* shrank the tuple group to one member, and the phantom survived the session
|
|
297
|
+
* that refuted it).
|
|
298
|
+
*
|
|
299
|
+
* Records without a usable `session_id` are dropped, unless
|
|
300
|
+
* `keepUnidentified: true` (they cannot be deduplicated; a COUNT-style or
|
|
301
|
+
* effectiveness-style consumer would rather keep them than shrink its `n`).
|
|
302
|
+
*
|
|
303
|
+
* Output order follows FIRST appearance of each surviving id in the input; kept
|
|
304
|
+
* unidentified records are appended after them, in their original order.
|
|
305
|
+
*
|
|
306
|
+
* @param {Array<unknown>} records
|
|
307
|
+
* @param {object} [opts]
|
|
308
|
+
* @param {boolean} [opts.keepUnidentified=false] pass id-less record objects
|
|
309
|
+
* through untouched instead of dropping them.
|
|
310
|
+
* @returns {{records: Array<object>, ignoredSupersedes: Array<{by: string,
|
|
311
|
+
* target: string, reason: string}>}}
|
|
312
|
+
*/
|
|
313
|
+
export function canonicalizeSessionsDetailed(records, { keepUnidentified = false } = {}) {
|
|
314
|
+
if (!Array.isArray(records)) return { records: [], ignoredSupersedes: [] };
|
|
315
|
+
|
|
316
|
+
// -- (1) newest-wins per id ------------------------------------------------
|
|
317
|
+
// Map insertion order = FIRST appearance of the id; the stored value is the
|
|
318
|
+
// LAST record carrying it, so a superseding append wins without reordering
|
|
319
|
+
// the ledger's chronology.
|
|
320
|
+
const byId = new Map();
|
|
321
|
+
const unidentified = [];
|
|
322
|
+
for (const rec of records) {
|
|
323
|
+
if (!isRecordObject(rec)) continue;
|
|
324
|
+
if (!isNonEmptyString(rec.session_id)) {
|
|
325
|
+
if (keepUnidentified) unidentified.push(rec);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
byId.set(rec.session_id, rec);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// -- (2) narrow abandoned-tuple collapse -----------------------------------
|
|
332
|
+
collapseAbandonedTuples(byId);
|
|
333
|
+
|
|
334
|
+
// -- (3) supersede removal (order-independent, join-key constrained) -------
|
|
335
|
+
const ignoredSupersedes = [];
|
|
336
|
+
resolveSupersedes(byId, ignoredSupersedes);
|
|
337
|
+
|
|
338
|
+
return { records: [...byId.values(), ...unidentified], ignoredSupersedes };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Collapse a raw sessions.jsonl record array to one record per physical
|
|
343
|
+
* session. Pure — the input array is never mutated. Thin wrapper over
|
|
344
|
+
* `canonicalizeSessionsDetailed`, returning only the records (the array shape
|
|
345
|
+
* every consumer reads).
|
|
346
|
+
*
|
|
347
|
+
* @param {Array<unknown>} records
|
|
348
|
+
* @param {object} [opts] — see `canonicalizeSessionsDetailed`.
|
|
349
|
+
* @param {boolean} [opts.keepUnidentified=false]
|
|
350
|
+
* @returns {Array<object>} canonical records
|
|
351
|
+
*/
|
|
352
|
+
export function canonicalizeSessions(records, opts) {
|
|
353
|
+
return canonicalizeSessionsDetailed(records, opts).records;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Count DISTINCT physical sessions in RAW `sessions.jsonl` text. Pure — no fs,
|
|
358
|
+
* so an async reader keeps its own `readFile` and only the counting rule is
|
|
359
|
+
* shared (the two async consumers, `memory-banner.mjs` and
|
|
360
|
+
* `cold-start-detector.mjs`, carried byte-identical copies of this body).
|
|
361
|
+
*
|
|
362
|
+
* Blank lines (incl. the trailing newline) are skipped. A line that does not
|
|
363
|
+
* PARSE is not counted at all — it cannot be attributed to any session (this
|
|
364
|
+
* replaces the pre-#1167 "count every non-empty line, never parse" rule).
|
|
365
|
+
*
|
|
366
|
+
* `canonicalizeSessions` DROPS records without a `session_id` (they cannot be
|
|
367
|
+
* deduplicated). For a COUNT that would under-report rather than de-duplicate,
|
|
368
|
+
* so id-less records are counted as-is and only the id-bearing ones go through
|
|
369
|
+
* the identity collapse.
|
|
370
|
+
*
|
|
371
|
+
* @param {string} raw — full file contents.
|
|
372
|
+
* @returns {number}
|
|
373
|
+
*/
|
|
374
|
+
export function countSessionsInJsonl(raw) {
|
|
375
|
+
if (typeof raw !== 'string' || raw.length === 0) return 0;
|
|
376
|
+
const parsed = [];
|
|
377
|
+
for (const line of raw.split('\n')) {
|
|
378
|
+
const trimmed = line.trim();
|
|
379
|
+
if (!trimmed) continue;
|
|
380
|
+
try {
|
|
381
|
+
parsed.push(JSON.parse(trimmed));
|
|
382
|
+
} catch {
|
|
383
|
+
/* skip malformed line */
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const identified = parsed.filter((r) => isRecordObject(r) && isNonEmptyString(r.session_id));
|
|
387
|
+
const anonymous = parsed.length - identified.length;
|
|
388
|
+
return canonicalizeSessions(identified).length + anonymous;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Read `sessions.jsonl` and return its canonical records (see
|
|
393
|
+
* `canonicalizeSessions` for the three collapse rules).
|
|
394
|
+
*
|
|
395
|
+
* Synchronous by design — every consumer of the ledger in this repo reads it
|
|
396
|
+
* with `readFileSync`, and the file is small (286 records / ~0.5 MB at the
|
|
397
|
+
* time of writing). A MISSING file (ENOENT) yields `[]` silently; an UNREADABLE
|
|
398
|
+
* one (EACCES/EISDIR/…) yields `[]` with a stderr WARN (#1188); each malformed line
|
|
399
|
+
* is skipped rather than aborting the whole read (same posture as the readers
|
|
400
|
+
* in `session-close-backfill.mjs` and `backfill-abandoned-sessions.mjs`).
|
|
401
|
+
*
|
|
402
|
+
* @param {object} [args]
|
|
403
|
+
* @param {string} [args.repoRoot] project root; the ledger is resolved as
|
|
404
|
+
* `<repoRoot>/.orchestrator/metrics/sessions.jsonl`. Defaults to
|
|
405
|
+
* `process.cwd()` when neither this nor `filePath` is given.
|
|
406
|
+
* @param {string} [args.filePath] explicit ledger path (wins over `repoRoot`).
|
|
407
|
+
* @returns {Array<object>} canonical records
|
|
408
|
+
*/
|
|
409
|
+
export function readCanonicalSessions({ repoRoot, filePath } = {}) {
|
|
410
|
+
const resolved = isNonEmptyString(filePath)
|
|
411
|
+
? filePath
|
|
412
|
+
: path.join(isNonEmptyString(repoRoot) ? repoRoot : process.cwd(), ...SESSIONS_REL);
|
|
413
|
+
|
|
414
|
+
let raw;
|
|
415
|
+
try {
|
|
416
|
+
raw = fs.readFileSync(resolved, 'utf8');
|
|
417
|
+
} catch (err) {
|
|
418
|
+
// #1188 — ENOENT and EACCES/EISDIR are different facts: a missing ledger is
|
|
419
|
+
// the ordinary fresh-repo case; an UNREADABLE one previously read as "no
|
|
420
|
+
// sessions" and made every downstream count silently wrong. Same split as
|
|
421
|
+
// readLockDetailed (session-lock.mjs § absent vs unreadable).
|
|
422
|
+
if (!err || err.code !== 'ENOENT') {
|
|
423
|
+
process.stderr.write(
|
|
424
|
+
`⚠ readCanonicalSessions: cannot read ${resolved} ` +
|
|
425
|
+
`(${err?.code ?? '?'}: ${err?.message ?? String(err)}) — ` +
|
|
426
|
+
'treating as EMPTY, counts below are floors\n',
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
// CEILING (BV-004): still [] rather than throw — SessionStart callers must
|
|
430
|
+
// not crash on a transient permissions fault. REVISIT if the warn rate in
|
|
431
|
+
// events.jsonl shows masked corruption.
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const parsed = [];
|
|
436
|
+
for (const line of raw.split('\n')) {
|
|
437
|
+
const trimmed = line.trim();
|
|
438
|
+
if (!trimmed) continue;
|
|
439
|
+
try {
|
|
440
|
+
parsed.push(JSON.parse(trimmed));
|
|
441
|
+
} catch {
|
|
442
|
+
/* skip malformed line */
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return canonicalizeSessions(parsed);
|
|
446
|
+
}
|
|
@@ -132,6 +132,7 @@ import path from 'node:path';
|
|
|
132
132
|
|
|
133
133
|
import { readLock, DEFAULT_TTL_HOURS } from './session-lock.mjs';
|
|
134
134
|
import { isRealSession } from './session-schema/filters.mjs';
|
|
135
|
+
import { readCanonicalSessions } from './sessions-canonical.mjs';
|
|
135
136
|
|
|
136
137
|
/** Repo-relative path to the session ledger (one record per closed session). */
|
|
137
138
|
const SESSIONS_PATH = '.orchestrator/metrics/sessions.jsonl';
|
|
@@ -240,20 +241,21 @@ function keepNewer(current, candidate) {
|
|
|
240
241
|
* result so the caller can say the anchor is a stub start, not a measured close;
|
|
241
242
|
* the key is omitted (`undefined`) on the genuine path.
|
|
242
243
|
*
|
|
243
|
-
*
|
|
244
|
+
* `records` is the CANONICAL (#1209b) record set — `readCanonicalSessions()`
|
|
245
|
+
* has already collapsed a duplicated `session_id` to its newest occurrence, so
|
|
246
|
+
* a since-corrected raw LINE for the same identity (e.g. one later marked
|
|
247
|
+
* `status: 'abandoned'`, or a stale `completed_at` a later record for the same
|
|
248
|
+
* id superseded) can no longer independently skew this max-reduce the way a
|
|
249
|
+
* raw-line scan over every append could.
|
|
250
|
+
*
|
|
251
|
+
* @param {object[]} records
|
|
244
252
|
* @returns {{iso: string, ms: number, stubFallback?: true}|null}
|
|
245
253
|
*/
|
|
246
|
-
function lastLedgerEntry(
|
|
254
|
+
function lastLedgerEntry(records) {
|
|
247
255
|
let newestGenuine = null; // newest genuine completed_at (or started_at floor)
|
|
248
256
|
let newestStub = null; // newest stub started_at
|
|
249
257
|
|
|
250
|
-
for (const
|
|
251
|
-
let record;
|
|
252
|
-
try {
|
|
253
|
-
record = JSON.parse(line);
|
|
254
|
-
} catch {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
258
|
+
for (const record of records) {
|
|
257
259
|
if (!record || typeof record !== 'object') continue;
|
|
258
260
|
|
|
259
261
|
if (isBackfillStub(record)) {
|
|
@@ -370,10 +372,15 @@ export function checkSessionsStaleness({ repoRoot, now = Date.now() } = {}) {
|
|
|
370
372
|
|
|
371
373
|
const nowMs = typeof now === 'number' && Number.isFinite(now) ? now : Date.now();
|
|
372
374
|
|
|
373
|
-
const
|
|
375
|
+
const sessionsPath = path.join(repoRoot, SESSIONS_PATH);
|
|
376
|
+
const sessionLines = readJsonlLines(sessionsPath);
|
|
374
377
|
if (sessionLines === null || sessionLines.length === 0) return null;
|
|
375
378
|
|
|
376
|
-
|
|
379
|
+
// CANONICAL (#1209b) record set — readJsonlLines() above only decides
|
|
380
|
+
// missing-vs-empty (readCanonicalSessions() cannot tell those apart, see
|
|
381
|
+
// its own doc); the anchor scan itself now runs over the collapsed set.
|
|
382
|
+
const sessionRecords = readCanonicalSessions({ filePath: sessionsPath });
|
|
383
|
+
const ledger = lastLedgerEntry(sessionRecords);
|
|
377
384
|
if (ledger === null) return null;
|
|
378
385
|
|
|
379
386
|
const eventLines = readJsonlLines(path.join(repoRoot, EVENTS_PATH));
|
|
@@ -35,6 +35,7 @@ import path from 'node:path';
|
|
|
35
35
|
import { fileURLToPath } from 'node:url';
|
|
36
36
|
|
|
37
37
|
import { isRealSession } from '../session-schema/filters.mjs';
|
|
38
|
+
import { readCanonicalSessions } from '../sessions-canonical.mjs';
|
|
38
39
|
|
|
39
40
|
const DEFAULT_INVOCATIONS_PATH = path.resolve(
|
|
40
41
|
fileURLToPath(import.meta.url),
|
|
@@ -81,6 +82,15 @@ async function readJsonl(filePath) {
|
|
|
81
82
|
* it to route the join to the `abandoned` outcome bucket instead of counting
|
|
82
83
|
* a zero-signal join as `sessionsJoined`.
|
|
83
84
|
*
|
|
85
|
+
* Callers pass `sessionRecords` from `readCanonicalSessions()` (#1209b) — one
|
|
86
|
+
* record per physical session (newest-wins per `session_id`, systemic
|
|
87
|
+
* double-stub twin dropped, superseded stubs removed). Without that upstream
|
|
88
|
+
* collapse, the two-writer double-stub class (a SEPARATE synthetic
|
|
89
|
+
* `session_id` for the SAME physical session) would enter this map as a
|
|
90
|
+
* second, independent `abandoned` entry — this function's own `map.set()`
|
|
91
|
+
* overwrite only dedupes an EXACT `session_id` repeat, never two different
|
|
92
|
+
* ids for one session.
|
|
93
|
+
*
|
|
84
94
|
* @param {object[]} sessionRecords
|
|
85
95
|
* @returns {Map<string, { agentSummary: { complete: number, partial: number, failed: number, spiral: number }, real: boolean }>}
|
|
86
96
|
*/
|
|
@@ -121,10 +131,13 @@ export async function joinSkillOutcomes({
|
|
|
121
131
|
invocationsPath = DEFAULT_INVOCATIONS_PATH,
|
|
122
132
|
sessionsPath = DEFAULT_SESSIONS_PATH,
|
|
123
133
|
} = {}) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
// Invocations stay on the raw async reader (no identity-collapse concept
|
|
135
|
+
// applies to a selection-event stream). Sessions move to the CANONICAL
|
|
136
|
+
// (#1209b) reader — synchronous by contract (see sessions-canonical.mjs) —
|
|
137
|
+
// so a session_id counted twice under the two-writer double-stub bug no
|
|
138
|
+
// longer inflates `sessionsAbandoned` (see buildSessionMap doc above).
|
|
139
|
+
const invocations = await readJsonl(invocationsPath);
|
|
140
|
+
const sessionRecords = readCanonicalSessions({ filePath: sessionsPath });
|
|
128
141
|
|
|
129
142
|
const sessionMap = buildSessionMap(sessionRecords);
|
|
130
143
|
|
package/scripts/lib/state-md.mjs
CHANGED
|
@@ -9,8 +9,15 @@
|
|
|
9
9
|
* @see scripts/lib/state-md/body-sections.mjs readCurrentTask, appendDeviation, markExpressPathComplete, appendWhatNotToRetry, readWhatNotToRetry, readOpenQuestions, appendOpenQuestion, markOpenQuestionAnswered
|
|
10
10
|
* @see scripts/lib/state-md/mission-status.mjs parseMissionStatus, parseMissionStatusStrict, MISSION_STATUS_VALUES, writeMissionStatus, setMissionStatus, setMissionStatusDetailed, readMissionStatus, recoverFrontmatterMissionStatusDetailed, writeMissionStatusOnDisk, setMissionStatusOnDisk
|
|
11
11
|
* @see scripts/lib/state-md/recommendations.mjs parseRecommendations
|
|
12
|
+
*
|
|
13
|
+
* Plus ONE small non-re-export surface: the `session-profile` frontmatter
|
|
14
|
+
* accessors at the bottom of this file (see their docblock for why they are
|
|
15
|
+
* composed here rather than added as a fourth mutator module).
|
|
12
16
|
*/
|
|
13
17
|
|
|
18
|
+
import { parseStateMd as _parseStateMd } from './state-md/yaml-parser.mjs';
|
|
19
|
+
import { updateFrontmatterFields as _updateFrontmatterFields } from './state-md/frontmatter-mutators.mjs';
|
|
20
|
+
|
|
14
21
|
export { parseStateMd, serializeStateMd } from './state-md/yaml-parser.mjs';
|
|
15
22
|
|
|
16
23
|
export {
|
|
@@ -61,3 +68,74 @@ export {
|
|
|
61
68
|
} from './state-md/mission-status.mjs';
|
|
62
69
|
|
|
63
70
|
export { parseRecommendations } from './state-md/recommendations.mjs';
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Session profile (PRD docs/prd/2026-09-06-ultradeep-session-profile.md)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
//
|
|
76
|
+
// `session-profile` is an OPTIONAL STATE.md frontmatter scalar that names a
|
|
77
|
+
// wave-shape variant on top of an unchanged `session-type`. Today exactly one
|
|
78
|
+
// value is defined — `ultradeep` (7 waves, coordinator-direct Synthesis-Gate at
|
|
79
|
+
// wave 2) — resolved from the `/session ultradeep` argument alias in
|
|
80
|
+
// `commands/session.md`.
|
|
81
|
+
//
|
|
82
|
+
// The vocabulary is deliberately NOT a closed set here. A profile changes only
|
|
83
|
+
// how the coordinator shapes waves; unlike `session_type` (a closed set in
|
|
84
|
+
// scripts/lib/session-schema/constants.mjs, telemetry and the close-backfill),
|
|
85
|
+
// no consumer branches on the value, so an unknown one degrades to "a profile
|
|
86
|
+
// this reader does not recognise" rather than to a silent mislabel. Revisit
|
|
87
|
+
// trigger: the first consumer that BRANCHES on a specific profile value — at
|
|
88
|
+
// that point the set becomes load-bearing and belongs in a shared constant.
|
|
89
|
+
//
|
|
90
|
+
// Composed from the two existing helpers above rather than reaching into the
|
|
91
|
+
// frontmatter with a second parser: `parseStateMd` for the read,
|
|
92
|
+
// `updateFrontmatterFields` for the write (whose null/undefined semantics
|
|
93
|
+
// already mean DELETE, which is exactly "no profile").
|
|
94
|
+
|
|
95
|
+
/** Frontmatter key holding the optional session profile. */
|
|
96
|
+
export const SESSION_PROFILE_FIELD = 'session-profile';
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read the session profile from STATE.md contents.
|
|
100
|
+
*
|
|
101
|
+
* ABSENCE IS NEVER COERCED. Returns `null` — not `''`, not `'none'` — when the
|
|
102
|
+
* document has no frontmatter, no `session-profile` key, or a value that is not
|
|
103
|
+
* a non-empty string. Callers test `=== null` for "no profile"; they must not
|
|
104
|
+
* test truthiness of a string they assumed was always present.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} contents Full STATE.md text.
|
|
107
|
+
* @returns {string|null} The profile name, or null when no profile is set.
|
|
108
|
+
*/
|
|
109
|
+
export function readSessionProfile(contents) {
|
|
110
|
+
if (typeof contents !== 'string') return null;
|
|
111
|
+
const parsed = _parseStateMd(contents);
|
|
112
|
+
if (parsed === null) return null;
|
|
113
|
+
const value = parsed.frontmatter[SESSION_PROFILE_FIELD];
|
|
114
|
+
if (typeof value !== 'string') return null;
|
|
115
|
+
const trimmed = value.trim();
|
|
116
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Set or clear the session profile in STATE.md contents.
|
|
121
|
+
*
|
|
122
|
+
* Passing `null` DELETES the key, restoring the absent (= no profile) state —
|
|
123
|
+
* it never writes a placeholder value. Every other frontmatter key, including
|
|
124
|
+
* unknown extensions, is preserved verbatim by `updateFrontmatterFields`.
|
|
125
|
+
* No-ops (returns the input unchanged) when `contents` has no frontmatter.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} contents Full STATE.md text.
|
|
128
|
+
* @param {string|null} profile Profile name, or null to clear.
|
|
129
|
+
* @returns {string} The new STATE.md text.
|
|
130
|
+
* @throws {TypeError} when `profile` is neither a non-empty string nor null.
|
|
131
|
+
*/
|
|
132
|
+
export function setSessionProfile(contents, profile) {
|
|
133
|
+
if (profile !== null && (typeof profile !== 'string' || profile.trim().length === 0)) {
|
|
134
|
+
throw new TypeError(
|
|
135
|
+
`setSessionProfile: profile must be a non-empty string or null, got: ${JSON.stringify(profile)}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return _updateFrontmatterFields(contents, {
|
|
139
|
+
[SESSION_PROFILE_FIELD]: profile === null ? null : profile.trim(),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -471,6 +471,12 @@ function collectFiles(dir) {
|
|
|
471
471
|
function isBoilerplateSite(relPath, kind, name) {
|
|
472
472
|
if (kind === 'agent') {
|
|
473
473
|
if (relPath === `agents/${name}.md`) return true;
|
|
474
|
+
// Kept for repoRoots where an authoring-spec file still lives at this path
|
|
475
|
+
// (pre-4.0.0 checkouts, template/consumer repos) — see
|
|
476
|
+
// tests/lib/sunset-walker.test.mjs "boilerplate exclusion (agents)". The
|
|
477
|
+
// session-orchestrator repo itself moved the spec to docs/agent-authoring.md,
|
|
478
|
+
// which is outside SCAN_DIRS and can never appear as a relPath here, so
|
|
479
|
+
// there is no live path to repoint this exclusion to.
|
|
474
480
|
if (relPath === 'agents/AGENTS.md') return true;
|
|
475
481
|
if (relPath === `agents/schemas/${name}.schema.json`) return true;
|
|
476
482
|
// Routing-table / validator boilerplate.
|