session-orchestrator 3.16.0 → 3.19.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.
Files changed (220) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/030-wave-execution.mdc +17 -1
  5. package/CHANGELOG.md +130 -412
  6. package/README.md +17 -12
  7. package/SECURITY.md +190 -27
  8. package/agents/AGENTS.md +20 -3
  9. package/agents/code-implementer.md +6 -6
  10. package/agents/db-specialist.md +1 -1
  11. package/agents/qa-strategist.md +31 -6
  12. package/agents/schemas/qa-strategist.schema.json +27 -0
  13. package/agents/schemas/test-writer.schema.json +60 -2
  14. package/agents/security-reviewer.md +1 -1
  15. package/agents/session-reviewer.md +1 -1
  16. package/agents/test-writer.md +29 -10
  17. package/agents/ui-developer.md +1 -1
  18. package/commands/contract-version-bump.md +28 -0
  19. package/commands/portfolio.md +1 -1
  20. package/docs/README.md +2 -1
  21. package/docs/USER-GUIDE.md +8 -3
  22. package/docs/ci-setup.md +121 -7
  23. package/docs/codex-setup.md +1 -1
  24. package/docs/components.md +7 -7
  25. package/docs/cursor-setup.md +22 -9
  26. package/docs/events-schema.md +5 -1
  27. package/docs/instruction-delivery.md +444 -0
  28. package/docs/pi-setup.md +1 -1
  29. package/docs/rule-authoring.md +58 -9
  30. package/docs/session-config-reference.md +306 -6
  31. package/docs/session-config-template.md +66 -3
  32. package/docs/telemetry/telemetry-claims.md +204 -0
  33. package/docs/telemetry.md +158 -0
  34. package/hooks/_lib/guard-source-loader.mjs +467 -0
  35. package/hooks/_lib/lock-bootstrap.mjs +21 -0
  36. package/hooks/_lib/vcs-create-matcher.mjs +119 -0
  37. package/hooks/config-protection.mjs +0 -0
  38. package/hooks/enforce-commands.mjs +10 -2
  39. package/hooks/hooks-codex.json +1 -1
  40. package/hooks/hooks-cursor.json +11 -2
  41. package/hooks/hooks-pi.json +10 -0
  42. package/hooks/hooks.json +21 -1
  43. package/hooks/on-session-end.mjs +178 -18
  44. package/hooks/on-session-start.mjs +23 -0
  45. package/hooks/post-bash-write-verify.mjs +977 -0
  46. package/hooks/post-subagent-discovery-validator.mjs +256 -41
  47. package/hooks/pre-bash-destructive-guard.mjs +525 -160
  48. package/hooks/pre-bash-issue-budget.mjs +167 -0
  49. package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
  50. package/hooks/pre-bash-templates-first.mjs +96 -63
  51. package/hooks/skill-invocation-telemetry.mjs +109 -10
  52. package/hooks/subagent-telemetry.mjs +527 -37
  53. package/package.json +16 -3
  54. package/pi/prompts/contract-version-bump.md +12 -0
  55. package/rules/README.md +32 -0
  56. package/scripts/archive-closed-prds.mjs +12 -22
  57. package/scripts/autopilot-multi.mjs +103 -20
  58. package/scripts/backfill-abandoned-sessions.mjs +160 -4
  59. package/scripts/check-doc-consistency.sh +17 -1
  60. package/scripts/compute-grounding-injection.sh +18 -3
  61. package/scripts/dialectic-deriver.mjs +7 -2
  62. package/scripts/eval-session.mjs +50 -9
  63. package/scripts/fleet-instruction-scan.mjs +141 -0
  64. package/scripts/lib/auto-dialectic.mjs +11 -2
  65. package/scripts/lib/auto-dream.mjs +16 -5
  66. package/scripts/lib/autopilot/mr-draft.mjs +31 -1
  67. package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
  68. package/scripts/lib/backlog-scan.mjs +39 -6
  69. package/scripts/lib/blocked-commands-policy.mjs +340 -0
  70. package/scripts/lib/build-live-signals.mjs +7 -4
  71. package/scripts/lib/ci-status-banner.mjs +75 -12
  72. package/scripts/lib/claude-md-budget-lint.mjs +283 -34
  73. package/scripts/lib/command-blocker.mjs +1013 -58
  74. package/scripts/lib/config/config-protection.mjs +2 -1
  75. package/scripts/lib/config/context-coverage.mjs +82 -0
  76. package/scripts/lib/config/drift-check.mjs +9 -1
  77. package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
  78. package/scripts/lib/config/issue-budget.mjs +123 -0
  79. package/scripts/lib/config/moc-staleness.mjs +98 -0
  80. package/scripts/lib/config/reconcile.mjs +21 -0
  81. package/scripts/lib/config/section-extractor.mjs +121 -1
  82. package/scripts/lib/config/worktree-orphans.mjs +138 -0
  83. package/scripts/lib/config-schema.mjs +23 -3
  84. package/scripts/lib/config.mjs +32 -0
  85. package/scripts/lib/context-coverage-banner.mjs +223 -0
  86. package/scripts/lib/convergence-monitor.mjs +49 -3
  87. package/scripts/lib/description-surface.mjs +535 -0
  88. package/scripts/lib/dispatcher/enumerate.mjs +136 -30
  89. package/scripts/lib/dispatcher/rank.mjs +22 -8
  90. package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
  91. package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
  92. package/scripts/lib/eval/engine.mjs +47 -5
  93. package/scripts/lib/events.mjs +59 -7
  94. package/scripts/lib/evolve/autonomy-verdict.mjs +5 -0
  95. package/scripts/lib/evolve/autopilot-effectiveness.mjs +54 -7
  96. package/scripts/lib/gates/gate-full.mjs +15 -3
  97. package/scripts/lib/gates/gate-helpers.mjs +132 -6
  98. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
  99. package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
  100. package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
  101. package/scripts/lib/handover-gate.mjs +7 -3
  102. package/scripts/lib/harness-audit/categories/category4.mjs +22 -5
  103. package/scripts/lib/instruction-budget-guard.mjs +402 -51
  104. package/scripts/lib/io.mjs +345 -10
  105. package/scripts/lib/issue-budget.mjs +269 -0
  106. package/scripts/lib/issue-close-strip-labels.mjs +39 -9
  107. package/scripts/lib/label-scope.mjs +47 -0
  108. package/scripts/lib/learnings/schema.mjs +43 -3
  109. package/scripts/lib/lock-reaper.mjs +1 -2
  110. package/scripts/lib/memory-proposals/schema.mjs +36 -1
  111. package/scripts/lib/moc-staleness-banner.mjs +267 -0
  112. package/scripts/lib/peer-discovery.mjs +645 -0
  113. package/scripts/lib/pi-hook-bridge.mjs +146 -17
  114. package/scripts/lib/product-repo-detect.mjs +9 -8
  115. package/scripts/lib/project-hygiene.mjs +432 -0
  116. package/scripts/lib/quality-gate.mjs +167 -0
  117. package/scripts/lib/recommendations-v0.mjs +1 -1
  118. package/scripts/lib/reconcile/eligibility.mjs +1 -1
  119. package/scripts/lib/reconcile/emitter.mjs +23 -4
  120. package/scripts/lib/reconcile/engine.mjs +147 -39
  121. package/scripts/lib/reconcile/idempotency.mjs +114 -14
  122. package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
  123. package/scripts/lib/resource-probe/evaluate.mjs +70 -4
  124. package/scripts/lib/resource-probe.mjs +19 -0
  125. package/scripts/lib/rule-loader.mjs +6 -0
  126. package/scripts/lib/scope-baseline.mjs +564 -0
  127. package/scripts/lib/scope-gate.mjs +399 -98
  128. package/scripts/lib/session-close-backfill.mjs +61 -6
  129. package/scripts/lib/session-end/phase-skip.mjs +1 -0
  130. package/scripts/lib/session-end/worktree-orphan-sweep.mjs +252 -0
  131. package/scripts/lib/session-id.mjs +221 -41
  132. package/scripts/lib/session-lock.mjs +304 -6
  133. package/scripts/lib/session-schema/constants.mjs +22 -3
  134. package/scripts/lib/session-schema/filters.mjs +88 -0
  135. package/scripts/lib/session-schema/validator.mjs +16 -0
  136. package/scripts/lib/session-schema.mjs +1 -0
  137. package/scripts/lib/sessions-integrity-banner.mjs +294 -0
  138. package/scripts/lib/sessions-staleness-banner.mjs +121 -12
  139. package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
  140. package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
  141. package/scripts/lib/skill-health/join.mjs +35 -9
  142. package/scripts/lib/spiral-carryover.mjs +142 -30
  143. package/scripts/lib/state-md/mission-status.mjs +53 -3
  144. package/scripts/lib/subagents-schema.mjs +43 -9
  145. package/scripts/lib/telemetry/anon-id.mjs +141 -0
  146. package/scripts/lib/telemetry/consent.mjs +299 -0
  147. package/scripts/lib/telemetry/paths.mjs +27 -0
  148. package/scripts/lib/telemetry/queue.mjs +287 -0
  149. package/scripts/lib/telemetry/schema.mjs +384 -0
  150. package/scripts/lib/telemetry/sync.mjs +312 -0
  151. package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
  152. package/scripts/lib/tests-src-ratio.mjs +484 -0
  153. package/scripts/lib/validate/check-agents.mjs +56 -0
  154. package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
  155. package/scripts/lib/validate/check-rules.mjs +217 -35
  156. package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
  157. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  158. package/scripts/lib/validate-vendored-rules.mjs +10 -2
  159. package/scripts/lib/vault-archive.mjs +17 -2
  160. package/scripts/lib/vault-backfill/glab.mjs +8 -0
  161. package/scripts/lib/vault-mirror/process.mjs +30 -0
  162. package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
  163. package/scripts/lib/vault-status/board-writer.mjs +63 -5
  164. package/scripts/lib/vault-status/narrative-mirror.mjs +13 -7
  165. package/scripts/lib/vcs-repo-spec.mjs +362 -0
  166. package/scripts/lib/wave-resource-gate.mjs +115 -11
  167. package/scripts/lib/worktree/listing.mjs +44 -7
  168. package/scripts/mcp-server.sh +32 -6
  169. package/scripts/measure-context-overhead.sh +151 -0
  170. package/scripts/memory-propose.mjs +72 -9
  171. package/scripts/print-applicable-rules.mjs +51 -12
  172. package/scripts/release.mjs +534 -0
  173. package/scripts/run-quality-gate.mjs +123 -5
  174. package/scripts/telemetry.mjs +250 -0
  175. package/scripts/validate-wave-scope.mjs +182 -17
  176. package/scripts/vault-integration-watcher.mjs +32 -10
  177. package/skills/_shared/config-reading.md +2 -2
  178. package/skills/bootstrap/fast-template.md +1 -1
  179. package/skills/claude-md-drift-check/checker.mjs +145 -28
  180. package/skills/contract-version-bump/SKILL.md +219 -0
  181. package/skills/discovery/SKILL.md +4 -4
  182. package/skills/discovery/issue-templates.md +11 -11
  183. package/skills/discovery/probes-audit.md +1 -1
  184. package/skills/discovery/probes-feature.md +1 -1
  185. package/skills/discovery/probes-session.md +26 -5
  186. package/skills/ecosystem-health/SKILL.md +1 -1
  187. package/skills/ecosystem-health/wizard.md +4 -4
  188. package/skills/evolve/SKILL.md +1 -0
  189. package/skills/gitlab-ops/SKILL.md +20 -12
  190. package/skills/gitlab-portfolio/SKILL.md +2 -2
  191. package/skills/hook-development/SKILL.md +1 -1
  192. package/skills/mode-selector/SKILL.md +1 -1
  193. package/skills/npm-publish/SKILL.md +97 -0
  194. package/skills/plan/SKILL.md +5 -5
  195. package/skills/plan/mode-feature.md +4 -4
  196. package/skills/plan/mode-new.md +10 -10
  197. package/skills/plan/mode-retro.md +1 -1
  198. package/skills/quality-gates/SKILL.md +1 -1
  199. package/skills/reconcile/SKILL.md +21 -4
  200. package/skills/session-end/SKILL.md +108 -14
  201. package/skills/session-end/discovery-scan.md +4 -2
  202. package/skills/session-end/drift-operations.md +4 -4
  203. package/skills/session-end/metrics-collection.md +13 -0
  204. package/skills/session-end/phase-3-2-docs-verification.md +1 -1
  205. package/skills/session-end/phase-3-6-tail.md +2 -1
  206. package/skills/session-end/plan-verification.md +5 -2
  207. package/skills/session-end/vault-operations.md +1 -1
  208. package/skills/session-end/verification-checklist.md +1 -1
  209. package/skills/session-plan/SKILL.md +6 -2
  210. package/skills/session-plan/wave-template.md +2 -0
  211. package/skills/session-start/SKILL.md +149 -7
  212. package/skills/session-start/phase-4-5-resource-health.md +15 -2
  213. package/skills/test-runner/SKILL.md +2 -2
  214. package/skills/vault-sync/SKILL.md +1 -1
  215. package/skills/vault-sync/package-lock.json +3 -3
  216. package/skills/vault-sync/validator.mjs +228 -40
  217. package/skills/wave-executor/SKILL.md +5 -2
  218. package/skills/wave-executor/circuit-breaker.md +2 -0
  219. package/skills/wave-executor/wave-loop.md +163 -10
  220. package/templates/_shared/loop.md +4 -4
