session-orchestrator 5.12.0 → 5.13.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/agents/explorer.toml +1 -1
- package/.codex-plugin/agents/session-reviewer.toml +2 -2
- package/.codex-plugin/agents/wave-worker.toml +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +8 -0
- package/.orchestrator/policy/public-export.json +30 -0
- package/CHANGELOG.md +162 -4
- package/README.md +126 -127
- package/agents/analyst.md +7 -2
- package/agents/architect-reviewer.md +7 -2
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +9 -2
- package/agents/dialectic-deriver.md +9 -3
- package/agents/docs-writer.md +7 -2
- package/agents/eval-judge.md +7 -1
- package/agents/qa-strategist.md +7 -2
- package/agents/security-reviewer.md +9 -2
- package/agents/session-reviewer.md +18 -2
- package/agents/skill-applied-judge.md +7 -1
- package/agents/test-writer.md +9 -2
- package/agents/ui-developer.md +9 -2
- package/agents/ux-evaluator.md +6 -1
- package/commands/session.md +1 -1
- package/commands/templates-ack.md +89 -18
- package/docs/agent-authoring.md +30 -11
- package/docs/ci-setup.md +38 -2
- package/docs/codex-setup.md +33 -9
- package/docs/components.md +13 -2
- package/docs/cursor-setup.md +3 -3
- package/docs/events-schema.md +5 -4
- package/docs/evidence-candidates.md +63 -0
- package/docs/fleet-checkin-audit.md +7 -4
- package/docs/github-mirror-protection.md +150 -3
- package/docs/install.md +90 -17
- package/docs/instruction-delivery.md +1 -1
- package/docs/migration-v4.md +2 -2
- package/docs/mods.md +118 -0
- package/docs/owner-config-schema.md +1 -0
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/pm-skills-marketplace.md +4 -3
- package/docs/rule-authoring.md +24 -13
- package/docs/scope-collision-guard.md +2 -1
- package/docs/session-config-reference.md +49 -15
- package/docs/session-config-template.md +30 -22
- package/docs/sessions-ledger-repair.md +137 -0
- package/docs/vault-docs-architecture.md +1 -1
- package/hooks/_lib/hook-import-set.json +406 -73
- package/hooks/_lib/hook-root.mjs +301 -0
- package/hooks/_lib/lock-bootstrap.mjs +434 -4
- package/hooks/_lib/orchestrator-ignore.mjs +3 -0
- package/hooks/_lib/subagent-transcript.mjs +2 -2
- package/hooks/_lib/vcs-create-matcher.mjs +13 -17
- package/hooks/cwd-change-restore.mjs +5 -1
- package/hooks/enforce-commands.mjs +4 -3
- package/hooks/enforce-scope.mjs +11 -1
- package/hooks/hooks-codex.json +48 -2
- package/hooks/hooks.json +31 -31
- package/hooks/on-session-end.mjs +38 -15
- package/hooks/on-session-start.mjs +139 -49
- package/hooks/on-stop.mjs +11 -6
- package/hooks/post-bash-issue-budget-refund.mjs +6 -1
- package/hooks/post-bash-write-verify.mjs +8 -4
- package/hooks/post-edit-validate.mjs +45 -52
- package/hooks/post-subagent-discovery-validator.mjs +2 -2
- package/hooks/post-tool-batch-wave-signal.mjs +8 -4
- package/hooks/post-tool-failure-corrective-context.mjs +5 -1
- package/hooks/pre-bash-destructive-guard.mjs +220 -8
- package/hooks/pre-bash-issue-budget.mjs +6 -1
- package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
- package/hooks/pre-bash-staging-fence.mjs +258 -14
- package/hooks/pre-bash-templates-first.mjs +28 -27
- package/hooks/run-node.sh +1 -1
- package/hooks/skill-invocation-telemetry.mjs +15 -8
- package/hooks/subagent-telemetry.mjs +77 -14
- package/hooks/wave-scope-commit-guard.mjs +143 -14
- package/mods/so-hud/.claude-plugin/plugin.json +11 -0
- package/mods/so-hud/README.md +57 -0
- package/mods/so-hud/hooks/beacon-io.mjs +101 -0
- package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
- package/mods/so-hud/hooks/hooks.json +1 -0
- package/mods/so-hud/hooks/logic.ts +182 -0
- package/mods/so-hud/hooks/register.tsx +670 -0
- package/mods/so-hud/hooks/rules.ts +2998 -0
- package/mods/so-hud/tsconfig.json +8 -0
- package/mods/so-hud/types/index.d.ts +36 -0
- package/package.json +4 -2
- package/rules/_index.md +1 -1
- package/rules/always-on/ask-via-tool.md +1 -1
- package/rules/always-on/loop-and-monitor.md +17 -194
- package/rules/always-on/parallel-sessions.md +3 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +1 -1
- package/rules/opt-in-stack/security-web.md +5 -1
- package/scripts/agent-usage-census.mjs +79 -0
- package/scripts/apply-session-learnings.mjs +34 -6
- package/scripts/archive-closed-prds.mjs +21 -5
- package/scripts/autopilot.mjs +11 -5
- package/scripts/backfill-abandoned-sessions.mjs +13 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +61 -6
- package/scripts/backfill-sessions.mjs +16 -2
- package/scripts/check-package-manager.mjs +26 -8
- package/scripts/check-sessions-integrity.mjs +4 -1
- package/scripts/ci/assert-vitest-green.mjs +3 -0
- package/scripts/ci/windows-smoke.mjs +201 -0
- package/scripts/close-precheck.mjs +55 -0
- package/scripts/dialectic-deriver.mjs +20 -12
- package/scripts/emit-event.mjs +2 -2
- package/scripts/emit-session.mjs +239 -20
- package/scripts/eval-session.mjs +17 -3
- package/scripts/export-hw-learnings.mjs +29 -2
- package/scripts/fleet-checkin-audit.mjs +8 -4
- package/scripts/fleet-checkin.mjs +39 -27
- package/scripts/gc-stale-worktrees.mjs +6 -1
- package/scripts/github-export.mjs +128 -0
- package/scripts/github-protection-audit.mjs +3 -1
- package/scripts/handover-queue-check.mjs +209 -0
- package/scripts/harness-audit.mjs +6 -4
- package/scripts/lib/agent-frontmatter.mjs +57 -10
- package/scripts/lib/agent-status.mjs +125 -7
- package/scripts/lib/agent-usage-census.mjs +394 -0
- package/scripts/lib/auto-dialectic.mjs +17 -10
- package/scripts/lib/auto-dream.mjs +22 -14
- package/scripts/lib/autopilot/loop.mjs +3 -1
- package/scripts/lib/autopilot/recent-runs.mjs +4 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
- package/scripts/lib/autopilot/telemetry.mjs +5 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/baseline-archetypes.mjs +10 -2
- package/scripts/lib/blocked-commands-policy.mjs +4 -3
- package/scripts/lib/build-live-signals.mjs +13 -9
- package/scripts/lib/ci-status-banner.mjs +329 -24
- package/scripts/lib/codex/plugin-contract.mjs +8 -0
- package/scripts/lib/cold-start-detector.mjs +5 -2
- package/scripts/lib/command-blocker.mjs +603 -43
- package/scripts/lib/common.mjs +3 -2
- package/scripts/lib/config/cross-repo.mjs +74 -7
- package/scripts/lib/config/host-paths.mjs +6 -2
- package/scripts/lib/config/section-extractor.mjs +65 -11
- package/scripts/lib/config/template-keys.mjs +153 -0
- package/scripts/lib/config/vault-integration.mjs +1 -0
- package/scripts/lib/config.mjs +2 -2
- package/scripts/lib/coordinator-snapshot.mjs +1 -1
- package/scripts/lib/discovery/triage-state.mjs +2 -1
- package/scripts/lib/dispatcher/rank.mjs +5 -7
- package/scripts/lib/ecosystem-health.mjs +2 -2
- package/scripts/lib/eval/engine.mjs +3 -2
- package/scripts/lib/eval/session-resolve.mjs +6 -2
- package/scripts/lib/eval/sink.mjs +4 -3
- package/scripts/lib/events-retention-banner.mjs +3 -2
- package/scripts/lib/events-schema.mjs +10 -0
- package/scripts/lib/events.mjs +23 -12
- package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
- package/scripts/lib/fleet-protocol.mjs +4 -0
- package/scripts/lib/gates/gate-full.mjs +172 -11
- package/scripts/lib/gates/gate-helpers.mjs +236 -23
- package/scripts/lib/git-config-drift.mjs +141 -7
- package/scripts/lib/github-export.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
- package/scripts/lib/handover-queue-marks.mjs +159 -0
- package/scripts/lib/hardware-pattern-detector.mjs +103 -13
- package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
- package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
- package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
- package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
- package/scripts/lib/host-identity.mjs +9 -5
- package/scripts/lib/http-request.mjs +64 -0
- package/scripts/lib/instruction-budget-guard.mjs +30 -22
- package/scripts/lib/io.mjs +6 -3
- package/scripts/lib/issue-budget-reconcile.mjs +3 -2
- package/scripts/lib/issue-budget.mjs +36 -7
- package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
- package/scripts/lib/learnings/io.mjs +289 -15
- package/scripts/lib/learnings/select.mjs +53 -4
- package/scripts/lib/learnings/store-role.mjs +209 -0
- package/scripts/lib/learnings/surface.mjs +46 -7
- package/scripts/lib/ledger-path.mjs +113 -0
- package/scripts/lib/maintenance-due-banner.mjs +63 -12
- package/scripts/lib/memory-banner.mjs +5 -6
- package/scripts/lib/memory-proposals/collector.mjs +5 -2
- package/scripts/lib/memory-proposals/sink.mjs +7 -4
- package/scripts/lib/memory-proposals/store.mjs +22 -5
- package/scripts/lib/mode-selector-accuracy.mjs +2 -1
- package/scripts/lib/no-follow-open.mjs +57 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
- package/scripts/lib/own-lock-root.mjs +95 -0
- package/scripts/lib/owner-interview.mjs +2 -1
- package/scripts/lib/owner-yaml.mjs +70 -7
- package/scripts/lib/platform.mjs +161 -9
- package/scripts/lib/plugin-update-banner.mjs +15 -11
- package/scripts/lib/pre-push-local-partial.mjs +117 -0
- package/scripts/lib/pre-push-remote-gate.mjs +151 -25
- package/scripts/lib/process-group.mjs +270 -34
- package/scripts/lib/project-hygiene.mjs +28 -0
- package/scripts/lib/quality-gate.mjs +33 -9
- package/scripts/lib/reconcile/backlog.mjs +29 -15
- package/scripts/lib/reconcile/engine.mjs +26 -2
- package/scripts/lib/reconcile/idempotency.mjs +65 -3
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
- package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
- package/scripts/lib/resource-probe/evaluate.mjs +32 -9
- package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
- package/scripts/lib/rule-loader.mjs +20 -10
- package/scripts/lib/scope-baseline.mjs +123 -8
- package/scripts/lib/scope-echo.mjs +5 -2
- package/scripts/lib/scope-gate.mjs +55 -20
- package/scripts/lib/session-close-backfill.mjs +75 -7
- package/scripts/lib/session-end/close-precheck.mjs +151 -0
- package/scripts/lib/session-end/phase-skip.mjs +8 -7
- package/scripts/lib/session-end/tail-runner.mjs +7 -2
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +72 -9
- package/scripts/lib/session-schema/constants.mjs +9 -0
- package/scripts/lib/session-schema/filters.mjs +56 -1
- package/scripts/lib/session-schema/validator.mjs +15 -5
- package/scripts/lib/session-start-probes.mjs +149 -22
- package/scripts/lib/session-token-rollup.mjs +2 -1
- package/scripts/lib/session-work-trees.mjs +166 -0
- package/scripts/lib/sessions-canonical.mjs +61 -3
- package/scripts/lib/sessions-integrity-banner.mjs +17 -39
- package/scripts/lib/sessions-staleness-banner.mjs +8 -5
- package/scripts/lib/sh-quote.mjs +1 -0
- package/scripts/lib/skill-evidence-window.mjs +5 -1
- package/scripts/lib/skill-health/join.mjs +10 -9
- package/scripts/lib/skill-invocations-schema.mjs +13 -0
- package/scripts/lib/skill-judgments-schema.mjs +6 -2
- package/scripts/lib/snapshot-probe.mjs +414 -0
- package/scripts/lib/so-hud/collect.mjs +49 -5
- package/scripts/lib/so-hud/runtime.mjs +15 -4
- package/scripts/lib/spiral-carryover.mjs +8 -1
- package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
- package/scripts/lib/state-md/idle-reset.mjs +14 -5
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md-peer-guard.mjs +2 -0
- package/scripts/lib/state-md.mjs +31 -8
- package/scripts/lib/sunset/walker.mjs +4 -3
- package/scripts/lib/telemetry/consent.mjs +4 -2
- package/scripts/lib/telemetry/queue.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +45 -9
- package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
- package/scripts/lib/tmux-layout/layouts.mjs +20 -3
- package/scripts/lib/ux-grill/paths.mjs +5 -1
- package/scripts/lib/validate/check-agents.mjs +37 -6
- package/scripts/lib/validate/check-component-paths.mjs +25 -1
- package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
- package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
- package/scripts/lib/validate/check-json-files.mjs +3 -1
- package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
- package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
- package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
- package/scripts/lib/validate/check-unwired-features.mjs +4 -2
- package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
- package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
- package/scripts/lib/validate/confidential-names.mjs +3 -1
- package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
- package/scripts/lib/vault-mirror/namespace.mjs +19 -7
- package/scripts/lib/vault-mirror/process.mjs +56 -27
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
- package/scripts/lib/vault-mirror/retention.mjs +2 -2
- package/scripts/lib/vault-mirror/utils.mjs +79 -0
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +2 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
- package/scripts/lib/win-process-tree.mjs +137 -0
- package/scripts/lib/workspace.mjs +1 -1
- package/scripts/lib/worktree/constants.mjs +1 -1
- package/scripts/lib/worktree/lifecycle.mjs +1 -1
- package/scripts/lib/worktree/listing.mjs +1 -1
- package/scripts/lib/zx.mjs +50 -0
- package/scripts/mcp-server.mjs +16 -7
- package/scripts/mcp-server.sh +13 -83
- package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
- package/scripts/memory-propose.mjs +10 -3
- package/scripts/migrate-cold-start-seed.mjs +7 -0
- package/scripts/migrate-legacy-learnings.sh +22 -0
- package/scripts/migrate-sessions-jsonl.mjs +16 -2
- package/scripts/migrate-vault-paths.mjs +29 -13
- package/scripts/parse-config.mjs +71 -14
- package/scripts/print-learnings-index.mjs +25 -2
- package/scripts/release-session-lock.mjs +94 -7
- package/scripts/release.mjs +451 -24
- package/scripts/relocate-vault-corpus.mjs +11 -7
- package/scripts/repair-invalid-sessions.mjs +68 -3
- package/scripts/resolve-session-invocation.mjs +10 -17
- package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
- package/scripts/run-quality-gate.mjs +33 -7
- package/scripts/self-update.mjs +56 -7
- package/scripts/session-commit-files.mjs +133 -0
- package/scripts/site-numbers.mjs +52 -9
- package/scripts/snapshot-probe.mjs +156 -0
- package/scripts/so-hud.mjs +29 -6
- package/scripts/state-md-idle-reset.mjs +2 -1
- package/scripts/sweep-expired-learnings.mjs +54 -6
- package/scripts/sweep-expired-rules.mjs +4 -1
- package/scripts/validate-plugin.mjs +22 -0
- package/scripts/vault-integration-watcher.mjs +54 -6
- package/scripts/vault-mirror.mjs +9 -0
- package/skills/_shared/fleet-protocol.md +142 -1
- package/skills/_shared/instruction-file-resolution.md +1 -1
- package/skills/_shared/monitor-patterns.md +44 -25
- package/skills/_shared/platform-tools.md +7 -0
- package/skills/_shared/state-ownership.md +3 -2
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/bootstrap/SKILL.md +3 -1
- package/skills/bootstrap/_shared-template.md +4 -4
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +1 -1
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
- package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
- package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
- package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
- package/skills/claude-md-drift-check/SKILL.md +3 -3
- package/skills/claude-md-drift-check/checker.mjs +61 -101
- package/skills/close/SKILL.md +20 -7
- package/skills/discovery/probes/docs-staleness.mjs +7 -5
- package/skills/discovery/probes/frontend-slop.mjs +7 -3
- package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
- package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
- package/skills/discovery/probes/vault-staleness.mjs +5 -3
- package/skills/discovery/probes-session.md +3 -0
- package/skills/discovery/probes-vault.md +2 -0
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eval/SKILL.md +3 -1
- package/skills/evolve/SKILL.md +2 -0
- package/skills/evolve/references/evolve-analyze-mode.md +3 -3
- package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
- package/skills/gitlab-ops/SKILL.md +7 -6
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/go/SKILL.md +13 -1
- package/skills/npm-publish/SKILL.md +5 -2
- package/skills/plan/SKILL.md +23 -10
- package/skills/plan/mode-feature.md +13 -8
- package/skills/plan/mode-new.md +6 -14
- package/skills/plan/prd-feature-template.md +19 -0
- package/skills/plan/prd-full-template.md +12 -5
- package/skills/plan/prd-premortem-panel.md +48 -0
- package/skills/plan/prd-reviewer-prompt.md +11 -3
- package/skills/plan/soul.md +1 -1
- package/skills/portfolio/SKILL.md +5 -6
- package/skills/quality-gates/SKILL.md +2 -4
- package/skills/reconcile/SKILL.md +27 -4
- package/skills/release/SKILL.md +16 -8
- package/skills/session-end/SKILL.md +94 -11
- package/skills/session-end/phase-3-2-docs-verification.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +11 -7
- package/skills/session-end/phase-3-7a-recommendations.md +9 -2
- package/skills/session-end/plan-verification.md +16 -1
- package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +4 -0
- package/skills/session-end/session-metrics-write.md +7 -4
- package/skills/session-plan/SKILL.md +5 -3
- package/skills/session-start/SKILL.md +9 -3
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-8-5-express-path.md +4 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
- package/skills/tmux-layout/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +3 -2
- package/skills/wave-executor/SKILL.md +5 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
- package/skills/wave-executor/references/wave-loop-review.md +13 -5
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
* these helpers only where the window is meant to represent REAL WORK.
|
|
35
35
|
*
|
|
36
36
|
* Exports: isRealSession, filterRealSessions, tailRealSessions,
|
|
37
|
-
* isCoordinatorDirectHousekeeping
|
|
37
|
+
* isCoordinatorDirectHousekeeping, waveAgentCount,
|
|
38
|
+
* isCoordinatorDirectWave, normalizeWaveRole
|
|
38
39
|
*/
|
|
39
40
|
|
|
40
41
|
// ---------------------------------------------------------------------------
|
|
@@ -111,3 +112,57 @@ export function isCoordinatorDirectHousekeeping(record) {
|
|
|
111
112
|
waves.every((w) => w?.role === 'Housekeeping' && w?.coordinator_direct === true)
|
|
112
113
|
);
|
|
113
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A wave's dispatched-agent count, read defensively across the key spellings
|
|
118
|
+
* the ledger holds (`/evolve analyze` effective-sizing, #1652): `agent_count`,
|
|
119
|
+
* else `agents` when it is a number, else the length of `agents` when it is an
|
|
120
|
+
* array (of descriptions), else `agents_dispatched`, else `agent_count_started`
|
|
121
|
+
* / its legacy spelling `agents_started` (the only count some waves carry).
|
|
122
|
+
*
|
|
123
|
+
* @param {unknown} wave — one entry of a record's `waves`
|
|
124
|
+
* @returns {number|null} — null when no key carries a count (unmeasured, never 0)
|
|
125
|
+
*/
|
|
126
|
+
export function waveAgentCount(wave) {
|
|
127
|
+
if (wave === null || typeof wave !== 'object' || Array.isArray(wave)) return null;
|
|
128
|
+
if (typeof wave.agent_count === 'number') return wave.agent_count;
|
|
129
|
+
if (typeof wave.agents === 'number') return wave.agents;
|
|
130
|
+
if (Array.isArray(wave.agents)) return wave.agents.length;
|
|
131
|
+
if (typeof wave.agents_dispatched === 'number') return wave.agents_dispatched;
|
|
132
|
+
if (typeof wave.agent_count_started === 'number') return wave.agent_count_started;
|
|
133
|
+
if (typeof wave.agents_started === 'number') return wave.agents_started;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* True when a wave dispatched no agents — flagged `coordinator_direct: true`,
|
|
139
|
+
* or a measured agent count of 0 without the flag (#1652). The ledger holds
|
|
140
|
+
* 222 unflagged 0-agent waves (2026-10-17, 249 real records); counting them in
|
|
141
|
+
* an agents-per-wave ratio drags the average down with waves no agent ran in.
|
|
142
|
+
* Read side on purpose: it covers the history without rewriting the ledger.
|
|
143
|
+
* An unmeasured count (`waveAgentCount` → null) is not coordinator-direct.
|
|
144
|
+
*
|
|
145
|
+
* @param {unknown} wave — one entry of a record's `waves`
|
|
146
|
+
* @returns {boolean}
|
|
147
|
+
*/
|
|
148
|
+
export function isCoordinatorDirectWave(wave) {
|
|
149
|
+
if (wave === null || typeof wave !== 'object' || Array.isArray(wave)) return false;
|
|
150
|
+
return wave.coordinator_direct === true || waveAgentCount(wave) === 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Grouping key for a wave role, case- and separator-insensitive (#1652): the
|
|
155
|
+
* ledger spells one role several ways (`Finalization`/`finalization`,
|
|
156
|
+
* `Express-Path`/`Express Path`). Lowercases and collapses runs of spaces,
|
|
157
|
+
* underscores and hyphens to one `-`. Ceiling: distinct words stay distinct —
|
|
158
|
+
* `Express` and `Express-Path` remain two keys; merging synonyms needs an
|
|
159
|
+
* alias table, revisit when a consumer has to report them as one role.
|
|
160
|
+
*
|
|
161
|
+
* @param {unknown} role
|
|
162
|
+
* @returns {string|null} — null for a non-string or blank role
|
|
163
|
+
*/
|
|
164
|
+
export function normalizeWaveRole(role) {
|
|
165
|
+
if (typeof role !== 'string') return null;
|
|
166
|
+
const key = role.trim().toLowerCase().replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
167
|
+
return key.length > 0 ? key : null;
|
|
168
|
+
}
|
|
@@ -37,6 +37,11 @@ const COST_BASES = Object.freeze(['api-list-equivalent']);
|
|
|
37
37
|
/** `pricing_table_date` (#1470 Pkt 6): a calendar date, `YYYY-MM-DD`. */
|
|
38
38
|
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
39
39
|
|
|
40
|
+
/** #1603: namespace/project#issue, allowing nested groups and ASCII path segments.
|
|
41
|
+
* The final assertion requires the actual end of input, including after newlines.
|
|
42
|
+
*/
|
|
43
|
+
const QUALIFIED_ISSUE_RE = /^(?:[A-Za-z0-9](?:[A-Za-z0-9_.-]*[A-Za-z0-9])?\/)+[A-Za-z0-9](?:[A-Za-z0-9_.-]*[A-Za-z0-9])?#[1-9][0-9]*(?![\s\S])/;
|
|
44
|
+
|
|
40
45
|
/**
|
|
41
46
|
* Valid values for the optional `status` field (Epic #724 C1).
|
|
42
47
|
* `completed` — record written by a normal /close flow.
|
|
@@ -329,12 +334,17 @@ function _validateOptionalFields(entry) {
|
|
|
329
334
|
}
|
|
330
335
|
}
|
|
331
336
|
if (entry.issues_created !== undefined) {
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
entry.issues_created.some((n) => typeof n !== 'number')
|
|
335
|
-
) {
|
|
336
|
-
throw new ValidationError('issues_created must be an array of numbers');
|
|
337
|
+
if (!Array.isArray(entry.issues_created)) {
|
|
338
|
+
throw new ValidationError('issues_created must be an array of numbers or qualified references group/repo#n');
|
|
337
339
|
}
|
|
340
|
+
entry.issues_created.forEach((issue, index) => {
|
|
341
|
+
if (typeof issue !== 'number' &&
|
|
342
|
+
!(typeof issue === 'string' && QUALIFIED_ISSUE_RE.test(issue))) {
|
|
343
|
+
throw new ValidationError(
|
|
344
|
+
`issues_created[${index}] must be a number or qualified reference group/repo#n`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
338
348
|
}
|
|
339
349
|
|
|
340
350
|
// ADR-364 optional additive fields — remote-agent substrate thin-slice.
|
|
@@ -76,8 +76,15 @@ const CI_UNKNOWN_HINT_DEFAULT = 'run `glab ci status` on demand';
|
|
|
76
76
|
/**
|
|
77
77
|
* `ci-status` "no usable reading for HEAD" reasons after which the last PUSHED
|
|
78
78
|
* commit is worth asking about: GitLab's `no-pipeline-for-head-sha`, GitHub's
|
|
79
|
-
* `no-check-runs-for-head
|
|
80
|
-
* by `checkCiStatus`).
|
|
79
|
+
* `no-check-runs-for-head` and `head-sha-not-on-remote`, and GitLab's
|
|
80
|
+
* `pipeline-unmatched-ref` (all emitted by `checkCiStatus`).
|
|
81
|
+
*
|
|
82
|
+
* `head-sha-not-on-remote` (#1727) is GitHub's answer for a local HEAD the
|
|
83
|
+
* remote does not know — the normal state of a session with unpushed commits,
|
|
84
|
+
* now that the GitHub path asks for the LOCAL HEAD sha instead of the literal
|
|
85
|
+
* ref `HEAD`. Left out of this set, an unpushed GitHub HEAD rendered only the
|
|
86
|
+
* on-demand hint while the last pushed commit could be red: silence there
|
|
87
|
+
* reads as green, the #1332 collapse on the other host.
|
|
81
88
|
*
|
|
82
89
|
* `pipeline-unmatched-ref` (#857) is not a "no data" state — pipelines EXIST for
|
|
83
90
|
* the sha, they just all sit on a foreign ref, so the reading stays `unknown`.
|
|
@@ -88,9 +95,24 @@ const CI_UNKNOWN_HINT_DEFAULT = 'run `glab ci status` on demand';
|
|
|
88
95
|
const PUSHED_FOLLOW_UP_REASONS = new Set([
|
|
89
96
|
'no-pipeline-for-head-sha',
|
|
90
97
|
'no-check-runs-for-head',
|
|
98
|
+
'head-sha-not-on-remote',
|
|
91
99
|
'pipeline-unmatched-ref',
|
|
92
100
|
]);
|
|
93
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The {@link PUSHED_FOLLOW_UP_REASONS} whose HEAD reading was taken for the
|
|
104
|
+
* LOCAL HEAD sha and found no CI data for it — so when the pushed commit IS
|
|
105
|
+
* HEAD, re-querying it would ask the identical question (see
|
|
106
|
+
* {@link ciPushedFollowUp}). `pipeline-unmatched-ref` is deliberately absent:
|
|
107
|
+
* there pipelines DO exist for the sha, only on a foreign ref, and the
|
|
108
|
+
* re-query keeps the behaviour #857 gave it.
|
|
109
|
+
*/
|
|
110
|
+
const NO_DATA_FOR_LOCAL_HEAD_REASONS = new Set([
|
|
111
|
+
'no-pipeline-for-head-sha',
|
|
112
|
+
'no-check-runs-for-head',
|
|
113
|
+
'head-sha-not-on-remote',
|
|
114
|
+
]);
|
|
115
|
+
|
|
94
116
|
/**
|
|
95
117
|
* Full SHAs for `refs`, in order, from ONE `git rev-parse` — or `null` when
|
|
96
118
|
* any ref does not resolve (no upstream, detached HEAD, not a git repo, git
|
|
@@ -143,16 +165,23 @@ async function revParseShas(repoRoot, refs) {
|
|
|
143
165
|
* `skipped: network-probe-opt-in` and this never runs). It runs INSIDE the
|
|
144
166
|
* probe's budget race and gets only what is left of that budget; if it
|
|
145
167
|
* overruns or throws, the runner keeps the HEAD reading it was given (see
|
|
146
|
-
* `runSessionStartProbes`). REVISIT TRIGGER:
|
|
147
|
-
*
|
|
148
|
-
*
|
|
168
|
+
* `runSessionStartProbes`). REVISIT TRIGGER: `follow_up: 'budget-exceeded'`
|
|
169
|
+
* on the `ci-status` record of `orchestrator.probes.completed` showing up
|
|
170
|
+
* regularly. Since #1728 that value means EITHER `ci-status` follow-up
|
|
171
|
+
* overran — this pushed-SHA requery OR {@link ciRedHistoryFollowUp}'s
|
|
172
|
+
* last-green history read — and the record carries no field that tells the
|
|
173
|
+
* two apart. Before caching the verdict per pushed SHA instead of re-asking,
|
|
174
|
+
* check the record's `outcome`: the pushed-SHA path runs only on an
|
|
175
|
+
* `unknown` reading (`ran-warn`), the history path only on a red one
|
|
176
|
+
* (`ran-alert`).
|
|
149
177
|
*
|
|
150
|
-
* The CLI round trip is skipped when the pushed commit IS HEAD on
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
178
|
+
* The CLI round trip is skipped when the pushed commit IS HEAD on a
|
|
179
|
+
* {@link NO_DATA_FOR_LOCAL_HEAD_REASONS} reading, on GitLab and GitHub alike:
|
|
180
|
+
* both paths take the HEAD reading for the LOCAL HEAD SHA, so the re-query
|
|
181
|
+
* would ask the identical question. GitHub joined that skip with #1727 —
|
|
182
|
+
* before it, `checkCiStatus` asked the GitHub API for the literal ref `HEAD`,
|
|
183
|
+
* which GitHub resolves to the REMOTE default branch, so the pushed SHA was a
|
|
184
|
+
* different question there even when it equalled local HEAD.
|
|
156
185
|
*
|
|
157
186
|
* The resolved SHA is handed to the runner through `deliver` BEFORE the CLI
|
|
158
187
|
* round trip, so a requery that overruns the budget or throws still leaves
|
|
@@ -172,7 +201,7 @@ async function ciPushedFollowUp(result, ctx, requery, deliver) {
|
|
|
172
201
|
// `pushed.sha: null` records "no upstream" as DATA — the renderer never asks git.
|
|
173
202
|
if (!shas) return { ...result, pushed: { sha: null } };
|
|
174
203
|
const [head, sha] = shas;
|
|
175
|
-
if (reason
|
|
204
|
+
if (NO_DATA_FOR_LOCAL_HEAD_REASONS.has(reason) && sha === head) {
|
|
176
205
|
return { ...result, pushed: { sha, sameAsHead: true } };
|
|
177
206
|
}
|
|
178
207
|
deliver({ ...result, pushed: { sha } });
|
|
@@ -186,6 +215,36 @@ async function ciPushedFollowUp(result, ctx, requery, deliver) {
|
|
|
186
215
|
return { ...result, pushed: { sha, verdict } };
|
|
187
216
|
}
|
|
188
217
|
|
|
218
|
+
/**
|
|
219
|
+
* `ci-status` follow-up (#1728): the GitHub red reading arrives WITHOUT its
|
|
220
|
+
* last-green history (`checkCiStatus({ deferHistory: true })`, reason
|
|
221
|
+
* `last-green-deferred`), so the red verdict is in the runner's hands before
|
|
222
|
+
* the ~0.5 s history query starts. It is delivered FIRST, re-marked
|
|
223
|
+
* `last-green-budget-exceeded` — the reading that stands if the query overruns
|
|
224
|
+
* the budget or throws, and that renders "history not read within the probe
|
|
225
|
+
* budget". Then the history query alone runs via the module's
|
|
226
|
+
* `readDeferredGithubHistory` — never a second full `checkCiStatus`, which
|
|
227
|
+
* would repeat `gh repo view` and the check-runs query.
|
|
228
|
+
*
|
|
229
|
+
* @param {*} result A red reading with `details.reason: 'last-green-deferred'`
|
|
230
|
+
* @param {{repoRoot?: string}} ctx
|
|
231
|
+
* @param {(partial: *) => void} deliver
|
|
232
|
+
* @param {*} mod The probe's imported module (`ci-status-banner.mjs`)
|
|
233
|
+
* @returns {Promise<*>}
|
|
234
|
+
*/
|
|
235
|
+
async function ciRedHistoryFollowUp(result, ctx, deliver, mod) {
|
|
236
|
+
const { deferredHistory: _pending, ...reading } = result;
|
|
237
|
+
const fallback = {
|
|
238
|
+
...reading,
|
|
239
|
+
details: { ...reading.details, reason: 'last-green-budget-exceeded' },
|
|
240
|
+
};
|
|
241
|
+
deliver(fallback);
|
|
242
|
+
// No reader on the module: the query never ran, so no budget was exceeded —
|
|
243
|
+
// keep the reading's own `last-green-deferred` ("history not read").
|
|
244
|
+
if (typeof mod?.readDeferredGithubHistory !== 'function') return reading;
|
|
245
|
+
return mod.readDeferredGithubHistory(result, { repoRoot: ctx?.repoRoot });
|
|
246
|
+
}
|
|
247
|
+
|
|
189
248
|
/**
|
|
190
249
|
* The `ci-status` follow-up (#1332, #1396): every git fact a `ci-status`
|
|
191
250
|
* banner names is resolved HERE, inside the probe's budget race, and carried
|
|
@@ -198,14 +257,20 @@ async function ciPushedFollowUp(result, ctx, requery, deliver) {
|
|
|
198
257
|
* {@link ciPushedFollowUp} (`pushed`)
|
|
199
258
|
* - a `green` reading beside a set-aside FAILED same-commit pipeline →
|
|
200
259
|
* `head: { sha }` for {@link droppedFailedLine} (#1390 P5)
|
|
260
|
+
* - a GitHub `red` reading whose history was deferred →
|
|
261
|
+
* {@link ciRedHistoryFollowUp} (`redCount` / `lastGreen`, #1728)
|
|
201
262
|
*
|
|
202
263
|
* @param {*} result
|
|
203
264
|
* @param {{repoRoot?: string}} ctx
|
|
204
265
|
* @param {(extra: object) => Promise<*>} requery
|
|
205
266
|
* @param {(partial: *) => void} deliver
|
|
267
|
+
* @param {*} [mod] The probe's imported module
|
|
206
268
|
* @returns {Promise<*>}
|
|
207
269
|
*/
|
|
208
|
-
async function ciStatusFollowUp(result, ctx, requery, deliver) {
|
|
270
|
+
async function ciStatusFollowUp(result, ctx, requery, deliver, mod) {
|
|
271
|
+
if (result?.status === 'red' && result?.details?.reason === 'last-green-deferred') {
|
|
272
|
+
return ciRedHistoryFollowUp(result, ctx, deliver, mod);
|
|
273
|
+
}
|
|
209
274
|
if (result?.status === 'unknown' && PUSHED_FOLLOW_UP_REASONS.has(result?.details?.reason)) {
|
|
210
275
|
return ciPushedFollowUp(result, ctx, requery, deliver);
|
|
211
276
|
}
|
|
@@ -276,6 +341,56 @@ function ciUnknownHint(result) {
|
|
|
276
341
|
return `last pushed: ${short} (pipeline not checked; run \`${commitRunsCommand(gh, sha)}\`)`;
|
|
277
342
|
}
|
|
278
343
|
|
|
344
|
+
/**
|
|
345
|
+
* The `(pipeline #<id>)` part of a red `ci-status` line. GitLab readings carry
|
|
346
|
+
* `details.currentPipelineId` (`?` when it is null, the pre-#856 text). GitHub
|
|
347
|
+
* readings have no pipeline id at all (#856) — there the part is omitted
|
|
348
|
+
* instead of printing a `(pipeline #?)` that names a pipeline that never
|
|
349
|
+
* existed.
|
|
350
|
+
*
|
|
351
|
+
* @param {*} r
|
|
352
|
+
* @returns {string}
|
|
353
|
+
*/
|
|
354
|
+
function redPipelineText(r) {
|
|
355
|
+
const details = r?.details;
|
|
356
|
+
if (!details || typeof details !== 'object' || !('currentPipelineId' in details)) return '';
|
|
357
|
+
return ` (pipeline #${details.currentPipelineId ?? '?'})`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The `— last green: …` part of a red `ci-status` line.
|
|
362
|
+
*
|
|
363
|
+
* - GitLab (`lastGreen.pipelineId` present): unchanged text, byte for byte.
|
|
364
|
+
* - GitHub (#856, no pipeline ids): `— last green: commit <sha7> (<n>
|
|
365
|
+
* commit(s) ago)` — interpolating the GitLab form there printed
|
|
366
|
+
* `#undefined` into the banner the operator reads.
|
|
367
|
+
* - `details.reason: 'last-green-query-failed'` with no `lastGreen`: says
|
|
368
|
+
* so. Omitting it would read exactly like "no green commit in the window",
|
|
369
|
+
* a different fact (three states, not two: found / none / not measured).
|
|
370
|
+
* - `last-green-budget-exceeded` (#1728): the red verdict arrived, the
|
|
371
|
+
* history query did not finish inside the probe budget — says so.
|
|
372
|
+
* `last-green-deferred` (the follow-up never re-marked it): not read.
|
|
373
|
+
*
|
|
374
|
+
* @param {*} r
|
|
375
|
+
* @returns {string}
|
|
376
|
+
*/
|
|
377
|
+
function lastGreenText(r) {
|
|
378
|
+
const lg = r?.lastGreen;
|
|
379
|
+
if (lg && typeof lg === 'object') {
|
|
380
|
+
const sha7 = String(lg.sha ?? '').slice(0, 7);
|
|
381
|
+
if (lg.pipelineId !== undefined) {
|
|
382
|
+
return ` — last green: #${lg.pipelineId} (commit ${sha7}, ${r.redCount} pipelines ago)`;
|
|
383
|
+
}
|
|
384
|
+
const n = lg.ageCommits;
|
|
385
|
+
return ` — last green: commit ${sha7} (${n} commit${n === 1 ? '' : 's'} ago)`;
|
|
386
|
+
}
|
|
387
|
+
const reason = r?.details?.reason;
|
|
388
|
+
if (reason === 'last-green-query-failed') return ' — last green: unknown (history query failed)';
|
|
389
|
+
if (reason === 'last-green-budget-exceeded') return ' — last green: unknown (history not read within the probe budget)';
|
|
390
|
+
if (reason === 'last-green-deferred') return ' — last green: unknown (history not read)';
|
|
391
|
+
return '';
|
|
392
|
+
}
|
|
393
|
+
|
|
279
394
|
/** Additive GitHub diagnostics; absent counts leave existing banners unchanged. */
|
|
280
395
|
function checkRunCountsText(result) {
|
|
281
396
|
const counts = result?.details?.checkRunCounts;
|
|
@@ -508,10 +623,12 @@ function startLoopBlockedMeter() {
|
|
|
508
623
|
* - `precondition` optional; returns a skip-reason string to skip the probe
|
|
509
624
|
* - `render` optional; maps a result to a banner line. Default:
|
|
510
625
|
* `result.message` when severity is warn/alert.
|
|
511
|
-
* - `followUp` optional; `async (result, ctx, requery, deliver) => result'`
|
|
626
|
+
* - `followUp` optional; `async (result, ctx, requery, deliver, mod) => result'`
|
|
512
627
|
* run INSIDE the budget race after the entry function, where
|
|
513
|
-
* `requery(extra)` re-invokes it with `{...args, ...extra}
|
|
514
|
-
*
|
|
628
|
+
* `requery(extra)` re-invokes it with `{...args, ...extra}`,
|
|
629
|
+
* `deliver(partial)` replaces the fallback result, and `mod`
|
|
630
|
+
* is the probe's imported module (for a narrower second call
|
|
631
|
+
* than a full requery, #1728). If it
|
|
515
632
|
* throws or overruns the budget, the last delivered result
|
|
516
633
|
* stands (recorded `followUp: 'threw'|'budget-exceeded'`).
|
|
517
634
|
* - `render` is a PURE function of the result (#1396): anything it names
|
|
@@ -571,10 +688,23 @@ export const PROBES = [
|
|
|
571
688
|
spec: local('./ci-status-banner.mjs'),
|
|
572
689
|
fn: 'checkCiStatus',
|
|
573
690
|
network: true,
|
|
574
|
-
|
|
691
|
+
// #1728: `deferHistory` keeps the GitHub last-green history query out of
|
|
692
|
+
// the entry function, so a red verdict is delivered before the query can
|
|
693
|
+
// overrun PROBE_BUDGET_MS (measured 2026-10-22: red with history
|
|
694
|
+
// 1605–1717 ms, the query alone ~495 ms). The follow-up reads it inside
|
|
695
|
+
// what is left of the budget. Ceiling: other local-HEAD callers of
|
|
696
|
+
// `checkCiStatus` still pay the query inline on red (see `checkGithub`).
|
|
697
|
+
// Second ceiling (BV-004): the red verdict is still lost when
|
|
698
|
+
// `checkCiStatus` ITSELF (git lookups + `gh repo view` + check-runs,
|
|
699
|
+
// measured 915–941 ms on 2026-10-22) overruns the work-time budget —
|
|
700
|
+
// `delivered` is set only after the entry function returns. Revisit
|
|
701
|
+
// trigger: an `orchestrator.probes.completed` record with `ci-status`
|
|
702
|
+
// `outcome: 'timeout'`.
|
|
703
|
+
args: ({ repoRoot }) => ({ repoRoot, deferHistory: true }),
|
|
575
704
|
// #1332: on `no-pipeline-for-head-sha` / `no-check-runs-for-head`, query
|
|
576
705
|
// the last PUSHED commit's verdict via `checkCiStatus({ sha })` — see the
|
|
577
706
|
// ceiling on the function. #1396: also resolves every SHA the renderer names.
|
|
707
|
+
// #1728: on a deferred GitHub red reading, reads the last-green history.
|
|
578
708
|
followUp: ciStatusFollowUp,
|
|
579
709
|
// Bespoke shape: `{status, ok, details, …}` with no `message` field. The
|
|
580
710
|
// banner text is prescribed by SKILL.md § Phase 4.
|
|
@@ -602,12 +732,8 @@ export const PROBES = [
|
|
|
602
732
|
return `${mark} ci-status: CI status for HEAD could not be determined (${reason})${checkRunCountsText(r)} — ${ciUnknownHint(r)}`;
|
|
603
733
|
}
|
|
604
734
|
if (r.status === 'red') {
|
|
605
|
-
const pid = r.details?.currentPipelineId ?? '?';
|
|
606
|
-
const green = r.lastGreen
|
|
607
|
-
? ` — last green: #${r.lastGreen.pipelineId} (commit ${String(r.lastGreen.sha ?? '').slice(0, 7)}, ${r.redCount} pipelines ago)`
|
|
608
|
-
: '';
|
|
609
735
|
const job = r.failingJobName ? ` Failing job: ${r.failingJobName}` : '';
|
|
610
|
-
return `🚨 CI RED on HEAD
|
|
736
|
+
return `🚨 CI RED on HEAD${redPipelineText(r)}${lastGreenText(r)}.${job}${checkRunCountsText(r)}`;
|
|
611
737
|
}
|
|
612
738
|
if (r.status === 'green') {
|
|
613
739
|
const lines = [];
|
|
@@ -1134,6 +1260,7 @@ export async function runSessionStartProbes(opts = {}, deps = {}) {
|
|
|
1134
1260
|
ctx,
|
|
1135
1261
|
(extra) => fn({ ...probe.args(ctx), ...extra }),
|
|
1136
1262
|
(partial) => { delivered = { __probeResult: partial }; },
|
|
1263
|
+
mod,
|
|
1137
1264
|
),
|
|
1138
1265
|
};
|
|
1139
1266
|
} catch {
|
|
@@ -122,6 +122,7 @@
|
|
|
122
122
|
import { readFileSync } from 'node:fs';
|
|
123
123
|
import { resolve } from 'node:path';
|
|
124
124
|
import { costUsd } from './telemetry/pricing.mjs';
|
|
125
|
+
import { resolveLedgerPath } from './ledger-path.mjs';
|
|
125
126
|
|
|
126
127
|
// ---------------------------------------------------------------------------
|
|
127
128
|
// Default subagents.jsonl path (relative to cwd, mirroring the rest of the
|
|
@@ -262,7 +263,7 @@ function recordCostUsd(record) {
|
|
|
262
263
|
*/
|
|
263
264
|
export function rollupSessionTokens({
|
|
264
265
|
parentSessionId,
|
|
265
|
-
subagentsPath = DEFAULT_SUBAGENTS_PATH,
|
|
266
|
+
subagentsPath = resolveLedgerPath(DEFAULT_SUBAGENTS_PATH, process.cwd()),
|
|
266
267
|
}) {
|
|
267
268
|
/** @type {TokenRollupResult} */
|
|
268
269
|
const ZERO = {
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-work-trees.mjs — which trees did a session commit in, and from which
|
|
3
|
+
* start ref (#1659).
|
|
4
|
+
*
|
|
5
|
+
* `session-start-ref` names ONE sha in ONE tree. A coordinator that commits in
|
|
6
|
+
* several of its own worktrees (`git -C <wt> commit`, `cd <wt> && git commit`,
|
|
7
|
+
* subagents committing there) leaves those commits unattributed, and a ref from
|
|
8
|
+
* a different tree is no fix: a two-dot diff across two trees lists foreign
|
|
9
|
+
* files. hooks/pre-bash-staging-fence.mjs therefore records, before every
|
|
10
|
+
* `git commit`, the pair `(tree, start_ref)` as an
|
|
11
|
+
* `orchestrator.session.work_tree` event in the events ledger. This module is
|
|
12
|
+
* the reader:
|
|
13
|
+
*
|
|
14
|
+
* - {@link readSessionWorkTrees} — the session's trees, FIRST record per tree
|
|
15
|
+
* wins (the writer does not dedupe; every commit appends one record).
|
|
16
|
+
* - {@link sessionCommitFiles} — per tree `git diff --name-only <ref>..HEAD`
|
|
17
|
+
* INSIDE THAT TREE ONLY. A diff between two different trees is never
|
|
18
|
+
* computed.
|
|
19
|
+
*
|
|
20
|
+
* Neither function throws. Unreadable ledger lines are COUNTED, not silently
|
|
21
|
+
* skipped (`.claude/rules/measurement-discipline.md` § Unlesbare JSONL-Zeilen
|
|
22
|
+
* zaehlen); an unusable tree is reported `skipped` with a reason.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { execFileSync } from 'node:child_process';
|
|
26
|
+
import { existsSync } from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
|
|
29
|
+
import { readEventsWithRotations } from './events.mjs';
|
|
30
|
+
import { SESSION_WORK_TREE_EVENT } from './events-schema.mjs';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Events-ledger window for the #1401 census (`events-retention-banner.mjs`):
|
|
34
|
+
* `null` — {@link readSessionWorkTrees} reads back to a session's FIRST
|
|
35
|
+
* `orchestrator.session.work_tree` record, however old, so no day count bounds
|
|
36
|
+
* it. A cut history loses that tree's first start_ref; the reader then takes
|
|
37
|
+
* the oldest surviving record for it, which under-reports that tree (#1659).
|
|
38
|
+
*/
|
|
39
|
+
export const REQUIRED_EVENTS_WINDOW_DAYS = null;
|
|
40
|
+
|
|
41
|
+
/** Per-git-call timeout. A hung git must not hang a consumer (close, gate). */
|
|
42
|
+
const GIT_TIMEOUT_MS = 10_000;
|
|
43
|
+
|
|
44
|
+
/** Full sha, SHA-1 or SHA-256 object format. */
|
|
45
|
+
const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read the `(tree, start_ref)` pairs one session recorded.
|
|
49
|
+
*
|
|
50
|
+
* Only records whose `session_id` EQUALS `sessionId` (the raw harness id — a
|
|
51
|
+
* dispatched subagent carries the coordinator's raw id, so its commits count
|
|
52
|
+
* for the coordinator, as intended). The first record per `tree` wins, in
|
|
53
|
+
* ledger order (rotated archives included, oldest first). A record of the
|
|
54
|
+
* right event and session but with a missing/invalid `tree` or `start_ref` is
|
|
55
|
+
* counted in `invalid`, never used.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} args
|
|
58
|
+
* @param {string} args.sessionId — raw session id to match.
|
|
59
|
+
* @param {string} [args.eventsFile] — explicit active ledger path (wins over repoRoot).
|
|
60
|
+
* @param {string} [args.repoRoot] — project root whose ledger is read; defaults
|
|
61
|
+
* like `eventsFilePath()` (`resolveEventsLedgerRoot()`).
|
|
62
|
+
* @returns {{ trees: Array<{ tree: string, start_ref: string }>,
|
|
63
|
+
* malformed: number, invalid: number, complete: boolean|null,
|
|
64
|
+
* error?: string }}
|
|
65
|
+
* `malformed` = unreadable ledger lines (all sessions, the reader cannot
|
|
66
|
+
* attribute a line it cannot parse); `complete` mirrors
|
|
67
|
+
* `readEventsWithRotations` (`null` = no ledger found, NOT "no trees").
|
|
68
|
+
*/
|
|
69
|
+
export function readSessionWorkTrees({ sessionId, eventsFile, repoRoot } = {}) {
|
|
70
|
+
if (typeof sessionId !== 'string' || sessionId.trim() === '') {
|
|
71
|
+
return { trees: [], malformed: 0, invalid: 0, complete: null, error: 'sessionId missing' };
|
|
72
|
+
}
|
|
73
|
+
let read;
|
|
74
|
+
try {
|
|
75
|
+
read = readEventsWithRotations(repoRoot, eventsFile ? { filePath: eventsFile } : {});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return { trees: [], malformed: 0, invalid: 0, complete: null, error: String(err?.message ?? err) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
const trees = [];
|
|
82
|
+
let invalid = 0;
|
|
83
|
+
for (const rec of read.events) {
|
|
84
|
+
if (rec?.event !== SESSION_WORK_TREE_EVENT || rec.session_id !== sessionId) continue;
|
|
85
|
+
const { tree, start_ref: startRef } = rec;
|
|
86
|
+
if (typeof tree !== 'string' || !path.isAbsolute(tree)
|
|
87
|
+
|| typeof startRef !== 'string' || !FULL_SHA_RE.test(startRef)) {
|
|
88
|
+
invalid += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (seen.has(tree)) continue; // first record per tree wins (#1659)
|
|
92
|
+
seen.add(tree);
|
|
93
|
+
trees.push({ tree, start_ref: startRef });
|
|
94
|
+
}
|
|
95
|
+
return { trees, malformed: read.malformed_lines, invalid, complete: read.complete };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run git with an argv array (no shell) and a timeout; never throws.
|
|
100
|
+
*
|
|
101
|
+
* @param {string[]} args
|
|
102
|
+
* @returns {{ ok: true, stdout: string } | { ok: false, error: string }}
|
|
103
|
+
*/
|
|
104
|
+
function git(args) {
|
|
105
|
+
try {
|
|
106
|
+
const stdout = execFileSync('git', args, {
|
|
107
|
+
encoding: 'utf8',
|
|
108
|
+
timeout: GIT_TIMEOUT_MS,
|
|
109
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
110
|
+
});
|
|
111
|
+
return { ok: true, stdout };
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const stderr = typeof err?.stderr === 'string' ? err.stderr.trim() : '';
|
|
114
|
+
return { ok: false, error: stderr || String(err?.message ?? err) };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Files each tree's commits touched since that tree's own start ref.
|
|
120
|
+
*
|
|
121
|
+
* For every `{ tree, start_ref }`: `git -C <tree> diff --name-only
|
|
122
|
+
* <start_ref>..HEAD`, run in THAT tree with THAT tree's ref — never a diff
|
|
123
|
+
* between two trees. Paths are tree-relative, as git prints them; `union` is
|
|
124
|
+
* the deduped, sorted union of those relative paths across trees (trees of the
|
|
125
|
+
* same repo share a path space).
|
|
126
|
+
*
|
|
127
|
+
* A tree whose directory is gone, or whose ref no longer resolves there (gc'd,
|
|
128
|
+
* different repo), lands in `skipped` with a reason — the caller decides what
|
|
129
|
+
* an unmeasured tree means; it is never silently dropped and never throws.
|
|
130
|
+
*
|
|
131
|
+
* @param {{ trees: Array<{ tree: string, start_ref: string }> }} args
|
|
132
|
+
* @returns {{ perTree: Array<{ tree: string, start_ref: string, files: string[] }>,
|
|
133
|
+
* skipped: Array<{ tree: string, start_ref: string, reason: string }>,
|
|
134
|
+
* union: string[] }}
|
|
135
|
+
*/
|
|
136
|
+
export function sessionCommitFiles({ trees } = {}) {
|
|
137
|
+
const perTree = [];
|
|
138
|
+
const skipped = [];
|
|
139
|
+
const union = new Set();
|
|
140
|
+
for (const entry of Array.isArray(trees) ? trees : []) {
|
|
141
|
+
const tree = entry?.tree;
|
|
142
|
+
const startRef = entry?.start_ref;
|
|
143
|
+
if (typeof tree !== 'string' || typeof startRef !== 'string' || !FULL_SHA_RE.test(startRef)) {
|
|
144
|
+
skipped.push({ tree: String(tree), start_ref: String(startRef), reason: 'invalid-entry' });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!existsSync(tree)) {
|
|
148
|
+
skipped.push({ tree, start_ref: startRef, reason: 'tree-missing' });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const verify = git(['-C', tree, 'rev-parse', '--verify', '--quiet', `${startRef}^{commit}`]);
|
|
152
|
+
if (!verify.ok) {
|
|
153
|
+
skipped.push({ tree, start_ref: startRef, reason: 'ref-unresolvable' });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const diff = git(['-C', tree, 'diff', '--name-only', `${startRef}..HEAD`]);
|
|
157
|
+
if (!diff.ok) {
|
|
158
|
+
skipped.push({ tree, start_ref: startRef, reason: `diff-failed: ${diff.error.split('\n')[0]}` });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const files = diff.stdout.split('\n').filter((l) => l !== '');
|
|
162
|
+
for (const f of files) union.add(f);
|
|
163
|
+
perTree.push({ tree, start_ref: startRef, files });
|
|
164
|
+
}
|
|
165
|
+
return { perTree, skipped, union: [...union].sort() };
|
|
166
|
+
}
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
|
|
102
102
|
import fs from 'node:fs';
|
|
103
103
|
import path from 'node:path';
|
|
104
|
+
import { resolveEventsLedgerRoot } from './platform.mjs';
|
|
104
105
|
|
|
105
106
|
const SESSIONS_REL = ['.orchestrator', 'metrics', 'sessions.jsonl'];
|
|
106
107
|
|
|
@@ -213,6 +214,51 @@ export function isBackfillStub(rec) {
|
|
|
213
214
|
return !hasWork;
|
|
214
215
|
}
|
|
215
216
|
|
|
217
|
+
/** Session types a record must carry (or show work) to count as measured. */
|
|
218
|
+
const MEASURED_SESSION_TYPES = new Set(['housekeeping', 'feature', 'deep']);
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* True when a record is a MEASURED session — one a decision may count:
|
|
222
|
+
* "sessions", deep share, close rate, staleness, dispatcher ranking, site
|
|
223
|
+
* numbers, cold-start history. A record is measured when it is a parseable
|
|
224
|
+
* object, is NOT a backfill stub (`isBackfillStub`), does NOT carry
|
|
225
|
+
* `_synthetic: true`, and EITHER carries a known `session_type`
|
|
226
|
+
* (`housekeeping|feature|deep`) OR shows at least one wave/agent.
|
|
227
|
+
*
|
|
228
|
+
* Why: 59.1 % of fleet session records are synthetic backfill stubs written by
|
|
229
|
+
* `hooks/on-session-end.mjs` via `session-close-backfill.mjs::synthesizeRecord`
|
|
230
|
+
* (`_synthetic: true`, `session_type: 'unknown'`, 0 waves/agents) — measured by
|
|
231
|
+
* D1 on 2026-10-12 (audit E1, `docs/audits/2026-10-12-portfolio-analyse/`).
|
|
232
|
+
* `isRealSession` (`session-schema/filters.mjs`) only drops `status:
|
|
233
|
+
* 'abandoned'`, so a `state-md-completed` stub with `status: 'completed'` still
|
|
234
|
+
* read as a real session; a stub-only ledger then counted as session history.
|
|
235
|
+
* This predicate is the narrower READER question; it does not replace
|
|
236
|
+
* `isRealSession` (whose fail-open contract schema/render readers rely on).
|
|
237
|
+
* Decision readers use this definition alone, including legacy known-type
|
|
238
|
+
* records without a completed status. The staleness banner separately retains
|
|
239
|
+
* unmeasured starts as activity anchors, never as measured completion times.
|
|
240
|
+
*
|
|
241
|
+
* CEILING (BV-004): a legacy pre-4.0.0 record with no `session_type` but with
|
|
242
|
+
* waves counts as measured — waves are evidence of real work, the missing type
|
|
243
|
+
* is only a schema gap. Conversely a known-type record with 0 waves (a
|
|
244
|
+
* coordinator-direct housekeeping close) counts too.
|
|
245
|
+
* Revisit-Trigger: if a writer starts emitting unknown-type records WITH waves
|
|
246
|
+
* that are not real work, or `_synthetic` records stop carrying 0 waves.
|
|
247
|
+
*
|
|
248
|
+
* @param {unknown} rec
|
|
249
|
+
* @returns {boolean}
|
|
250
|
+
*/
|
|
251
|
+
export function isMeasuredSession(rec) {
|
|
252
|
+
if (!isRecordObject(rec)) return false;
|
|
253
|
+
if (isBackfillStub(rec) || rec._synthetic === true) return false;
|
|
254
|
+
if (MEASURED_SESSION_TYPES.has(rec.session_type)) return true;
|
|
255
|
+
return (Array.isArray(rec.waves) && rec.waves.length > 0)
|
|
256
|
+
|| rec.total_waves > 0 || rec.total_agents > 0
|
|
257
|
+
// Legacy records may carry measured agent counts only in agent_summary.
|
|
258
|
+
|| (isRecordObject(rec.agent_summary)
|
|
259
|
+
&& ['complete', 'partial', 'failed', 'spiral'].some((key) => rec.agent_summary[key] > 0));
|
|
260
|
+
}
|
|
261
|
+
|
|
216
262
|
/**
|
|
217
263
|
* Rule (1), cross-id half (#1443) — drop every backfill stub that shares a
|
|
218
264
|
* non-empty `raw_session_id` with at least one non-stub. A group of stubs only
|
|
@@ -463,10 +509,16 @@ export function canonicalizeSessions(records, opts) {
|
|
|
463
509
|
* so id-less records are counted as-is and only the id-bearing ones go through
|
|
464
510
|
* the identity collapse.
|
|
465
511
|
*
|
|
512
|
+
* `measuredOnly` drops every record `isMeasuredSession` rejects (backfill
|
|
513
|
+
* stubs, `_synthetic`, unknown type without work) — for a reader whose count
|
|
514
|
+
* is a "has this repo real session history?" decision (cold-start, audit E1).
|
|
515
|
+
*
|
|
466
516
|
* @param {string} raw — full file contents.
|
|
517
|
+
* @param {object} [opts]
|
|
518
|
+
* @param {boolean} [opts.measuredOnly=false]
|
|
467
519
|
* @returns {number}
|
|
468
520
|
*/
|
|
469
|
-
export function countSessionsInJsonl(raw) {
|
|
521
|
+
export function countSessionsInJsonl(raw, { measuredOnly = false } = {}) {
|
|
470
522
|
if (typeof raw !== 'string' || raw.length === 0) return 0;
|
|
471
523
|
const parsed = [];
|
|
472
524
|
for (const line of raw.split('\n')) {
|
|
@@ -479,6 +531,12 @@ export function countSessionsInJsonl(raw) {
|
|
|
479
531
|
}
|
|
480
532
|
}
|
|
481
533
|
const identified = parsed.filter((r) => isRecordObject(r) && isNonEmptyString(r.session_id));
|
|
534
|
+
if (measuredOnly) {
|
|
535
|
+
const anonymousMeasured = parsed.filter(
|
|
536
|
+
(r) => isMeasuredSession(r) && !isNonEmptyString(r.session_id),
|
|
537
|
+
).length;
|
|
538
|
+
return canonicalizeSessions(identified).filter(isMeasuredSession).length + anonymousMeasured;
|
|
539
|
+
}
|
|
482
540
|
const anonymous = parsed.length - identified.length;
|
|
483
541
|
return canonicalizeSessions(identified).length + anonymous;
|
|
484
542
|
}
|
|
@@ -515,7 +573,7 @@ export function countSessionsInJsonl(raw) {
|
|
|
515
573
|
*
|
|
516
574
|
* @param {object} [args]
|
|
517
575
|
* @param {string} [args.repoRoot] project root; the ledger is resolved as
|
|
518
|
-
*
|
|
576
|
+
* the main checkout's `.orchestrator/metrics/sessions.jsonl`. Defaults to
|
|
519
577
|
* `process.cwd()` when neither this nor `filePath` is given.
|
|
520
578
|
* @param {string} [args.filePath] explicit ledger path (wins over `repoRoot`).
|
|
521
579
|
* @returns {Array<object>} canonical records
|
|
@@ -523,7 +581,7 @@ export function countSessionsInJsonl(raw) {
|
|
|
523
581
|
export function readCanonicalSessions({ repoRoot, filePath } = {}) {
|
|
524
582
|
const resolved = isNonEmptyString(filePath)
|
|
525
583
|
? filePath
|
|
526
|
-
: path.join(isNonEmptyString(repoRoot) ? repoRoot : process.cwd(), ...SESSIONS_REL);
|
|
584
|
+
: path.join(resolveEventsLedgerRoot(isNonEmptyString(repoRoot) ? repoRoot : process.cwd()), ...SESSIONS_REL);
|
|
527
585
|
|
|
528
586
|
let raw;
|
|
529
587
|
try {
|