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
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* @property {number} waveNumber
|
|
40
40
|
* @property {number | null} filesChanged
|
|
41
41
|
* @property {number | null} testPassed
|
|
42
|
+
* @property {number | null} testFailed
|
|
42
43
|
* @property {number | null} agentDispatchCount
|
|
43
44
|
*/
|
|
44
45
|
|
|
@@ -143,21 +144,56 @@ function tailRead(absPath, prevOffset) {
|
|
|
143
144
|
* Event types this monitor is meant to classify. Anything else is ignored even
|
|
144
145
|
* when it carries a wave number.
|
|
145
146
|
*
|
|
146
|
-
* The allowlist is a wave-LIFECYCLE prefix
|
|
147
|
-
* i.e. exactly the records that can carry the
|
|
148
|
-
* `evaluateSignals` compares (`files_changed`,
|
|
149
|
-
*
|
|
150
|
-
* gains a `wave_number` field must be added here consciously.
|
|
147
|
+
* The allowlist is a wave-LIFECYCLE prefix, the agent-dispatch/-stop counters,
|
|
148
|
+
* and the quality-gate envelope — i.e. exactly the records that can carry the
|
|
149
|
+
* three measurements `evaluateSignals` compares (`files_changed`, the test
|
|
150
|
+
* pass count, the per-wave agent count). It is deliberately fail-CLOSED: a new
|
|
151
|
+
* event type that gains a `wave_number` field must be added here consciously.
|
|
152
|
+
*
|
|
153
|
+
* The gate admission is a TYPE-AND-SHAPE gate, never a bare prefix widening —
|
|
154
|
+
* see `isWaveScopedEvent` for why.
|
|
151
155
|
*/
|
|
152
156
|
const WAVE_EVENT_PREFIX = 'orchestrator.wave.';
|
|
153
157
|
const WAVE_EVENT_NAMES = new Set(['agent.dispatched', 'orchestrator.agent.dispatched']);
|
|
158
|
+
const AGENT_STOPPED_EVENT = 'orchestrator.agent.stopped';
|
|
159
|
+
const GATE_EVENT_PREFIX = 'orchestrator.quality_gate.';
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* True when a record carries a `counts` object this monitor can fold, i.e. a
|
|
163
|
+
* plain object with a numeric `passed`. `counts: null`, an array, or a string
|
|
164
|
+
* payload are all rejected — the shape half of the type-and-shape gate.
|
|
165
|
+
*
|
|
166
|
+
* @param {Record<string, unknown>} rec
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
function hasFoldableCounts(rec) {
|
|
170
|
+
const counts = rec.counts;
|
|
171
|
+
if (counts === null || typeof counts !== 'object' || Array.isArray(counts)) return false;
|
|
172
|
+
return pickInt(/** @type {Record<string, unknown>} */ (counts).passed) !== null;
|
|
173
|
+
}
|
|
154
174
|
|
|
155
175
|
/**
|
|
176
|
+
* Type-AND-shape gate: admit a record only when its event type is one this
|
|
177
|
+
* monitor classifies AND the record actually carries a measurement `classify`
|
|
178
|
+
* can fold. A bare prefix widening for `orchestrator.quality_gate.*` would
|
|
179
|
+
* re-open the #966 trap — the gate is the single highest-volume record type in
|
|
180
|
+
* `events.jsonl`, and admitting the session-level runs (no `wave_number`, or an
|
|
181
|
+
* envelope without `counts`) would instantiate a `WaveSummary` per gate run,
|
|
182
|
+
* advance `latestWave`, and burn the once-per-wave `alreadyEmitted` keys before
|
|
183
|
+
* the real wave record arrives. Hence the discriminator: a gate record is
|
|
184
|
+
* wave-scoped only when it has BOTH `wave_number` and a well-formed `counts`.
|
|
185
|
+
*
|
|
156
186
|
* @param {string} evType
|
|
187
|
+
* @param {Record<string, unknown>} rec
|
|
157
188
|
* @returns {boolean}
|
|
158
189
|
*/
|
|
159
|
-
function isWaveScopedEvent(evType) {
|
|
160
|
-
|
|
190
|
+
function isWaveScopedEvent(evType, rec = {}) {
|
|
191
|
+
if (evType.startsWith(WAVE_EVENT_PREFIX)) return true;
|
|
192
|
+
if (WAVE_EVENT_NAMES.has(evType) || evType === AGENT_STOPPED_EVENT) return true;
|
|
193
|
+
if (evType.startsWith(GATE_EVENT_PREFIX)) {
|
|
194
|
+
return pickInt(rec.wave_number) !== null && hasFoldableCounts(rec);
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
161
197
|
}
|
|
162
198
|
|
|
163
199
|
/**
|
|
@@ -179,12 +215,29 @@ function isWaveScopedEvent(evType) {
|
|
|
179
215
|
* `wave_number`, every gate run would have instantiated or refreshed a
|
|
180
216
|
* `WaveSummary`, advanced `latestWave`, and burnt the once-per-wave
|
|
181
217
|
* `alreadyEmitted` keys — suppressing the genuine signal when the real wave
|
|
182
|
-
* record arrived later. Several other high-volume types (`
|
|
183
|
-
* `
|
|
218
|
+
* record arrived later. Several other high-volume types (`turn.stopped` — and its
|
|
219
|
+
* deprecated alias `session.stopped`, GitLab #1234 — plus `memory.propose_invoked`)
|
|
220
|
+
* already carry a wave and sat in the same trap. The gate is an ALLOWLIST
|
|
221
|
+
* (`isWaveRelevantType`), so the rename needed no edit here: a new name is
|
|
222
|
+
* ignored by default, which is the correct default for this classifier.
|
|
184
223
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
224
|
+
* ## Where the pass count actually comes from (#980)
|
|
225
|
+
*
|
|
226
|
+
* Two independent paths, deliberately not merged:
|
|
227
|
+
* - the FLAT `rec['test.passed']` alias, kept for a future direct emitter on
|
|
228
|
+
* a wave-lifecycle record — no producer writes it today;
|
|
229
|
+
* - the gate envelope's NESTED `counts.passed`, folded ONLY for
|
|
230
|
+
* `orchestrator.quality_gate.*` records that also carry `wave_number`.
|
|
231
|
+
* That gating is what keeps the #966 invariant intact: an ungated record
|
|
232
|
+
* (a `wave.completed` carrying `counts`, or a session-level gate run with
|
|
233
|
+
* no `wave_number`) still folds NOTHING, so a gate run cannot manufacture a
|
|
234
|
+
* `WaveSummary` and burn the once-per-wave emit keys.
|
|
235
|
+
*
|
|
236
|
+
* `orchestrator.agent.stopped` is counted per record toward the wave's agent
|
|
237
|
+
* count, the same way `agent.dispatched` is — it is the only per-agent record
|
|
238
|
+
* this repo actually emits with a wave number (11,754 records measured
|
|
239
|
+
* 2026-09-05 in `.orchestrator/metrics/events.jsonl`, against 0 for
|
|
240
|
+
* `agent.dispatched`).
|
|
188
241
|
*
|
|
189
242
|
* @param {Record<string, unknown>} rec
|
|
190
243
|
* @param {Map<number, WaveSummary>} state
|
|
@@ -192,7 +245,7 @@ function isWaveScopedEvent(evType) {
|
|
|
192
245
|
*/
|
|
193
246
|
function classify(rec, state) {
|
|
194
247
|
const evType = String(rec.event_type ?? rec.event ?? '');
|
|
195
|
-
if (!isWaveScopedEvent(evType)) return null;
|
|
248
|
+
if (!isWaveScopedEvent(evType, rec)) return null;
|
|
196
249
|
|
|
197
250
|
const waveNumber = pickInt(rec.wave_number ?? rec.wave ?? rec.waveId);
|
|
198
251
|
if (waveNumber === null) return null;
|
|
@@ -202,6 +255,7 @@ function classify(rec, state) {
|
|
|
202
255
|
waveNumber,
|
|
203
256
|
filesChanged: null,
|
|
204
257
|
testPassed: null,
|
|
258
|
+
testFailed: null,
|
|
205
259
|
agentDispatchCount: null,
|
|
206
260
|
};
|
|
207
261
|
state.set(waveNumber, summary);
|
|
@@ -210,8 +264,17 @@ function classify(rec, state) {
|
|
|
210
264
|
if (filesChanged !== null) summary.filesChanged = filesChanged;
|
|
211
265
|
const testPassed = pickInt(rec['test.passed'] ?? rec.test_passed ?? rec.testsPassed);
|
|
212
266
|
if (testPassed !== null) summary.testPassed = testPassed;
|
|
213
|
-
//
|
|
214
|
-
|
|
267
|
+
// Nested gate envelope — admitted by isWaveScopedEvent only WITH wave_number,
|
|
268
|
+
// so the ungated #966 invariant (no fold) survives for every other record.
|
|
269
|
+
if (evType.startsWith(GATE_EVENT_PREFIX)) {
|
|
270
|
+
const counts = /** @type {Record<string, unknown>} */ (rec.counts);
|
|
271
|
+
const gatePassed = pickInt(counts.passed);
|
|
272
|
+
if (gatePassed !== null) summary.testPassed = gatePassed;
|
|
273
|
+
const gateFailed = pickInt(counts.failed);
|
|
274
|
+
if (gateFailed !== null) summary.testFailed = gateFailed;
|
|
275
|
+
}
|
|
276
|
+
// Count one dispatched/stopped agent record toward this wave's agent count.
|
|
277
|
+
if (WAVE_EVENT_NAMES.has(evType) || evType === AGENT_STOPPED_EVENT) {
|
|
215
278
|
summary.agentDispatchCount = (summary.agentDispatchCount ?? 0) + 1;
|
|
216
279
|
} else {
|
|
217
280
|
const dispatched = pickInt(rec.agents_dispatched ?? rec.agentsDispatched);
|
|
@@ -443,4 +506,7 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.a
|
|
|
443
506
|
main();
|
|
444
507
|
}
|
|
445
508
|
|
|
446
|
-
export
|
|
509
|
+
// `_evaluateSignals` is a test-only export (leading `_` per the repo convention,
|
|
510
|
+
// cf. `_clearCompileCache` in scripts/lib/agent-output-schema.mjs): the signal
|
|
511
|
+
// firing is otherwise reachable only through the live tail loop.
|
|
512
|
+
export { classify, isWaveScopedEvent, evaluateSignals as _evaluateSignals };
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { readdirSync, existsSync } from 'node:fs';
|
|
32
|
-
import os from 'node:os';
|
|
33
32
|
import path from 'node:path';
|
|
34
33
|
|
|
35
34
|
import { getConfinementRoot, getCrossRepoProjects } from '../config/cross-repo.mjs';
|
|
36
35
|
import { validatePathInsideProject } from '../path-utils.mjs';
|
|
37
36
|
import { readLock, isLockLive } from '../session-lock.mjs';
|
|
37
|
+
import { expandTilde } from '../common.mjs';
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* @typedef {Object} Candidate
|
|
@@ -97,21 +97,6 @@ function shouldDescendInto(name) {
|
|
|
97
97
|
return true;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
/**
|
|
101
|
-
* Expand a leading `~` to the current user's home directory. Mirrors the helper
|
|
102
|
-
* in board-writer.mjs (a shared extraction is deferred to a later epic). Used to
|
|
103
|
-
* normalise config-declared cross-repo paths that may begin with `~/`.
|
|
104
|
-
*
|
|
105
|
-
* @param {string} p
|
|
106
|
-
* @returns {string}
|
|
107
|
-
*/
|
|
108
|
-
function expandHome(p) {
|
|
109
|
-
if (typeof p !== 'string' || p.length === 0) return p;
|
|
110
|
-
if (p === '~') return os.homedir();
|
|
111
|
-
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
|
112
|
-
return p;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
100
|
/**
|
|
116
101
|
* Derive free/busy status for a single repo from its session.lock lease.
|
|
117
102
|
* Mirrors board-writer.mjs collectRows semantics exactly:
|
|
@@ -322,7 +307,7 @@ export async function enumerateCandidates({ startDir, now, deps } = {}) {
|
|
|
322
307
|
if (Array.isArray(declared)) {
|
|
323
308
|
for (const raw of declared) {
|
|
324
309
|
if (typeof raw !== 'string' || raw.length === 0) continue;
|
|
325
|
-
const expanded =
|
|
310
|
+
const expanded = expandTilde(raw);
|
|
326
311
|
const abs = path.resolve(expanded);
|
|
327
312
|
// Confinement-filter against the same root as the FS scan.
|
|
328
313
|
const guard = validatePathInsideProjectFn(abs, root);
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
* @typedef {{ repoRoot: string, repoName: string, free: boolean, status: 'frei'|'in-progress'|'force-closed', heartbeat: string|null, sessionId: string|null }} Candidate
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { readFileSync } from 'node:fs';
|
|
33
32
|
import path from 'node:path';
|
|
34
33
|
|
|
35
34
|
import { scanBacklog } from '../backlog-scan.mjs';
|
|
36
35
|
import { checkCiStatus as realCheckCiStatus } from '../ci-status-banner.mjs';
|
|
37
36
|
import { probe as realProbe, evaluate as realEvaluate, DEFAULT_RESOURCE_THRESHOLDS as CANONICAL_RESOURCE_THRESHOLDS } from '../resource-probe.mjs';
|
|
37
|
+
import { readCanonicalSessions } from '../sessions-canonical.mjs';
|
|
38
38
|
import { isRealSession } from '../session-schema/filters.mjs';
|
|
39
39
|
|
|
40
40
|
/** Staleness cap (days). Beyond this, additional age does not raise the score. */
|
|
@@ -154,17 +154,29 @@ async function defaultFetchPriority(repoRoot, nowMs) {
|
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
156
|
* Default STALENESS source: read `<repoRoot>/.orchestrator/metrics/sessions.jsonl`,
|
|
157
|
-
* find the
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
157
|
+
* find the MOST RECENT REAL (non-phantom) record's timestamp, and compute days
|
|
158
|
+
* since `completed_at` (fallback `started_at`). No file / no parsable REAL
|
|
159
|
+
* record / no timestamp ⇒ `STALENESS_CAP_DAYS` (treat as maximally stale =
|
|
160
|
+
* most worthwhile).
|
|
161
161
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
162
|
+
* Skips `status: 'abandoned'` phantom stubs (#834) — session-close-backfill
|
|
163
|
+
* writes these for sessions that ended without a real close (0 waves, seconds
|
|
164
|
+
* of runtime). Counting one would let a single recent phantom make a
|
|
165
|
+
* genuinely neglected repo look freshly touched, defeating the dispatcher's
|
|
166
|
+
* whole purpose (this is the N=1 extreme case of the phantom-tail problem —
|
|
167
|
+
* one stub is enough to zero out staleness).
|
|
168
|
+
*
|
|
169
|
+
* #1209: takes the MAX timestamp over every canonical REAL record instead of
|
|
170
|
+
* scanning backward through raw file lines from the tail. A raw backward scan
|
|
171
|
+
* trusts FILE POSITION as a proxy for recency, which silently breaks the
|
|
172
|
+
* moment a backfill/repair writer appends an OLDER record after genuinely
|
|
173
|
+
* newer ones already exist — `session-record-repair.mjs` and
|
|
174
|
+
* `migrate-sessions-jsonl.mjs` both do this by design (see
|
|
175
|
+
* `sessions-canonical.mjs`'s module header, "by-design raw" list). A
|
|
176
|
+
* duplicated `session_id` re-appended late at the tail would make the old
|
|
177
|
+
* backward scan report the STALE re-append's date instead of the genuinely
|
|
178
|
+
* newer session's date; `readCanonicalSessions` collapses the duplicate to
|
|
179
|
+
* one record first, so the max-reduce below sees only the true timestamps.
|
|
168
180
|
*
|
|
169
181
|
* @param {string} repoRoot
|
|
170
182
|
* @param {number} nowMs
|
|
@@ -173,33 +185,21 @@ async function defaultFetchPriority(repoRoot, nowMs) {
|
|
|
173
185
|
async function defaultStaleDaysFor(repoRoot, nowMs) {
|
|
174
186
|
try {
|
|
175
187
|
const file = path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
} catch {
|
|
188
|
-
continue; // Skip a corrupt line and try the previous one.
|
|
189
|
-
}
|
|
190
|
-
if (isRealSession(parsed)) {
|
|
191
|
-
last = parsed;
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
188
|
+
const records = readCanonicalSessions({ filePath: file });
|
|
189
|
+
if (records.length === 0) return STALENESS_CAP_DAYS;
|
|
190
|
+
|
|
191
|
+
let latestMs = null;
|
|
192
|
+
for (const rec of records) {
|
|
193
|
+
if (!isRealSession(rec)) continue;
|
|
194
|
+
const iso = rec.completed_at || rec.started_at || null;
|
|
195
|
+
if (!iso || typeof iso !== 'string') continue;
|
|
196
|
+
const t = Date.parse(iso);
|
|
197
|
+
if (Number.isNaN(t)) continue;
|
|
198
|
+
if (latestMs === null || t > latestMs) latestMs = t;
|
|
194
199
|
}
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
const iso = last.completed_at || last.started_at || null;
|
|
198
|
-
if (!iso || typeof iso !== 'string') return STALENESS_CAP_DAYS;
|
|
199
|
-
const t = Date.parse(iso);
|
|
200
|
-
if (Number.isNaN(t)) return STALENESS_CAP_DAYS;
|
|
200
|
+
if (latestMs === null) return STALENESS_CAP_DAYS;
|
|
201
201
|
|
|
202
|
-
const days = (nowMs -
|
|
202
|
+
const days = (nowMs - latestMs) / MS_PER_DAY;
|
|
203
203
|
return days > 0 ? days : 0;
|
|
204
204
|
} catch {
|
|
205
205
|
// No sessions file (or unreadable) ⇒ never worked on ⇒ maximally stale.
|
|
@@ -209,21 +209,75 @@ async function defaultStaleDaysFor(repoRoot, nowMs) {
|
|
|
209
209
|
|
|
210
210
|
/**
|
|
211
211
|
* Default READINESS (CI) source: thin wrapper over `checkCiStatus`.
|
|
212
|
-
*
|
|
213
|
-
*
|
|
212
|
+
*
|
|
213
|
+
* Passes the probe's result through VERBATIM rather than flattening it to a
|
|
214
|
+
* status string (#1031 follow-up). `checkCiStatus` has THREE return states —
|
|
215
|
+
* `null` (benign absence), `{status, …}` (a real reading), and
|
|
216
|
+
* `{severity:'warn', degraded:<reason>}` (the state could NOT be read) — and
|
|
217
|
+
* the old `typeof result.status === 'string' ? result.status : null` collapsed
|
|
218
|
+
* the third onto the first. That was not a scoring bug (both are non-blocking)
|
|
219
|
+
* but it discarded the one thing the operator needs to know: the ranking was
|
|
220
|
+
* computed WITHOUT a CI reading, and why. {@link normalizeCiSignal} in
|
|
221
|
+
* `rankCandidates` performs the shape reduction and keeps the reason.
|
|
222
|
+
*
|
|
223
|
+
* A THROW is the fourth state, and it is NOT the first: `checkCiStatus` is
|
|
224
|
+
* documented never to throw, so an exception here means the probe itself broke
|
|
225
|
+
* (a bad import, an unexpected runtime error). Returning `null` for it would
|
|
226
|
+
* assert measured ABSENCE — the one thing we know is false. It is mapped to the
|
|
227
|
+
* degraded shape instead, so {@link normalizeCiSignal} surfaces it as
|
|
228
|
+
* `ciStatus: 'unknown'` + `ciDegraded: 'probe-threw'` rather than silently.
|
|
214
229
|
*
|
|
215
230
|
* @param {{ repoRoot: string }} args
|
|
216
|
-
* @returns {Promise<'green'|'red'|'unknown'|null>}
|
|
231
|
+
* @returns {Promise<'green'|'red'|'unknown'|null|object>} raw probe result.
|
|
217
232
|
*/
|
|
218
233
|
async function defaultCheckCiStatus({ repoRoot }) {
|
|
219
234
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return
|
|
235
|
+
return (await realCheckCiStatus({ repoRoot })) ?? null;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
238
|
+
return {
|
|
239
|
+
severity: 'warn',
|
|
240
|
+
ok: false,
|
|
241
|
+
message: `⚠ ci-status: CI probe threw — state UNKNOWN, not "green". ${detail}`,
|
|
242
|
+
degraded: 'probe-threw',
|
|
243
|
+
};
|
|
224
244
|
}
|
|
225
245
|
}
|
|
226
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Reduce whatever a `checkCiStatus` dep returned to the pair the ranking needs:
|
|
249
|
+
* a scoring status and — when the state was UNREADABLE — the reason.
|
|
250
|
+
*
|
|
251
|
+
* Accepts every shape the dep contract permits, old and new:
|
|
252
|
+
* - a bare status string (`'green'`/`'red'`/`'unknown'`) — the shape every
|
|
253
|
+
* injected test dep uses; passed through unchanged.
|
|
254
|
+
* - `null`/`undefined` — genuine absence, no signal, no reason.
|
|
255
|
+
* - `{status: <string>}` — a real reading from `checkCiStatus`.
|
|
256
|
+
* - `{degraded: <reason>}` with no usable `status` — the probe FAILED. Mapped
|
|
257
|
+
* to `'unknown'`, which `scoreCandidate` already treats as non-blocking
|
|
258
|
+
* (`ciFactor` dampens on `'red'` only), so ranking is byte-identical to the
|
|
259
|
+
* previous `null` — but the reason survives into `readiness.ciDegraded`
|
|
260
|
+
* and into a `warnings` entry.
|
|
261
|
+
* - anything else — `null`, as before.
|
|
262
|
+
*
|
|
263
|
+
* `ciDegraded` is `null` (and the caller OMITS the key) whenever the state was
|
|
264
|
+
* readable, so existing strict `toEqual` pins on `signals.readiness` hold.
|
|
265
|
+
*
|
|
266
|
+
* @param {unknown} raw
|
|
267
|
+
* @returns {{ ciStatus: 'green'|'red'|'unknown'|null, ciDegraded: string|null }}
|
|
268
|
+
*/
|
|
269
|
+
export function normalizeCiSignal(raw) {
|
|
270
|
+
if (typeof raw === 'string' && raw) return { ciStatus: raw, ciDegraded: null };
|
|
271
|
+
if (!raw || typeof raw !== 'object') return { ciStatus: null, ciDegraded: null };
|
|
272
|
+
if (typeof raw.status === 'string' && raw.status) {
|
|
273
|
+
return { ciStatus: raw.status, ciDegraded: null };
|
|
274
|
+
}
|
|
275
|
+
if (typeof raw.degraded === 'string' && raw.degraded) {
|
|
276
|
+
return { ciStatus: 'unknown', ciDegraded: raw.degraded };
|
|
277
|
+
}
|
|
278
|
+
return { ciStatus: null, ciDegraded: null };
|
|
279
|
+
}
|
|
280
|
+
|
|
227
281
|
/**
|
|
228
282
|
* Default READINESS (resource) source: probe the host ONCE and evaluate against
|
|
229
283
|
* the canonical default thresholds. This is a HOST-level signal (identical for
|
|
@@ -290,7 +344,7 @@ export function defaultDeps() {
|
|
|
290
344
|
* ranked: Array<{ candidate: Candidate, score: number, signals: {
|
|
291
345
|
* priority: { criticalCount: number, highCount: number } | null,
|
|
292
346
|
* staleDays: number,
|
|
293
|
-
* readiness: { ciStatus: 'green'|'red'|'unknown'|null, resourceVerdict: 'green'|'warn'|'degraded'|'critical' },
|
|
347
|
+
* readiness: { ciStatus: 'green'|'red'|'unknown'|null, resourceVerdict: 'green'|'warn'|'degraded'|'critical', ciDegraded?: string },
|
|
294
348
|
* } }>,
|
|
295
349
|
* warnings: string[],
|
|
296
350
|
* }>}
|
|
@@ -342,18 +396,40 @@ export async function rankCandidates(freeCandidates, opts = {}) {
|
|
|
342
396
|
staleDays = STALENESS_CAP_DAYS;
|
|
343
397
|
}
|
|
344
398
|
|
|
345
|
-
// READINESS — CI status.
|
|
399
|
+
// READINESS — CI status. A degraded probe result ("state unknown") is
|
|
400
|
+
// NON-BLOCKING but not INVISIBLE: it scores like `null`, and reports why.
|
|
401
|
+
// Declared WITHOUT an initialiser on purpose: both paths below assign, and
|
|
402
|
+
// a `= null` seed would quietly re-create a fall-through "absent" default.
|
|
346
403
|
let ciStatus;
|
|
404
|
+
let ciDegraded;
|
|
347
405
|
try {
|
|
348
|
-
ciStatus = await deps.checkCiStatus({ repoRoot });
|
|
406
|
+
({ ciStatus, ciDegraded } = normalizeCiSignal(await deps.checkCiStatus({ repoRoot })));
|
|
349
407
|
} catch {
|
|
350
|
-
|
|
408
|
+
// A THROWING dep is not measured ABSENCE. Leaving `ciDegraded` at null
|
|
409
|
+
// here re-created the exact collapse `normalizeCiSignal` exists to
|
|
410
|
+
// prevent: the ranking silently proceeded as if no CI signal existed,
|
|
411
|
+
// when in truth the probe broke. Scoring is unchanged (`'unknown'` is
|
|
412
|
+
// non-blocking) — only the reason becomes visible.
|
|
413
|
+
ciStatus = 'unknown';
|
|
414
|
+
ciDegraded = 'probe-threw';
|
|
415
|
+
}
|
|
416
|
+
if (ciDegraded) {
|
|
417
|
+
warnings.push(
|
|
418
|
+
`CI state unknown for ${repoName} (${ciDegraded}) — ranked without CI dampening`,
|
|
419
|
+
);
|
|
351
420
|
}
|
|
352
421
|
|
|
353
422
|
const signals = {
|
|
354
423
|
priority,
|
|
355
424
|
staleDays,
|
|
356
|
-
readiness: {
|
|
425
|
+
readiness: {
|
|
426
|
+
ciStatus,
|
|
427
|
+
resourceVerdict,
|
|
428
|
+
// Key OMITTED when the CI state was readable — a strict `toEqual` on
|
|
429
|
+
// `signals.readiness` must not have to know about a field that only
|
|
430
|
+
// exists on the failure path.
|
|
431
|
+
...(ciDegraded ? { ciDegraded } : {}),
|
|
432
|
+
},
|
|
357
433
|
};
|
|
358
434
|
const score = scoreCandidate(signals);
|
|
359
435
|
|
|
@@ -164,13 +164,27 @@ async function watchLoop(intervalS) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/**
|
|
167
|
+
* Poll delay.
|
|
168
|
+
*
|
|
169
|
+
* The timer is deliberately NOT `unref()`d: it is the only handle this process
|
|
170
|
+
* holds (the two signal handlers do not keep the loop alive), so an unref'd
|
|
171
|
+
* timer drains the event loop and node exits 0 the instant the first tick is
|
|
172
|
+
* scheduled — a watcher that supervises nothing while looking like a clean
|
|
173
|
+
* shutdown, because it exits 0 with an empty stderr. Measured 2026-09-06 on the
|
|
174
|
+
* unref'd variant: `node scripts/lib/ecosystem-health.mjs --watch --interval=1`
|
|
175
|
+
* returned exit 0 after 48 ms instead of running until SIGTERM. Third copy of
|
|
176
|
+
* the #980 defect A1 measured in `scripts/lib/wave-transcript-tail.mjs` and
|
|
177
|
+
* `scripts/lib/convergence-monitor.mjs`; this one was missed when those two
|
|
178
|
+
* were fixed. Pinned by a DURATION assertion (an exit-code assertion cannot
|
|
179
|
+
* tell a healthy monitor from a dead one) in
|
|
180
|
+
* `tests/lib/ecosystem-health-watch.test.mjs`.
|
|
181
|
+
*
|
|
167
182
|
* @param {number} ms
|
|
168
183
|
* @returns {Promise<void>}
|
|
169
184
|
*/
|
|
170
185
|
function sleep(ms) {
|
|
171
186
|
return new Promise((resolve) => {
|
|
172
|
-
|
|
173
|
-
t.unref?.();
|
|
187
|
+
setTimeout(resolve, ms);
|
|
174
188
|
});
|
|
175
189
|
}
|
|
176
190
|
|
|
@@ -36,6 +36,7 @@ import path from 'node:path';
|
|
|
36
36
|
|
|
37
37
|
import { resolvePluginRoot } from '../common.mjs';
|
|
38
38
|
import { readJsonlFile } from '../io.mjs';
|
|
39
|
+
import { readCanonicalSessions } from '../sessions-canonical.mjs';
|
|
39
40
|
import { buildRunId, CURRENT_STANDARD_VERSION, VALID_MODEL_SOURCES } from './schema.mjs';
|
|
40
41
|
import { resolveSession, computeWindow, findPeerOverlap } from './session-resolve.mjs';
|
|
41
42
|
|
|
@@ -538,7 +539,14 @@ export function evaluateSession(opts = {}) {
|
|
|
538
539
|
|
|
539
540
|
const sessionsPath = path.join(metricsDir, 'sessions.jsonl');
|
|
540
541
|
const eventsPath = path.join(metricsDir, 'events.jsonl');
|
|
541
|
-
|
|
542
|
+
// #1209: sessions.jsonl is APPEND-ONLY (the same physical session can carry
|
|
543
|
+
// more than one line — crash-recovery re-appends, #1068 stub/supersede
|
|
544
|
+
// pairs), so a raw readJsonlFile() left resolveSession()/findPeerOverlap()
|
|
545
|
+
// to hand-roll their own dedup over duplicated / phantom-doubled records.
|
|
546
|
+
// readCanonicalSessions() collapses those first (newest-wins per
|
|
547
|
+
// session_id, #1068 double-stub collapse, supersede removal) — see
|
|
548
|
+
// session-resolve.mjs for how that simplifies both callers below.
|
|
549
|
+
const records = readCanonicalSessions({ filePath: sessionsPath });
|
|
542
550
|
const events = readJsonlFile(eventsPath, { skipInvalid: true });
|
|
543
551
|
|
|
544
552
|
const { record: session, resolvedVia } = resolveSession(records, sessionId);
|
|
@@ -48,11 +48,17 @@ export function resolveSession(records, sessionId) {
|
|
|
48
48
|
|
|
49
49
|
// Explicit selection: the LAST record carrying this session_id (records may be
|
|
50
50
|
// rewritten/backfilled across a session's life; the latest is authoritative).
|
|
51
|
+
//
|
|
52
|
+
// #1209: engine.mjs's production caller now passes CANONICAL records
|
|
53
|
+
// (readCanonicalSessions already resolved newest-wins per session_id before
|
|
54
|
+
// this array reaches here — see sessions-canonical.mjs), so at most one
|
|
55
|
+
// record can carry a given id in practice. `resolveSession` stays a
|
|
56
|
+
// general, pure function — `findLast` keeps the exact "last match wins"
|
|
57
|
+
// semantics for a caller that hands in raw, un-deduped records directly
|
|
58
|
+
// (e.g. the unit tests below), while degenerating to a single candidate on
|
|
59
|
+
// the canonical path.
|
|
51
60
|
if (sessionId) {
|
|
52
|
-
|
|
53
|
-
for (const r of records) {
|
|
54
|
-
if (isPlainObject(r) && r.session_id === sessionId) match = r;
|
|
55
|
-
}
|
|
61
|
+
const match = records.findLast((r) => isPlainObject(r) && r.session_id === sessionId) ?? null;
|
|
56
62
|
if (!match) {
|
|
57
63
|
throw new SessionResolutionError(`session not found: ${sessionId}`);
|
|
58
64
|
}
|
|
@@ -114,6 +120,19 @@ export function computeWindow(record) {
|
|
|
114
120
|
* rewrites of the SAME session) are excluded, as are records without a valid
|
|
115
121
|
* window.
|
|
116
122
|
*
|
|
123
|
+
* #1209: the #1068 double-stub class — two ABANDONED records for ONE physical
|
|
124
|
+
* session (a real semantic `session_id` + a synthetic backfill twin, sharing
|
|
125
|
+
* an exact `started_at`/`completed_at` tuple, see `sessions-canonical.mjs`
|
|
126
|
+
* rule 2) — used to be counted as TWO distinct peers here, because the raw
|
|
127
|
+
* `records` array carried both lines under different ids and this function has
|
|
128
|
+
* no way to know they are the same physical session. With engine.mjs's
|
|
129
|
+
* production caller now passing `readCanonicalSessions()` output,
|
|
130
|
+
* `collapseAbandonedTuples()` has already dropped the synthetic twin before
|
|
131
|
+
* this array reaches here — a double-stub pair can therefore no longer
|
|
132
|
+
* produce a false EXTRA peer (`count` inflated by one) on the canonical path.
|
|
133
|
+
* `findPeerOverlap` itself stays general/pure for callers (including the unit
|
|
134
|
+
* tests below) that pass raw, un-deduped records directly.
|
|
135
|
+
*
|
|
117
136
|
* @param {object[]} records — all sessions.jsonl records.
|
|
118
137
|
* @param {object} resolved — the resolved session record.
|
|
119
138
|
* @returns {{ count: number, peers: string[] }} unique overlapping session_ids.
|
|
@@ -12,6 +12,54 @@
|
|
|
12
12
|
* Pure functions, no filesystem access — safe to import anywhere.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Current events.jsonl record schema version (#1177). Mirrors
|
|
17
|
+
* `subagents-schema.mjs § CURRENT_SCHEMA_VERSION`.
|
|
18
|
+
*
|
|
19
|
+
* Records written before #1177 carry NO `schema_version` key at all; an absent
|
|
20
|
+
* key therefore reads as "pre-versioned", never as version 0. Stamping is
|
|
21
|
+
* strictly additive — see `stampEventSchemaVersion()`.
|
|
22
|
+
*/
|
|
23
|
+
export const CURRENT_SCHEMA_VERSION = 1;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Thrown by `emitEvent()` when a record fails `validateEventRecord()`.
|
|
27
|
+
*
|
|
28
|
+
* Carries the individual validator messages so a CLI/hook caller can surface
|
|
29
|
+
* them without re-running the validator.
|
|
30
|
+
*/
|
|
31
|
+
export class EventValidationError extends Error {
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} message — human-readable summary.
|
|
34
|
+
* @param {string[]} [errors=[]] — the validator's individual error strings.
|
|
35
|
+
* @param {string} [eventType] — the offending event name, when known.
|
|
36
|
+
*/
|
|
37
|
+
constructor(message, errors = [], eventType) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'EventValidationError';
|
|
40
|
+
this.errors = errors;
|
|
41
|
+
this.eventType = eventType;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Return a shallow copy of `record` with `schema_version` stamped to
|
|
47
|
+
* `CURRENT_SCHEMA_VERSION` — but ONLY when the field is absent
|
|
48
|
+
* (`undefined`/`null`). An existing value is never overwritten, so a caller
|
|
49
|
+
* (or a migration re-writing historical records) keeps authority over its own
|
|
50
|
+
* version field.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} record
|
|
53
|
+
* @returns {object} shallow copy, `schema_version` guaranteed present.
|
|
54
|
+
*/
|
|
55
|
+
export function stampEventSchemaVersion(record) {
|
|
56
|
+
const out = { ...record };
|
|
57
|
+
if (out.schema_version === undefined || out.schema_version === null) {
|
|
58
|
+
out.schema_version = CURRENT_SCHEMA_VERSION;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
15
63
|
/** ISO-8601 UTC timestamp with trailing Z (e.g. 2026-05-28T14:35:13.123Z). */
|
|
16
64
|
const ISO_8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
17
65
|
|