@@ -339,6 +339,7 @@ Reset rules — applies ONLY on the `completed` branch. Do NOT perform this rese
339
339
  ```
340
340
 
341
341
  Omit individual bullets for null-valued fields. If all 5 are null (i.e., `parseRecommendations` returned non-null but every field is null after type-coercion), skip the archival block entirely.
342
+ 7. **Scope-baseline key deletion (Epic #894 S5, #898):** If ANY of the 5 `scope-baseline-*` frontmatter keys (`scope-baseline-intent`, `scope-baseline-owner-boundary`, `scope-baseline-planned-files`, `scope-baseline-session`, `scope-baseline-frozen-at`) is present, remove them via the same `updateFrontmatterFields(contents, {field: null, ...})` mechanism as rule 6 (null value deletes the key). Rule 5 leaves unknown frontmatter fields intact and no other rule removes these five — without this step they survive into session N+1 and silently corrupt the next session's drift-baseline denominator. This is a hygiene layer only: the primary defense is mechanical — `scripts/lib/scope-baseline.mjs` compares `scope-baseline-session` against the canonical `session` field, so a stale baseline self-invalidates (`readBaseline()` returns `{stale: true, …}`) even if this rule were skipped. Delete exactly these five keys; do not remove any other unknown key.
342
343
 
343
344
  Rationale: `/close` intentionally keeps STATE.md as a record so the next session-start can read it. This reset completes that contract by demoting the record before new session state is written, so a fresh session never appears "already completed". The Recommendation archival (rule 6) preserves the session-to-session handoff in a human-readable form after the Recommendations Banner has rendered — Phase B's Mode-Selector will read the LIVE frontmatter of the current session and does not need the archived copy, so this is purely informational for humans browsing STATE.md history.
344
345
 
@@ -489,22 +490,36 @@ This is **best-effort**, exactly like the Phase 4 banners: a board-write failure
489
490
  Run these checks as ONE parallel Bash block — background the independent git ops with `&` and `wait`:
490
491
 
491
492
  ```bash
493
+ # Refresh remote-tracking refs BEFORE reading them. Without this, `origin/main`
494
+ # is a snapshot from the last fetch or clone, and every ahead/behind derivation
495
+ # below silently compares against stale data — a repo can read "in sync" while
496
+ # the real remote is many commits ahead. Best-effort and non-blocking: connect
497
+ # timeouts are bounded (no `timeout(1)` — it is absent on macOS by default) and
498
+ # any failure (offline, no remote, auth prompt) falls through to `|| true`,
499
+ # leaving the previous behaviour of reading whatever refs are on disk.
500
+ GIT_SSH_COMMAND='ssh -o ConnectTimeout=5 -o BatchMode=yes' \
501
+ git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=5 \
502
+ fetch --quiet --prune 2>/dev/null || true
503
+
492
504
  # Independent ops — launch in parallel, collect output via tmpfiles
493
505
  git branch -a > /tmp/so-branches.$$ &
494
506
  git log --oneline -N > /tmp/so-commits.$$ & # N from Session Config `recent-commits` (default 20)
495
507
  git status --short > /tmp/so-status.$$ &
496
- git log origin/main..HEAD --oneline > /tmp/so-ahead.$$ &
508
+ # `--left-right --count A...B` emits "<behind>\t<ahead>": commits reachable only
509
+ # from origin/main, then only from HEAD. The older `git log origin/main..HEAD`
510
+ # form could express ahead ONLY, so "behind" was structurally unreportable.
511
+ git rev-list --left-right --count origin/main...HEAD > /tmp/so-divergence.$$ 2>/dev/null &
497
512
  wait
498
513
  # Then read the 4 tmpfiles in a single step and derive: branch state, recent commits,
499
514
  # unpushed/uncommitted, open branches. Clean up tmpfiles once derivations are done:
500
- rm -f /tmp/so-branches.$$ /tmp/so-commits.$$ /tmp/so-status.$$ /tmp/so-ahead.$$
515
+ rm -f /tmp/so-branches.$$ /tmp/so-commits.$$ /tmp/so-status.$$ /tmp/so-divergence.$$
501
516
  ```
502
517
 
503
518
  Checks to run (derived from the collected output):
504
519
 
505
- 1. **Branch state**: current branch (from `branch -a`), ahead/behind origin (from `ahead` tmpfile)
520
+ 1. **Branch state**: current branch (from `branch -a`), ahead/behind origin (from the `divergence` tmpfile — field 1 is behind, field 2 is ahead). Report BOTH directions. A non-zero behind count means the local branch is missing remote work: surface it, because agents reading repo instructions from a stale checkout will follow superseded guidance. An empty `divergence` tmpfile means no `origin/main` ref resolved (no remote, or a differently-named default branch) — report that as unknown, never as zero.
506
521
  2. **Recent commits**: parse `commits` tmpfile — identify last session's work by commit patterns
507
- 3. **Unpushed/uncommitted**: `status` tmpfile + `ahead` tmpfile combined
522
+ 3. **Unpushed/uncommitted**: `status` tmpfile + the ahead field of the `divergence` tmpfile combined
508
523
  4. **Open branches**: parse `branch -a` tmpfile, identify which are mergeable to develop/main
509
524
  5. **Stale branches**: run AFTER the parallel block — requires iterating over branches (depends on `branch -a` output). Use `git log -1 --format=%ct <branch>` per branch; flag those with no commits in more than `stale-branch-days` (default: 7) days.
510
525
 
@@ -635,7 +650,7 @@ Using the detected VCS CLI, query (reading `issue-limit` from Session Config, de
635
650
  5. **Pipeline/CI status** — is CI green?
636
651
 
637
652
  Group issues by:
638
- - `priority:critical` / `priority:high` — must-address
653
+ - `priority::critical` / `priority::high` — must-address
639
654
  - `status:ready` — ready to work on
640
655
  - Session-type relevance (housekeeping tasks vs feature tasks vs deep-work tasks)
641
656
 
@@ -667,7 +682,8 @@ Group issues by:
667
682
 
668
683
  Additionally, if the current repo has a configured `origin` remote and `glab` (GitLab) or `gh` (GitHub) is available, invoke the CI-status probe (`scripts/lib/ci-status-banner.mjs`) via `checkCiStatus({ repoRoot: process.cwd() })`. The helper returns `null` (silent no-op) when no VCS remote, no CLI tool, parse failure, or CLI timeout (8s default). When `result.status === 'red'`, render a banner alongside the bootstrap-lock and vault-staleness warnings:
669
684
  - **Red** (`status === 'red'`): `"🚨 CI RED on HEAD (pipeline #<currentPipelineId>) — last green: #<lastGreen.pipelineId> (commit <SHA-7>, <redCount> pipelines ago). Failing job: <failingJobName>"`
670
- - **Green** or **unknown**: silent (no banner) — informational only.
685
+ - **Green with soft failures** (`status === 'green'` AND `result.allowFailureJobs` is present): `"⚠ CI green on HEAD, but <N> allow_failure job(s) FAILED: <names>. A pipeline reports success regardless of these a job red on every run stays invisible at the pipeline level."` Render this even though the pipeline passed: the whole point is that pipeline status cannot express it.
686
+ - **Green** (no `allowFailureJobs`) or **unknown**: silent (no banner) — informational only.
671
687
 
672
688
  The banner is non-blocking — display in the Session Overview, do not halt the session. If `ci-status-banner.mjs` is absent (pre-#369 plugin install), skip silently.
673
689
 
@@ -707,6 +723,23 @@ Group issues by:
707
723
 
708
724
  Non-blocking. Cross-reference: `scripts/lib/session-lock.mjs` (`readLock`, `DEFAULT_TTL_HOURS` — the current session's lock `started_at` is the self-exclusion cutoff), `scripts/backfill-abandoned-sessions.mjs` (the backfill CLI the message recommends) and issue #724.
709
725
 
726
+ **The backfill is mechanical since #926 — the banner's CLI hint is a fallback, not the primary path.** `hooks/on-session-start.mjs` calls `backfillOnSessionStart()` from `scripts/backfill-abandoned-sessions.mjs` on every SessionStart, which **applies** (writes) the reconstructed stubs rather than only previewing them. This decouples recovery from `/close`: `hooks/on-session-end.mjs` also backfills, but SessionEnd fires only on a REGULAR close, so a session killed by Ctrl-C, a timeout, or a crash left no ledger record until the NEXT clean close — which may never come (observed: this repo's ledger 18.9h behind events.jsonl across 8 commits). Running at start means the *next* session recovers the previous one, whatever killed it.
727
+
728
+ Four properties make that safe to run unattended on every start:
729
+ - **Idempotent.** Dedupe against sessions.jsonl plus an atomic `wx` marker file; repeated starts write nothing new. Synthetic ids are derived from the session's own `started_at` + a sha256 of its UUID, so they are stable across runs.
730
+ - **Self-excluding.** It runs BEFORE this session emits `orchestrator.session.started`, so the starting session is not a candidate at all. On a clear/compact/resume re-fire (where an earlier started-event *is* present) the core's `skipped-own-live-lock` guard catches it against the lock bootstrapped moments earlier.
731
+ - **Foreign-safe.** Lock ownership is evaluated against the CANDIDATE, not the running process: a candidate holding a live lock returns `skipped-own-live-lock` before the `relaxDeadByAge` (#731) relaxation is consulted. A running foreign session is therefore never recorded as `abandoned`. Residual, accepted: a live session that does NOT hold the lock (it lost the acquire race) AND has emitted no event for longer than `DEFAULT_TTL_HOURS` (4h) can still be relaxed past — a candidate the system's own liveness model already treats as dead.
732
+ - **Bounded + non-blocking.** Capped at `SESSION_START_LIMIT` (25) core calls, walked newest-first so the budget reaches the recent abandoned sessions rather than being spent on ancient already-recorded ones; **measured median 845ms** (5 steady-state runs: 713/835/845/921/984) on a 1.7MB events.jsonl / 187-candidate store, coordinator-verified 2026-07-30. Treat that as the cost this adds to every session start — it is roughly a second, not a rounding error, and it scales with the events ledger rather than the candidate count. Every failure is swallowed — a backfill error can never block a session start. Operator escape hatch: `SO_DISABLE_STARTUP_BACKFILL=1`.
733
+
734
+ When the run reports `truncated: true` (more candidates than the per-start budget), the remainder is picked up by subsequent starts; `node scripts/backfill-abandoned-sessions.mjs --dry-run` remains the way to inspect the full backlog, and `--apply` the way to drain it in one pass.
735
+
736
+ Additionally, invoke the sessions-integrity probe (`scripts/lib/sessions-integrity-banner.mjs`) via `checkSessionsIntegrity({ repoRoot })` (synchronous — no await). Where sessions-staleness above detects records that were never written, this detects records that WERE written but are schema-invalid — appended by a path that bypassed `scripts/emit-session.mjs` (which validates and would have refused). The loss is otherwise silent: `scripts/vault-mirror.mjs` reports such a record as `{"action":"skipped-invalid"}` on stdout and still exits 0, so the affected sessions simply have no vault note and nobody is told. Deliberately un-gated by Session Config (like `project-hygiene`) — a check nobody enables finds nothing. It returns `null` (silent no-op) when `.orchestrator/metrics/sessions.jsonl` is absent, empty, unreadable, or holds no parseable JSON line, and when every parseable record satisfies both validators; unparseable lines are skipped rather than reported (this probe judges schema integrity, not file corruption). The probe reports TWO populations, because measurement showed neither validator's failure set contains the other (this repo, 2026-07-31, 203 records: 3 vs 12, overlapping in only 2) — `validateSession()` treats `effectiveness` as optional while vault-mirror requires it, so reporting one alone would hide the other. The vault-mirror population is measured by invoking the real render path in a try/catch, never by re-deriving its field list. When a non-null result is returned (`{ severity, message, total, schemaInvalid, mirrorSkipped }`), render `result.message` alongside the other banners:
737
+ - **warn** (records fail `validateSession()` but all still mirror — corruption without loss): `"⚠ sessions-integrity: <N> of <M> records fail validateSession (<ids>) — records were appended without passing scripts/emit-session.mjs …"`
738
+ - **alert** (at least one record is dropped by vault-mirror — those sessions have no vault note right now): same message with a `🚨` prefix and an appended `"; <N> are dropped by vault-mirror as skipped-invalid — those sessions have NO vault note (<ids>)"` clause.
739
+ - **Fully valid ledger**: silent (no banner).
740
+
741
+ Non-blocking. Note the remedy is a re-emit of the affected records through `scripts/emit-session.mjs`, not an edit of the ledger by hand. Cross-reference: `scripts/lib/session-schema/validator.mjs` (`validateSession` — the canonical write-path schema), `scripts/lib/vault-mirror/render-sessions.mjs` (the render path whose throw becomes `skipped-invalid`), `skills/session-end/session-metrics-write.md` (the prose prohibition this banner backstops), `hooks/pre-bash-sessions-ledger-guard.mjs` (the write-guard half) and GitLab issue #958.
742
+
710
743
  Additionally, invoke the owner-config probe (`scripts/lib/owner-config-banner.mjs`) via `checkOwnerConfig()` (synchronous — no await, no `repoRoot` argument: the probe reads the host-wide `owner.yaml`, not a per-repo file). The helper returns `null` (silent no-op) on a clean load, when `owner.yaml` is simply absent, or on any internal read/parse error. When a non-null result is returned (`{ severity: 'warn', message, droppedSections?, sectionWarnings?, discarded? }`), render `result.message` alongside the other banners:
711
744
  - **Optional section(s) dropped to defaults** (`droppedSections` present): an OPTIONAL object section (`paths`, `dispatcher`) was malformed and replaced by its default value.
712
745
  - **Whole file discarded** (`discarded: true`): a REQUIRED section (`owner`, `tone`, `efficiency`, `hardware-sharing`) was invalid, so the entire file was discarded and defaults are in effect.
@@ -714,7 +747,43 @@ Group issues by:
714
747
 
715
748
  Non-blocking. Cross-reference: `.claude/rules/owner-persona.md` (host-wide `owner.yaml` schema + privacy contract) and issue #820.
716
749
 
717
- All banners are non-blocking display in the Session Overview, do not halt the session. If `bootstrap-lock-freshness.mjs` is absent (pre-#186 plugin install) or `peer-cards/staleness-banner.mjs` is absent (pre-#503 plugin install) or `loop-readiness-banner.mjs` is absent (pre-#633 plugin install) or `instruction-budget-guard.mjs` is absent (pre-#687 plugin install) or `reconcile-nudge-banner.mjs` is absent (pre-#723 plugin install) or `sessions-staleness-banner.mjs` is absent (pre-#724 plugin install) or `owner-config-banner.mjs` is absent (pre-#820 plugin install), skip silently.
750
+ Additionally, invoke the MOC-staleness probe (`scripts/lib/moc-staleness-banner.mjs`) via `checkMocStaleness({ repoRoot, config: $CONFIG })` (synchronous — no await). The helper returns `null` (silent no-op) when `repoRoot` is missing/non-string, when `moc-staleness.enabled` is `false` or `moc-staleness.mode` is `off` (checked BEFORE any filesystem I/O), when no vault dir resolves (neither an explicit `vaultDir` test seam nor `config['vault-integration']['vault-dir']`), when `<vaultDir>/08-topics/` is absent, when no `*-moc.md` exists there, or when every present MOC's `updated:` frontmatter is missing/unparseable. When a non-null result is returned (`{ severity: 'warn', message, stale }`), render `result.message` alongside the other banners:
751
+ - **Stale MOC(s)** (`updated:` older than the threshold, default 90 days): `"⚠ moc-staleness: <N> MOCs stale (>90 days) — <file> (<N>d), … — review and refresh the \`updated:\` frontmatter."`
752
+ - **Healthy / disabled / no MOCs / all excluded**: silent (no banner). A MOC whose `updated:` is missing or unparseable is deliberately EXCLUDED rather than reported — the corrective action there is "fix the frontmatter", not the banner's hint (same rule as `peer-cards/staleness-banner.mjs`).
753
+
754
+ Non-blocking. Cross-reference: `scripts/lib/config/moc-staleness.mjs` (`_parseMocStaleness`) and issue #831.
755
+
756
+ Additionally, invoke the context-coverage probe (`scripts/lib/context-coverage-banner.mjs`) via `checkContextCoverage({ repoRoot, config: $CONFIG })` (synchronous — no await). The helper returns `null` (silent no-op) when `repoRoot` is missing/non-string, when `context-coverage.enabled` is `false` or `context-coverage.mode` is `off` (checked BEFORE any filesystem I/O), when no vault dir resolves, when `<vaultDir>/01-projects/` is absent or empty, when zero registered projects exist, or when every registered project already carries a `context.md` or `_passive.md`. When a non-null result is returned (`{ severity: 'warn', message, gaps, registered, covered }`), render `result.message` alongside the other banners:
757
+ - **Gaps found**: `"⚠ context-coverage: <N> of <M> registered projects lack context.md and _passive.md — <slug>, … — add a context.md or mark the project passive with _passive.md."` A project counts as **registered** iff its `01-projects/<slug>/` directory contains `_overview.md` — the same convention `discoverVaultRepos()` uses. Directories lacking `_overview.md` are never counted and never listed as gaps.
758
+ - **Fully covered / no vault configured / disabled**: silent (no banner).
759
+
760
+ Non-blocking. Cross-reference: `scripts/lib/gitlab-portfolio/vcs-detect.mjs` (`discoverVaultRepos` — the canonical "registered" definition), `scripts/lib/config/context-coverage.mjs` (`_parseContextCoverage`), and issue #831.
761
+
762
+ Additionally, invoke the CLAUDE.md budget-lint probe (`scripts/lib/claude-md-budget-lint.mjs`) via `checkClaudeMdBudgetLint({ repoRoot })` (synchronous — no await). This is a **warn-only** probe — its result is rendered, never gated; the underlying `lintClaudeMd()`/CLI exit-code contract (0/1/2, `--mode hard` by default) belongs to the standalone bootstrap-time lint (`skills/bootstrap/SKILL.md` § Step 2c) and is NEVER invoked here. The helper returns `null` (silent no-op) when no CLAUDE.md/AGENTS.md resolves under `repoRoot`, when the resolved file has zero violations, or on any read/parse failure. When a non-null result is returned (`{ severity: 'warn', message }`), render `result.message` alongside the other banners:
763
+ - **Violations found**: `"⚠ CLAUDE.md budget lint: <N> violation(s) (<rule names>) in <file> — run \`node scripts/lib/claude-md-budget-lint.mjs --mode warn\` for details."` — `<rule names>` is the de-duplicated set of violated rule ids (`max-lines`, `max-line-chars`, `provenance-header`) present in the file.
764
+ - **Clean file / no instruction file**: silent (no banner).
765
+
766
+ Non-blocking. Cross-reference: `scripts/lib/instruction-budget-guard.mjs` (sibling directive-COUNT probe over `.claude/rules/*.md` — this probe measures raw-file PROPERTIES of CLAUDE.md/AGENTS.md itself, a distinct dimension) and issue #878 (FA2b).
767
+
768
+ Additionally, invoke the tests:src-ratio probe (`scripts/lib/tests-src-ratio.mjs`) via `checkTestsSrcRatio({ repoRoot })` (synchronous — no await). It returns `null` (silent no-op) when the ratio is inside the TV-003 corridor, when `repoRoot` is missing, or on any measurement failure. When a non-null result is returned (`{ severity: 'warn', message, ratio, ceiling }`), render `result.message` alongside the other banners.
769
+
770
+ **Why this is a banner and not a gate.** `.claude/rules/test-value.md` § TV-003 names the ceiling as the trigger for a CONSOLIDATION wave — the rule's operative instrument. Before this wiring the trigger fired into a void: the only references were two rule files asking a human to run the command, so the condition could be true for months with nothing saying so (it was true, at 1.70, on the commit that introduced the script). The rule's refusal of a bidirectional ratchet stands unchanged — this surfaces the trigger, it does not block on it. `--check` remains deliberately unwired from CI.
771
+
772
+ Non-blocking. Cross-reference: `.claude/rules/test-value.md` § TV-003 (the corridor rule and why a ratchet was rejected), `.claude/rules/testing.md` § Coverage Enforcement (the 70% floor that binds independently), and issue #930.
773
+
774
+ Additionally, invoke the project-hygiene probe family (`scripts/lib/project-hygiene.mjs`) via `checkProjectHygiene({ repoRoot })` (synchronous — no await). **This is the only probe in Phase 4 besides `ci-status` that inspects the PROJECT rather than the orchestrator's own substrate** — every other probe above measures vault, peer-cards, loop readiness, instruction budget, or this tool's own ledger. It is deliberately NOT config-gated: a hygiene check nobody enables finds nothing, which is how the equivalent coverage was lost before (see `skills/session-end/discovery-scan.md` — the discovery scan defaults OFF for exactly the `housekeeping` session type that most needs it).
775
+
776
+ The helper returns `null` (silent no-op) when `repoRoot` is missing/non-string, when the path is not a git repository, or when every check passes. When a non-null result is returned (`{ severity: 'warn', message, findings, mechanical }`), render `result.message` alongside the other banners:
777
+ - **Findings present**: render the message verbatim. It already leads with the count and the mechanically-fixable subset, then names the top 3 and summarises the remainder — this shape was chosen because a flat list stops being read past roughly 25 findings.
778
+ - **Healthy repo**: silent (no banner).
779
+
780
+ Use `result.mechanical` when proposing session scope: findings with `fixable: true` (aged artifacts, ignored ballast, a missing CI audit step) are safe batch work, while the rest (release cadence, absent CI, undocumented configuration) need an operator decision and belong in the Q&A, not in an auto-fix batch.
781
+
782
+ The checks are: release-tag/CHANGELOG distance from HEAD, ignored working-tree ballast plus files that are neither tracked nor ignored, aged `.orchestrator/` artifacts, CI pipeline presence and dependency-audit coverage, and `.env.example` presence. Two high-yield checks are intentionally NOT here: **docs-drift** is already covered by `claude-md-drift-check` (it only runs at session-END, so the gap is scheduling, not implementation), and **env completeness** is omitted because diffing `process.env` reads against `.env.example` produced a 100% false-positive rate against code that reads configuration through a central schema module.
783
+
784
+ Non-blocking. Cross-reference: `scripts/lib/ci-status-banner.mjs` (the sibling project-facing probe) and `.claude/rules/test-value.md` § TV-005 (why structural gates beat unit-test volume).
785
+
786
+ All banners are non-blocking — display in the Session Overview, do not halt the session. If `bootstrap-lock-freshness.mjs` is absent (pre-#186 plugin install) or `peer-cards/staleness-banner.mjs` is absent (pre-#503 plugin install) or `loop-readiness-banner.mjs` is absent (pre-#633 plugin install) or `instruction-budget-guard.mjs` is absent (pre-#687 plugin install) or `reconcile-nudge-banner.mjs` is absent (pre-#723 plugin install) or `sessions-staleness-banner.mjs` is absent (pre-#724 plugin install) or `sessions-integrity-banner.mjs` is absent (pre-#958 plugin install) or `owner-config-banner.mjs` is absent (pre-#820 plugin install) or `moc-staleness-banner.mjs` / `context-coverage-banner.mjs` are absent (pre-#831 plugin install) or `claude-md-budget-lint.mjs` is absent (pre-#878 plugin install), skip silently.
718
787
 
719
788
  ## Phase 4.5: Resource Health (v3.1.0)
720
789
 
@@ -940,6 +1009,69 @@ if (bannerText) {
940
1009
 
941
1010
  Cross-reference: PRD F2.3 acceptance criteria (#505); `scripts/lib/memory-banner.mjs` API (`renderMemoryBanner`, `readBannerInputs`; test-only exports `_formatBanner`, `_extractCardExcerpt` carry the `_`-prefix per #542 convention).
942
1011
 
1012
+ ## Phase 6.8: Telemetry Consent (one-time, #845)
1013
+
1014
+ > Skip this phase silently when `persistence: false` in Session Config. Also skip silently when non-interactive (headless / CI — no TTY to prompt on), and when the consent decision has already been made (stored `granted`/`denied`, an env override, or the fleet flag). In all of these `resolveConsent().prompt` is `false` and the phase is a no-op — it must NEVER print anything or slow session-start in the common (already-decided / headless) case.
1015
+
1016
+ Anonymous usage telemetry is **strictly opt-in** and, on a host that has never decided, is offered exactly once via a single interactive AskUserQuestion. The consent machine lives in `scripts/lib/telemetry/consent.mjs`; this phase only decides *whether* to prompt and then records the operator's answer. The `resolveConsent()` precedence machine is fail-closed — `prompt` is `true` only for a fresh, interactive, not-yet-decided, not-fleet, not-env-overridden host.
1017
+
1018
+ ```javascript
1019
+ import { readTelemetryState, resolveConsent, isHeadless, grantConsent, denyConsent } from '${PLUGIN_ROOT}/scripts/lib/telemetry/consent.mjs';
1020
+ import { loadOwnerConfig } from '${PLUGIN_ROOT}/scripts/lib/owner-yaml.mjs';
1021
+
1022
+ const c = resolveConsent({
1023
+ env: process.env,
1024
+ ownerConfig: loadOwnerConfig().config, // fleet flag lives at .telemetry.enabled (host-local owner.yaml, never committed)
1025
+ state: readTelemetryState().record, // persisted per-user decision (~/.config/session-orchestrator/telemetry.json)
1026
+ interactive: !isHeadless(), // fail-closed toward headless — anything but a confirmed TTY counts as headless
1027
+ });
1028
+ if (!c.prompt) {
1029
+ // silent no-op — already decided, env-override, fleet-enabled, or headless. Do NOT print, do NOT prompt.
1030
+ }
1031
+ ```
1032
+
1033
+ **When `c.prompt === true`**, the coordinator renders EXACTLY ONE `AskUserQuestion` (per `.claude/rules/ask-via-tool.md` AUQ-003 — the tool, never inline prose):
1034
+
1035
+ ```js
1036
+ AskUserQuestion({
1037
+ questions: [{
1038
+ question: "Anonyme Usage-Telemetrie aktivieren? Strikt opt-in, whitelist-projiziert (keine Repo-Namen/Pfade/Prompts), jederzeit abschaltbar — Details: docs/telemetry.md",
1039
+ header: "Usage Telemetry",
1040
+ multiSelect: false,
1041
+ options: [
1042
+ { label: "Ja, aktivieren", description: "Anonymer Zähl-/Struktur-Datensatz (Skill-/Phasen-Nutzung, Erfolg/Abbruch) — whitelist-projiziert, keine Pfade/Prompts/Repo-Namen. Details: docs/telemetry.md" },
1043
+ { label: "Nein", description: "Keine Telemetrie senden. Jederzeit später aktivierbar via node scripts/telemetry.mjs." },
1044
+ ],
1045
+ }],
1046
+ });
1047
+ ```
1048
+
1049
+ > **Consent-Neutralität (deliberate AUQ-003 deviation):** this is the ONE AskUserQuestion in the session flow that carries **no `(Recommended)` label on either option** — neither "Ja" nor "Nein" is tagged. AUQ-003's "option 1 is always the recommendation" convention is intentionally NOT applied here, so the operator's consent is unbiased. Do not add a recommendation to either option.
1050
+
1051
+ - **Codex CLI / Cursor IDE fallback (numbered Markdown list — AUQ-004 exception 1):**
1052
+ ```
1053
+ Anonyme Usage-Telemetrie aktivieren? Strikt opt-in, whitelist-projiziert (keine Repo-Namen/Pfade/Prompts), jederzeit abschaltbar — Details: docs/telemetry.md
1054
+ 1. Ja, aktivieren — anonymer Zähl-/Struktur-Datensatz, keine Pfade/Prompts/Repo-Namen.
1055
+ 2. Nein — keine Telemetrie senden.
1056
+ Reply with the number of your choice. (No option is pre-recommended — the choice is yours.)
1057
+ ```
1058
+
1059
+ On the operator's answer:
1060
+ - **"Ja, aktivieren"** → call `grantConsent()`. Then add a single confirmation line to the Session Overview: `Telemetry: enabled — ändern via node scripts/telemetry.mjs`.
1061
+ - **"Nein"** → call `denyConsent()`. Then add: `Telemetry: disabled — ändern via node scripts/telemetry.mjs`.
1062
+
1063
+ Both helpers atomically persist the decision (read-modify-write, `anon_id` fields preserved) to `~/.config/session-orchestrator/telemetry.json`.
1064
+
1065
+ ### Fleet mode (host-local, no prompt)
1066
+
1067
+ Setting `telemetry:\n enabled: true` in the host-local `~/.config/session-orchestrator/owner.yaml` (never committed — same host-local-data contract as `.claude/rules/owner-persona.md`) enables telemetry across every repo on the host WITHOUT ever prompting: `resolveConsent()` then returns `prompt: false` with state `enabled-fleet`, so this phase is a silent no-op. The per-shell escape hatches `SO_TELEMETRY_DISABLED=1` and `DO_NOT_TRACK` outrank the fleet flag for a single shell. See `docs/telemetry.md` for the full precedence table (PRD FA5).
1068
+
1069
+ ### One-time guarantee
1070
+
1071
+ The decision persists host-locally in `~/.config/session-orchestrator/telemetry.json`; once `consent` is non-`null` (granted or denied), `resolveConsent().prompt` stays `false` and this phase never fires again on that host — no repeat prompting across repos or sessions.
1072
+
1073
+ Cross-reference: GitLab #845 (Epic #841); `docs/prd/2026-07-20-anonymous-usage-telemetry.md` §3 FA1/FA5; `docs/telemetry.md`; consent API in `scripts/lib/telemetry/consent.mjs` (`resolveConsent`, `grantConsent`, `denyConsent`, `isHeadless`, `readTelemetryState`).
1074
+
943
1075
  ## Phase 7: Research (session type dependent)
944
1076
 
945
1077
  > **Note:** Implementation-specific research (library APIs, best practices for specific code changes) is deferred to session-plan, which knows the exact scope. Session-start focuses on state analysis.
@@ -953,6 +1085,15 @@ Cross-reference: PRD F2.3 acceptance criteria (#505); `scripts/lib/memory-banner
953
1085
  - Focus on git cleanup, documentation currency, CI health
954
1086
  - Skip deep research — prioritize operational tasks
955
1087
  - Run token efficiency check: `bash "${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-$PLUGIN_ROOT}}/scripts/token-audit.sh"` and include findings in Session Overview. Flag any HIGH/WARN items as recommended housekeeping tasks.
1088
+ - **Run the drift check as a work-list, not as a gate:**
1089
+ ```bash
1090
+ node "${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-$PLUGIN_ROOT}}/skills/claude-md-drift-check/checker.mjs" --mode warn
1091
+ ```
1092
+ `--mode warn` always exits 0 and returns findings as JSON — it must never block session-start. Summarise `errors[]` and `warnings[]` by check name in the Session Overview and offer them as candidate scope in the Phase 8 Q&A.
1093
+
1094
+ **Why here and not only at close.** The same checker already runs at session-end (`skills/session-end/SKILL.md` Phase 2), where it verifies the work just done. That is the wrong moment to *discover* drift: doc-vs-reality drift was the single most frequently confirmed finding in the six-repo diagnostic run (6 of 6 repos), and a housekeeping session that only learns about it at close cannot act on it. Running it at the start turns it into the session's work-list. It is deliberately scoped to `housekeeping` — for `feature`/`deep` sessions this list is a distraction from the agreed scope, and the close-time run still covers them.
1095
+
1096
+ **Read the output critically.** In a consumer repo the checker reported 69 errors of which zero concerned that repo — all were dangling `## See Also` citations inside vendored, never-curated baseline rule copies. Before proposing any of it as scope, check whether a finding points at repo-owned content or at vendored files; report the split rather than the raw count.
956
1097
 
957
1098
  ## Phase 7.1: Issue Premise Verification (#730/H3)
958
1099
 
@@ -1038,6 +1179,7 @@ After user alignment:
1038
1179
  | (inline) Phase 2.7 | GitLab Portfolio Snapshot — dry-run aggregation banner; gated on `gitlab-portfolio.enabled: true` + `vault-integration.enabled: true`; dispatches `scripts/lib/gitlab-portfolio/cli.mjs --dry-run`; 8s timeout; never blocks session-start |
1039
1180
  | `phase-4-5-resource-health.md` | Phase 4.5 full procedural body — resource probe, adaptive thresholds table, AUQ presentation, session-plan cap handoff |
1040
1181
  | (inline) Phase 6.7 | Memory Banner — `renderMemoryBanner` from `scripts/lib/memory-banner.mjs` (#505); silent no-op when `memory.banner.enabled: false` or `persistence: false` |
1182
+ | (inline) Phase 6.8 | Telemetry Consent (one-time, #845) — `resolveConsent()` from `scripts/lib/telemetry/consent.mjs` decides `prompt`; when true, ONE consent-neutral `AskUserQuestion` (no `(Recommended)` on either option) → `grantConsent()`/`denyConsent()`; silent no-op when `persistence: false`, headless/CI, already-decided, fleet-enabled (`owner.yaml telemetry.enabled`), or env-overridden (`SO_TELEMETRY_DISABLED=1`/`DO_NOT_TRACK`); host-local one-time guarantee via `~/.config/session-orchestrator/telemetry.json` |
1041
1183
  | `phase-7-1-premise-check.md` | Phase 7.1 full procedural body — claim extraction, one-grep-per-claim verification, verdict table, emission block format |
1042
1184
  | `phase-7-5-mode-selector.md` | Phase 7.5 full procedural body — buildLiveSignals, selectMode invocation, banner rendering, AUQ ordering protocol, graceful no-op rules, accuracy learning write |
1043
1185
  | `phase-8-5-express-path.md` | Phase 8.5 full procedural body — activation conditions, banner, coordinator-direct execution, STATE.md logging, condition examples table |
@@ -10,7 +10,10 @@ Read `.orchestrator/host.json` (written by `hooks/on-session-start.mjs`) and run
10
10
  // Conceptual — the wave-executor and session-plan skills call these directly.
11
11
  import { probe, evaluate } from '$PLUGIN_ROOT/scripts/lib/resource-probe.mjs';
12
12
  const snapshot = await probe();
13
- const verdict = evaluate(snapshot, config['resource-thresholds']);
13
+ const verdict = evaluate(snapshot, config['resource-thresholds'], {
14
+ heavyRepo: config['heavy-repo'],
15
+ agentsPerWave: config['agents-per-wave'],
16
+ });
14
17
  ```
15
18
 
16
19
  The `evaluate()` result has three fields:
@@ -18,16 +21,20 @@ The `evaluate()` result has three fields:
18
21
  - `reasons`: array of human-readable explanations
19
22
  - `recommended_agents_per_wave_cap`: integer cap (0 = coordinator-direct) or null
20
23
 
24
+ The third `options` argument is optional (HR-003/HR-004, baseline #60) — when `config['heavy-repo']` is `true`, the cap is forced to at most `config['agents-per-wave']` regardless of the live verdict (static preflight ceiling; more-restrictive-wins against whatever the resource signals already computed). Omitting `options` entirely preserves pre-#60 behaviour.
25
+
21
26
  ## Adaptive Rules (default thresholds; configurable via `resource-thresholds`)
22
27
 
23
28
  | Signal | Threshold | Action |
24
29
  |--------|-----------|--------|
25
30
  | RAM free below `ram-free-min-gb` (default 4) | warn | Cap `agents-per-wave` at 2 |
26
31
  | RAM free below `ram-free-critical-gb` (default 2) | critical | Recommend coordinator-direct (0 agents) |
27
- | CPU load above `cpu-load-max-pct` (default 80) sustained | warn | Cap `agents-per-wave` at 2 |
32
+ | CPU load above `cpu-load-max-pct` (default 80) sustained — judged on **min(1m, 5m)** load average (#943) | warn | Cap `agents-per-wave` at 2 |
28
33
  | Claude processes ≥ `concurrent-sessions-warn` (default 5) | warn | Warn; suggest sequencing or waiting |
29
34
  | SSH session detected AND `ssh-no-docker: true` | info | Append note: host is SSH-attached, Docker-dependent steps should run on a local dev host |
30
35
 
36
+ **CPU methodology (#943):** the gate/probe runs right after the coordinator's own CPU-saturating quality-gate run by construction, so the 1-minute load average systematically carries that decaying tail (observed: 96% → 75% within 36s). `probe()` therefore also emits `cpu_load_5m` / `cpu_load_5m_pct`, and `evaluate()` + `evaluateWaveResourceGate()` judge the CPU threshold on **min(1m, 5m)**: only-1m-high is reported as an informational "decaying transient" reason without capping; both-high (genuine sustained load) still caps. When `cpu_load_5m_pct` is `null` (Windows, zero-load), judging falls back to the 1m-derived `cpu_load_pct` alone.
37
+
31
38
  ## Presentation
32
39
 
33
40
  Print a one-line Resource Health verdict immediately after Phase 4's output:
@@ -36,6 +43,12 @@ Print a one-line Resource Health verdict immediately after Phase 4's output:
36
43
  Resource Health: ⚠ warn — RAM free 3.1 GB below threshold 4 GB; capping agents-per-wave at 2.
37
44
  ```
38
45
 
46
+ When `config['heavy-repo']` is `true` and the HR-004 preflight ceiling actually reduces `recommended_agents_per_wave_cap` below what the live verdict alone would have produced, print an additional banner line right after the verdict line:
47
+
48
+ ```
49
+ ⚠ Heavy-repo mode active — agents-per-wave capped to 4 (Session Config heavy-repo: true)
50
+ ```
51
+
39
52
  When verdict is `warn` or `critical`, use the AskUserQuestion tool to present:
40
53
  1. **Proceed as recommended** (apply the cap) — Recommended
41
54
  2. **Proceed as originally planned** (user accepts the risk)
@@ -201,8 +201,8 @@ If the glab query fails, log the error and proceed with an empty fingerprint set
201
201
 
202
202
  | Severity | Action |
203
203
  |----------|--------|
204
- | `critical` | Auto-create issue — no AUQ. Label: `from:test-runner,priority:critical` |
205
- | `high` | Auto-create issue — no AUQ. Label: `from:test-runner,priority:high` |
204
+ | `critical` | Auto-create issue — no AUQ. Label: `from:test-runner,priority::critical` |
205
+ | `high` | Auto-create issue — no AUQ. Label: `from:test-runner,priority::high` |
206
206
  | `medium` | Batched AUQ triage (see below) |
207
207
  | `low` | Batched AUQ triage (see below) |
208
208
 
@@ -18,7 +18,7 @@ Phase 1 ships a self-contained validator that reads every `.md` file under `VAUL
18
18
 
19
19
  ### Files
20
20
 
21
- - `validator.mjs` — Node.js ESM validator. Uses `zod` + `yaml` npm packages. Reads `VAULT_DIR` (env or default cwd), walks the tree, skipping `node_modules/`, `.git/`, `.obsidian/`, `90-archive/`. For each `.md`: parses frontmatter, validates against the inline Zod schema, extracts `[[wiki-links]]`, verifies each target resolves. Emits JSON report on stdout.
21
+ - `validator.mjs` — Node.js ESM validator. Uses `zod` + `yaml` npm packages. Reads `VAULT_DIR` (env or default cwd), walks the tree, skipping `node_modules/`, `.git/`, `.obsidian/`. `90-archive/` is walked but never *checked* (#833): archived notes stay in the link-target register — so an inbound `[[wiki-link]]` to an archived note resolves instead of dangling — while their frontmatter is skipped and counted in `archived_skipped_count`. For each `.md`: parses frontmatter, validates against the inline Zod schema, extracts `[[wiki-links]]`, verifies each target resolves. Emits JSON report on stdout.
22
22
  - `validator.sh` — Thin POSIX wrapper. Resolves `VAULT_DIR` from arg 1 or env, self-bootstraps deps via `pnpm install --silent` on first run, execs the Node validator. Session-end and other callers use this entry point.
23
23
  - `package.json` — Declares `zod` (`^3.24.0`, matching projects-baseline) and `yaml` (`^2.5.0`) as deps. `pnpm-lock.yaml` is committed; `node_modules/` is gitignored.
24
24
  - `tests/validator.bats` — 16 BATS cases covering clean vaults, broken frontmatter, missing required fields, dangling links, no-vault skipping, README-style files, nested directories, and archive/obsidian exclusion.
@@ -13,9 +13,9 @@
13
13
  }
14
14
  },
15
15
  "node_modules/yaml": {
16
- "version": "2.9.0",
17
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
18
- "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
16
+ "version": "2.8.3",
17
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
18
+ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
19
19
  "license": "ISC",
20
20
  "bin": {
21
21
  "yaml": "bin.mjs"