session-orchestrator 5.3.0 → 5.5.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 (260) hide show
  1. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
  2. package/.agents/skills/test-audit/SKILL.md +21 -0
  3. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  4. package/.claude-plugin/marketplace.json +1 -1
  5. package/.claude-plugin/plugin.json +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  8. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  9. package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
  10. package/.cursor/commands/test-audit.md +14 -0
  11. package/.cursor/rules/060-evolve.mdc +28 -13
  12. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  13. package/.cursor/skills/test-audit/SKILL.md +12 -0
  14. package/.cursor-plugin/plugin.json +1 -1
  15. package/.orchestrator/policy/blocked-commands.json +1 -1
  16. package/AGENTS.md +9 -31
  17. package/CHANGELOG.md +162 -0
  18. package/README.md +14 -10
  19. package/docs/agent-authoring.md +2 -2
  20. package/docs/baseline.md +10 -1
  21. package/docs/ci-setup.md +154 -4
  22. package/docs/components.md +11 -11
  23. package/docs/events-schema.md +25 -19
  24. package/docs/install.md +14 -2
  25. package/docs/migration-v4.md +24 -17
  26. package/docs/persona-panel.md +5 -4
  27. package/docs/plugin-architecture-v3.md +1 -1
  28. package/docs/rule-authoring.md +27 -12
  29. package/docs/scope-collision-guard.md +40 -16
  30. package/docs/session-config-reference.md +61 -12
  31. package/docs/session-config-template.md +11 -2
  32. package/docs/vault-docs-architecture.md +8 -4
  33. package/hooks/_lib/hook-import-set.json +42 -29
  34. package/hooks/_lib/lock-bootstrap.mjs +118 -26
  35. package/hooks/config-protection.mjs +1 -1
  36. package/hooks/enforce-commands.mjs +28 -13
  37. package/hooks/enforce-scope.mjs +201 -23
  38. package/hooks/hooks-codex.json +1 -1
  39. package/hooks/hooks-cursor.json +5 -0
  40. package/hooks/hooks-pi.json +5 -0
  41. package/hooks/hooks.json +6 -1
  42. package/hooks/on-session-start.mjs +57 -2
  43. package/hooks/on-stop.mjs +70 -168
  44. package/hooks/post-bash-write-verify.mjs +98 -17
  45. package/hooks/post-edit-validate.mjs +15 -5
  46. package/hooks/post-tool-batch-wave-signal.mjs +32 -152
  47. package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
  48. package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
  49. package/hooks/pre-bash-staging-fence.mjs +29 -8
  50. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  51. package/hooks/pre-task-scope-disjoint.mjs +1143 -279
  52. package/hooks/subagent-telemetry.mjs +96 -19
  53. package/hooks/wave-scope-commit-guard.mjs +168 -72
  54. package/package.json +2 -6
  55. package/pi/prompts/test-audit.md +12 -0
  56. package/rules/README.md +6 -1
  57. package/rules/_index.md +3 -3
  58. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  59. package/rules/opt-in-domain/prompt-caching.md +5 -1
  60. package/rules/opt-in-stack/backend-data.md +9 -1
  61. package/rules/opt-in-stack/backend.md +7 -1
  62. package/rules/opt-in-stack/frontend.md +6 -1
  63. package/rules/opt-in-stack/security-web.md +7 -1
  64. package/rules/opt-in-stack/swift.md +5 -1
  65. package/scripts/apply-session-learnings.mjs +513 -0
  66. package/scripts/archive-closed-prds.mjs +40 -12
  67. package/scripts/autopilot.mjs +57 -54
  68. package/scripts/backfill-abandoned-sessions.mjs +81 -6
  69. package/scripts/backfill-learnings-from-vault.mjs +49 -22
  70. package/scripts/baseline-archetypes.mjs +1 -1
  71. package/scripts/check-package-manager.mjs +62 -5
  72. package/scripts/codex-install.mjs +3 -0
  73. package/scripts/cursor-install.mjs +47 -6
  74. package/scripts/emit-session.mjs +298 -2
  75. package/scripts/export-hw-learnings.mjs +40 -10
  76. package/scripts/fleet-checkin.mjs +170 -0
  77. package/scripts/lib/agent-frontmatter.mjs +3 -3
  78. package/scripts/lib/auto-dream.mjs +7 -6
  79. package/scripts/lib/autopilot/flags.mjs +6 -1
  80. package/scripts/lib/autopilot/loop.mjs +32 -13
  81. package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
  82. package/scripts/lib/baseline-archetypes.mjs +41 -22
  83. package/scripts/lib/blocked-commands-policy.mjs +40 -0
  84. package/scripts/lib/common.mjs +41 -0
  85. package/scripts/lib/config/host-paths.mjs +79 -0
  86. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  87. package/scripts/lib/config/reaper.mjs +7 -6
  88. package/scripts/lib/config/vault-integration.mjs +151 -16
  89. package/scripts/lib/config-schema.mjs +13 -0
  90. package/scripts/lib/config.mjs +75 -10
  91. package/scripts/lib/eval/engine.mjs +13 -0
  92. package/scripts/lib/events-retention-banner.mjs +393 -0
  93. package/scripts/lib/events-rotation.mjs +21 -6
  94. package/scripts/lib/events.mjs +72 -5
  95. package/scripts/lib/fleet-protocol.mjs +288 -0
  96. package/scripts/lib/gates/gate-baseline.mjs +13 -3
  97. package/scripts/lib/gates/gate-full.mjs +7 -2
  98. package/scripts/lib/gates/gate-helpers.mjs +93 -2
  99. package/scripts/lib/gates/gate-incremental.mjs +23 -22
  100. package/scripts/lib/gates/gate-per-file.mjs +16 -7
  101. package/scripts/lib/hardening.mjs +1 -0
  102. package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
  103. package/scripts/lib/instruction-budget-guard.mjs +77 -11
  104. package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
  105. package/scripts/lib/learnings/io.mjs +274 -37
  106. package/scripts/lib/learnings/schema.mjs +163 -15
  107. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  108. package/scripts/lib/lock-reaper.mjs +38 -14
  109. package/scripts/lib/maintenance-due-banner.mjs +23 -8
  110. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  111. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  112. package/scripts/lib/named-vault-resolver.mjs +147 -5
  113. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  114. package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
  115. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  116. package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
  117. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  118. package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
  119. package/scripts/lib/orphan-reaper/scan.mjs +510 -0
  120. package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
  121. package/scripts/lib/orphan-reaper.mjs +47 -1315
  122. package/scripts/lib/owner-yaml.mjs +104 -6
  123. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  124. package/scripts/lib/platform.mjs +206 -2
  125. package/scripts/lib/plugin-update-banner.mjs +3 -1
  126. package/scripts/lib/process-group.mjs +134 -22
  127. package/scripts/lib/project-hygiene.mjs +21 -1
  128. package/scripts/lib/quality-gate.mjs +45 -5
  129. package/scripts/lib/resource-probe.mjs +1 -1
  130. package/scripts/lib/rule-loader.mjs +100 -62
  131. package/scripts/lib/rules-sync.mjs +27 -12
  132. package/scripts/lib/scope-baseline.mjs +119 -29
  133. package/scripts/lib/scope-echo.mjs +7 -0
  134. package/scripts/lib/scope-gate.mjs +194 -27
  135. package/scripts/lib/session-close-backfill.mjs +129 -4
  136. package/scripts/lib/session-end/tail-runner.mjs +10 -5
  137. package/scripts/lib/session-lock-shape.mjs +6 -3
  138. package/scripts/lib/session-lock.mjs +320 -67
  139. package/scripts/lib/session-record-repair.mjs +183 -26
  140. package/scripts/lib/session-registry.mjs +67 -17
  141. package/scripts/lib/session-schema/constants.mjs +30 -4
  142. package/scripts/lib/session-schema/validator.mjs +28 -1
  143. package/scripts/lib/session-start-probes.mjs +242 -68
  144. package/scripts/lib/session-token-rollup.mjs +252 -54
  145. package/scripts/lib/sessions-canonical.mjs +100 -9
  146. package/scripts/lib/sessions-staleness-banner.mjs +3 -1
  147. package/scripts/lib/sh-quote.mjs +29 -0
  148. package/scripts/lib/subagents-schema.mjs +30 -3
  149. package/scripts/lib/tail-window.mjs +27 -3
  150. package/scripts/lib/telemetry/pricing.mjs +121 -71
  151. package/scripts/lib/telemetry/sync.mjs +17 -0
  152. package/scripts/lib/tests-src-ratio.mjs +215 -23
  153. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  154. package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
  155. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  156. package/scripts/lib/ux-grill/reconcile.mjs +1 -1
  157. package/scripts/lib/validate/check-agents.mjs +1 -1
  158. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  159. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  160. package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
  161. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  162. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  163. package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
  164. package/scripts/lib/validate-vendored-rules.mjs +206 -81
  165. package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
  166. package/scripts/lib/vault-mirror/namespace.mjs +47 -26
  167. package/scripts/lib/vault-mirror/process.mjs +350 -62
  168. package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
  169. package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
  170. package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
  171. package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
  172. package/scripts/lib/vault-status/board-writer.mjs +53 -12
  173. package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
  174. package/scripts/lib/vcs-repo-spec.mjs +8 -0
  175. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  176. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  177. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  178. package/scripts/lib/wave-resource-gate.mjs +34 -8
  179. package/scripts/materialize-wave-scope.mjs +7 -4
  180. package/scripts/memory-propose.mjs +18 -5
  181. package/scripts/release-session-lock.mjs +2 -1
  182. package/scripts/release.mjs +11 -3
  183. package/scripts/repair-invalid-sessions.mjs +37 -4
  184. package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
  185. package/scripts/run-quality-gate.mjs +331 -307
  186. package/scripts/self-update.mjs +229 -0
  187. package/scripts/site-numbers.mjs +163 -5
  188. package/scripts/sweep-expired-learnings.mjs +603 -20
  189. package/scripts/sweep-expired-rules.mjs +2 -1
  190. package/scripts/validate-plugin.mjs +7 -0
  191. package/scripts/validate-wave-scope.mjs +21 -2
  192. package/scripts/vault-backfill.mjs +50 -37
  193. package/scripts/vault-integration-watcher.mjs +2 -1
  194. package/scripts/vault-mirror.mjs +238 -75
  195. package/scripts/wave-scope-binding.mjs +2 -1
  196. package/skills/_shared/config-reading.md +3 -3
  197. package/skills/_shared/fleet-protocol.md +134 -0
  198. package/skills/_shared/state-ownership.md +41 -12
  199. package/skills/autopilot/SKILL.md +25 -11
  200. package/skills/bootstrap/SKILL.md +6 -4
  201. package/skills/bootstrap/_shared-template.md +97 -2
  202. package/skills/bootstrap/fast-template.md +4 -1
  203. package/skills/bootstrap/private-contract.md +2 -1
  204. package/skills/bootstrap/public-fallback.md +6 -3
  205. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
  206. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  207. package/skills/bootstrap/standard-template.md +2 -0
  208. package/skills/claude-md-drift-check/SKILL.md +10 -10
  209. package/skills/claude-md-drift-check/checker.mjs +26 -18
  210. package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
  211. package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
  212. package/skills/discovery/probes/vault-staleness.mjs +9 -3
  213. package/skills/docs-orchestrator/SKILL.md +12 -5
  214. package/skills/docs-orchestrator/audience-mapping.md +5 -3
  215. package/skills/evolve/SKILL.md +13 -4
  216. package/skills/evolve/references/evolve-analyze-mode.md +42 -18
  217. package/skills/memory-cleanup/SKILL.md +1 -1
  218. package/skills/persona-panel/SKILL.md +2 -2
  219. package/skills/persona-panel/persona-format.md +2 -2
  220. package/skills/persona-panel/presets/designer-lens.md +1 -1
  221. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  222. package/skills/persona-panel/presets/pm-lens.md +1 -1
  223. package/skills/plan/mode-retro.md +19 -4
  224. package/skills/quality-gates/SKILL.md +6 -4
  225. package/skills/remote-offload/SKILL.md +4 -2
  226. package/skills/session-end/learning-patterns.md +23 -13
  227. package/skills/session-end/metrics-collection.md +6 -7
  228. package/skills/session-end/phase-3-2-docs-verification.md +7 -6
  229. package/skills/session-end/plan-verification.md +25 -9
  230. package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
  231. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  232. package/skills/session-end/references/session-summary-template.md +1 -1
  233. package/skills/session-end/session-metrics-write.md +65 -68
  234. package/skills/session-plan/SKILL.md +0 -3
  235. package/skills/session-start/SKILL.md +8 -2
  236. package/skills/session-start/references/operations-contract.md +6 -0
  237. package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
  238. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  239. package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
  240. package/skills/test-audit/SKILL.md +152 -0
  241. package/skills/test-audit/references/campaign.md +111 -0
  242. package/skills/test-audit/references/mutate.sh +50 -0
  243. package/skills/vault-mirror/SKILL.md +21 -11
  244. package/skills/wave-executor/SKILL.md +2 -2
  245. package/skills/wave-executor/circuit-breaker.md +1 -1
  246. package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
  247. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
  248. package/skills/wave-executor/references/wave-loop-review.md +4 -6
  249. package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
  250. package/templates/personas/README.md +5 -5
  251. package/templates/personas/accounting-compliance.v1.md +1 -1
  252. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  253. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  254. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  255. package/templates/personas/buyer-p3-build.v1.md +1 -1
  256. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  257. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  258. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  259. package/templates/personas/klima-ai-expert.v1.md +1 -1
  260. package/templates/personas/klima-physicist.v1.md +1 -1
@@ -236,10 +236,11 @@ If issues were created and a 5+-action Epic wrapper was used (see `SKILL.md` §
236
236
 
237
237
  ### 3.3 Update Learnings
238
238
 
239
- Append new patterns to `.orchestrator/metrics/learnings.jsonl`. Use the same JSONL schema as session-end writes (see session-end/SKILL.md for canonical field reference):
239
+ Collect new patterns as learning objects in the same strict schema session-end uses (canonical field reference: `skills/session-end/learning-patterns.md` § 3.5a):
240
240
 
241
241
  ```json
242
242
  {
243
+ "schema_version": 1,
243
244
  "id": "<uuid-v4>",
244
245
  "type": "fragile-file|effective-sizing|recurring-issue|scope-guidance|deviation-pattern",
245
246
  "subject": "<what the learning is about>",
@@ -247,11 +248,25 @@ Append new patterns to `.orchestrator/metrics/learnings.jsonl`. Use the same JSO
247
248
  "evidence": "<supporting data>",
248
249
  "confidence": 0.5,
249
250
  "source_session": "retro-YYYY-MM-DD",
250
- "created_at": "<ISO 8601>",
251
- "expires_at": "<ISO 8601 + learning-expiry-days (default: 30)>"
251
+ "scope": "local",
252
+ "host_class": null,
253
+ "anonymized": false,
254
+ "file_paths": ["<repo-relative path(s) this learning applies to>"],
255
+ "created_at": "<ISO 8601>"
252
256
  }
253
257
  ```
254
258
 
259
+ `scope` MUST be `local|private|public` — a file path goes in `file_paths`, never in `scope` (GH#69). `schema_version` is the integer `1`. Omit `expires_at`: the CLI derives it per `type` via `deriveExpiresAt()`.
260
+
261
+ Write them **only** through `scripts/apply-session-learnings.mjs` (#1446) — never by hand, never with a shell redirect. Put `{"confidence_updates": [...], "new_learnings": [...]}` in a sidecar under `.orchestrator/tmp/` via the Write tool, preview, then apply. `--decay-rate 0` keeps a retro from applying the per-session passive decay a second time:
262
+
263
+ ```bash
264
+ node scripts/apply-session-learnings.mjs --input .orchestrator/tmp/retro-learnings.json --decay-rate 0 --json
265
+ node scripts/apply-session-learnings.mjs --input .orchestrator/tmp/retro-learnings.json --decay-rate 0 --apply --json
266
+ ```
267
+
268
+ Exit `1` means a record failed validation (the message names its id) and nothing was written.
269
+
255
270
  Typical retro learning types:
256
271
  - `fragile-file` — hotspots correlating with failures or carryover
257
272
  - `effective-sizing` — wave/agent configs producing best completion rates
@@ -259,7 +274,7 @@ Typical retro learning types:
259
274
  - `scope-guidance` — session scope calibration insights
260
275
  - `deviation-pattern` — systematic plan deviations indicating planning gaps
261
276
 
262
- Before writing, check existing entries for matching `type` + `subject`. Confirm (+0.15 confidence, cap 1.0) or contradict (-0.2). New learnings start at 0.5.
277
+ Before writing, check existing entries for matching `type` + `subject`. A match is not a new learning: list its `id` in `confidence_updates` with `"operation": "confirm"` (+0.15, cap 1.0) or `"contradict"` (-0.2) — the CLI applies both. New learnings start at 0.5.
263
278
 
264
279
  ### 3.4 Optional Baseline Update
265
280
 
@@ -71,9 +71,11 @@ as the session baseline for later comparison.
71
71
 
72
72
  **Script output schema (Baseline):**
73
73
  ```json
74
- {"variant": "baseline", "typecheck": {"status": "pass|fail|skip", "output": "string"}, "test": {"status": "pass|fail|skip", "output": "string"}}
74
+ {"variant": "baseline", "typecheck": {"status": "pass|fail|skip"}, "test": {"status": "pass|fail|skip"}, "typecheck_output": "string", "test_output": "string"}
75
75
  ```
76
76
 
77
+ In every variant, each command object that was actually spawned also carries its per-command kill ladder: `timed_out` (boolean), `kill_signals` (string[]) and `survivors` (pid[]). A skipped or stubbed command has none of the three keys (absent, never `false`/`[]`). `scripts/run-quality-gate.mjs` joins them into `orchestrator.quality_gate.{passed,failed}` (#1457). A run whose joined `timed_out` is `true` is recorded as `.failed` in every variant, although Baseline, Incremental and Per-File still exit 0 (#1459, decided 2026-10-01).
78
+
77
79
  ## Variant 2: Incremental
78
80
 
79
81
  **Used by:** wave-executor (after implementation waves)
@@ -91,8 +93,8 @@ Metrics output (for consuming skills to capture):
91
93
  {
92
94
  "variant": "incremental",
93
95
  "duration_seconds": null,
94
- "typecheck": "pass|fail|skip",
95
- "test": "pass|fail|skip",
96
+ "typecheck": {"status": "pass|fail|skip"},
97
+ "test": {"status": "pass|fail|skip"},
96
98
  "errors": []
97
99
  }
98
100
  ```
@@ -182,7 +184,7 @@ node "${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-$PLUGIN_ROOT}}/scripts/run-qual
182
184
  node "${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-$PLUGIN_ROOT}}/scripts/run-quality-gate.mjs" --variant per-file --config "$CONFIG" --files specific-file.ts
183
185
  ```
184
186
 
185
- The script handles graceful degradation (missing tools → skip), structured JSON output matching the schemas above, and proper exit codes (0=pass, 1=error, 2=gate-failed).
187
+ The script handles graceful degradation (missing tools → skip), structured JSON output matching the schemas above, and proper exit codes (0=pass, 1=error, 2=gate-failed). Only Full Gate exits 2: the three reporting variants exit 0 after a failing or killed check, so their verdict is in the JSON envelope (a kill also turns the event into `.failed`; a plain failing check does not).
186
188
 
187
189
  ## Baseline Cache (#258)
188
190
 
@@ -28,12 +28,14 @@ Exit codes (`offload --help`, measured 2026-09-02): `0` ok · `1` usage/config
28
28
 
29
29
  ## 1. Decision rule — offload vs reduce
30
30
 
31
- The gate decides, not the coordinator. `applyOffloadDecision()` in `scripts/lib/wave-resource-gate.mjs` only fires when the resource verdict is already `reduce` or `coordinator-direct`, and only AFTER the HR-004 heavy-repo cap — a capped wave that offloads still respects the cap. It never probes the network; the coordinator supplies a readiness WITNESS:
31
+ The gate decides, not the coordinator. `applyOffloadDecision()` in `scripts/lib/wave-resource-gate.mjs` fires when the resource verdict is `reduce` or `coordinator-direct` (and, with `offload-first: true`, on `proceed` as well — see below), and only AFTER the HR-004 heavy-repo cap — a capped wave that offloads still respects the cap. It never probes the network; the coordinator supplies a readiness WITNESS:
32
32
 
33
33
  - `opts.remoteReady` — `{ [alias]: boolean }`, built from the SessionStart banner line `Offload <alias>: ready=yes …`, or
34
34
  - `opts.probeFn` — an async `(alias) => boolean` fallback, consulted only for aliases `remoteReady` doesn't answer for (backed by `remoteDoctor()`, i.e. `offload doctor -H <alias> --brief` parsed by `parseDoctorLine()`).
35
35
 
36
- With neither supplied, no host counts as ready and the wave stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/dispatch-common.mjs`: `impl-core`, `security-review`, `migration`, `release`, `secrets`) is never offloaded regardless of readiness.
36
+ With neither supplied, no host counts as ready and the wave stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/dispatch-common.mjs`: `impl-core`, `security-review`, `migration`, `release`, `secrets`, `incident`, `refactor-crosscut`) is never offloaded regardless of readiness.
37
+
38
+ With `offload-first: true` in Session Config (#1465, default `false`) the placement arm also fires on `proceed`, so an offloadable role goes to a witnessed-ready host without waiting for local pressure. The witness stays mandatory — without `remoteReady`/`probeFn` nothing moves — and a wave that stays local carries a named reason (`offload-first: no ready host for '<role>' — staying local`, or `… no remote-hosts declared — staying local`).
37
39
 
38
40
  ## 2. What is declared where
39
41
 
@@ -10,13 +10,13 @@
10
10
  Analyze the completed session to extract reusable learnings for future sessions.
11
11
 
12
12
  **What to extract:**
13
- - **Fragile files**: use `git log --name-only --format="" $SESSION_START_REF..HEAD | sort | uniq -c | sort -rn | head -10` to find files changed most frequently across commits this session. Files appearing in 3+ commits are candidates for fragile-file learnings. Cross-reference with `<state-dir>/STATE.md` Wave History to correlate with specific waves.
13
+ - **Fragile files**: use `git log --name-only --format="" $SESSION_START_REF..HEAD | sort | uniq -c | sort -rn | head -10` to find files changed most frequently across commits this session. Files appearing in 3+ commits are candidates for fragile-file learnings. Cross-reference with `<state-dir>/STATE.md` Wave History to correlate with specific waves. Skip this when `$SESSION_START_REF` is empty (the accessor's skip in `plan-verification.md`): `git log ..HEAD` reads as `HEAD..HEAD` and prints nothing with exit 0, which would pass for "no fragile files".
14
14
  - **Effective sizing**: actual agent count vs. planned — what worked for this complexity level
15
15
  - **Recurring issues**: same issue type appearing across waves (e.g., type errors, missing imports)
16
16
  - **Scope guidance**: was the scope too large/small? How many issues fit comfortably in one session?
17
17
  - **Deviation patterns**: read the `## Deviations` section from `<state-dir>/STATE.md` — were there plan adaptations? What triggered them? Extract as `deviation-pattern` type if a pattern emerges across sessions (e.g., "scope expansion during Impl-Core is common for this project")
18
18
 
19
- **Learning format** (append each as one JSONL line to `.orchestrator/metrics/learnings.jsonl`):
19
+ **Learning format** (collect each as a learning object; Phase 3.6 writes them through `scripts/apply-session-learnings.mjs`):
20
20
  ```json
21
21
  {
22
22
  "schema_version": 1,
@@ -27,11 +27,19 @@ Analyze the completed session to extract reusable learnings for future sessions.
27
27
  "evidence": "<what happened this session>",
28
28
  "confidence": 0.5,
29
29
  "source_session": "<session_id>",
30
- "created_at": "<ISO 8601>",
31
- "expires_at": "<ISO 8601 + learning-expiry-days (default: 30)>"
30
+ "scope": "local",
31
+ "host_class": null,
32
+ "anonymized": false,
33
+ "file_paths": ["<repo-relative path(s) this learning applies to>"],
34
+ "created_at": "<ISO 8601>"
32
35
  }
33
36
  ```
34
37
 
38
+ - `scope` MUST be one of `local|private|public`. A file PATH belongs in `file_paths`, **never** in `scope` — a producer that wrote a path into `scope` is the GH#69 root cause, and the strict `validateLearning` in the write CLI rejects it.
39
+ - `schema_version` MUST be the integer `1` (not `"1"`, not `2`).
40
+ - `expires_at` may be omitted: the CLI derives it per `type` via `deriveExpiresAt()` (`LEARNING_TTL_DAYS`), not a flat +30 days.
41
+ - `file_paths` may be omitted when the learning is not about specific files; for `fragile-file`, `file_paths: [subject]`.
42
+
35
43
  **Schema versioning** (`schema_version`, introduced 2026-04):
36
44
  - All new records MUST carry `schema_version: 1`. `scripts/lib/learnings.mjs` auto-stamps missing values on append/rewrite so callers can omit the field safely.
37
45
  - Records without `schema_version` are treated as `schema_version: 0` (pre-versioning legacy). They are read and validated successfully for backward compat, but the reader emits a one-line WARN to stderr flagging the missing tag.
@@ -39,7 +47,7 @@ Analyze the completed session to extract reusable learnings for future sessions.
39
47
 
40
48
  **Confidence updates for existing learnings:**
41
49
  Before writing new learnings, read `.orchestrator/metrics/learnings.jsonl` and check for existing entries with the same `type` + `subject` (exact string match on both fields):
42
- - If this session **confirms** an existing learning: note the update — increment `confidence` by +0.15 (cap at 1.0) and reset `expires_at` to current date + `learning-expiry-days` (default: 30)
50
+ - If this session **confirms** an existing learning: note the update — increment `confidence` by +0.15 (cap at 1.0) and reset `expires_at` via `deriveExpiresAt(now, type)` (the per-type TTL in `LEARNING_TTL_DAYS`)
43
51
  - If this session **contradicts** an existing learning: note the update — decrement `confidence` by -0.2
44
52
  - If no existing match: note as a new learning with confidence 0.5
45
53
 
@@ -59,11 +67,13 @@ Before writing new learnings, read `.orchestrator/metrics/learnings.jsonl` and c
59
67
  2. If count exceeds `memory-cleanup-threshold` (default: 5), suggest:
60
68
  "You have [N] session memory files. Consider running `/memory-cleanup` to consolidate."
61
69
  3. This is a suggestion only — not blocking
62
- 4. **Write learnings** to `.orchestrator/metrics/learnings.jsonl` (if file exists or new learnings were extracted):
63
- a. Read all existing lines from `learnings.jsonl` (if exists)
64
- b. Apply confidence updates from Phase 3.5a (confirmed: +0.15 capped at 1.0 AND reset `expires_at` to current date + `learning-expiry-days` (default: 30); contradicted: -0.2)
65
- c. Append new learnings from Phase 3.5a (those with no existing match)
66
- d. **Passive decay (#89)** — for every existing learning NOT touched this session (i.e., not in the set of learnings confirmed or contradicted in Phase 3.5a, and not newly appended in step c), subtract `learning-decay-rate` (from Session Config, default `0.05`) from its `confidence`. Clamp to 0.0 (do not produce negative values). The prune step in `e` will remove any entry that fell to `confidence <= 0.0`. Decay does NOT reset `expires_at` — let decayed entries continue to age naturally. If `learning-decay-rate` is `0.0`, skip this step entirely (opt-out).
70
+ 4. **Write learnings** to `.orchestrator/metrics/learnings.jsonl` (if the file exists or new learnings were extracted) — **only** through `scripts/apply-session-learnings.mjs` (#1446):
71
+ a. Write the Phase 3.5a result as ONE JSON object to a sidecar under `.orchestrator/tmp/` via the Write tool, e.g. `.orchestrator/tmp/session-learnings.json`:
72
+ `{"confidence_updates": [{"id": "<existing id>", "operation": "confirm"}], "new_learnings": [<learning objects per the format above>]}`
73
+ b. Dry run first and show the summary line (`read`, `confirmed`, `contradicted`, `appended`, `decayed`, `pruned`, `consolidated`, `kept`) — nothing is written:
74
+ `node scripts/apply-session-learnings.mjs --input .orchestrator/tmp/session-learnings.json --json`
75
+ c. Apply the same input: `node scripts/apply-session-learnings.mjs --input .orchestrator/tmp/session-learnings.json --apply --json`
76
+ d. The CLI performs everything the old hand-written steps did, in code: confirm (+0.15, cap 1.0, `expires_at` re-derived per type) / contradict (-0.2, floor 0); strict `validateLearning` on every new record; **passive decay (#89)** of every existing learning NOT confirmed or contradicted this session by `learning-decay-rate` (Session Config, default `0.05`; `0.0` opts out; `expires_at` is not reset); prune (`expires_at` < now OR `confidence` <= 0.0); consolidate duplicates (same `type` + `subject`, highest confidence wins); archive every removed record to `learnings-archive.jsonl`; atomic rewrite with a `.bak-<ISO>` snapshot — all via `pruneLearnings()` in `scripts/lib/learnings/expiry-sweep.mjs`.
67
77
 
68
78
  | Sessions since last touch | Confidence (starting 0.5, decay 0.05) | Status |
69
79
  |---|---|---|
@@ -72,7 +82,7 @@ Before writing new learnings, read `.orchestrator/metrics/learnings.jsonl` and c
72
82
  | 9 | 0.05 | active |
73
83
  | 10 | 0.00 | pruned next write |
74
84
 
75
- e. Prune: remove entries where `expires_at` < current date OR `confidence` <= 0.0
76
- f. Consolidate duplicates (same `type` + `subject`): keep the one with highest confidence
77
- g. Write the entire result back to `learnings.jsonl` (atomic rewrite with `>`, not append with `>>`)
85
+ e. Exit codes: `0` applied (or a clean no-op); `1` input or validation error — the message names the record id and the failing field, and **nothing was written**: fix the sidecar and re-run; `2` store/IO error — report it, do not retry by hand. Unparseable lines already in the store are NOT an error and never block the write: the CLI keeps them verbatim at the end of the store (never archived), reports `malformed: N` in its summary and prints a stderr WARN with their line numbers — carry that count into the Phase 6 report.
86
+ f. **Hand-writing the store is FORBIDDEN** — no shell rewrite, no shell append, no `jq … | tee`, and that includes "repairing" a malformed line the WARN reports: it stays until the operator decides. Those bypass validation, the `.bak` snapshot and the archive (see `skills/evolve/SKILL.md` § Critical Rules). The blocking validator `scripts/lib/validate/check-learnings-shell-writes.mjs` fails `validate-plugin` on prose that instructs one. Once the operator decides to remove a line, the ONE sanctioned path is the CLI (#1500): `node scripts/sweep-expired-learnings.mjs --drop-malformed --line N` previews it (dry run: the line's text and the store `generation`), then `node scripts/sweep-expired-learnings.mjs --drop-malformed --line N --generation <that token> --apply --repo-root .` removes it. The apply copies the whole store to `learnings.pre-drop-malformed.jsonl.bak-<ISO>` beside it first and appends one `orchestrator.learnings.malformed_dropped` record. Line numbers move with every rewrite (malformed lines go to the end), so a store changed since the dry run exits `3` — re-run the dry run; a line that is not malformed exits `1`. Both write nothing. A malformed line that still holds a readable record — behind a UTF-8 BOM, or fused after a torn prefix (the dry run reports `bom` / `embedded_record`) — is refused at `--apply` with exit `1` as well, because dropping it loses that record; `--accept-embedded` is the operator's explicit decision to drop it anyway (the pre-drop snapshot keeps the line).
87
+ g. Proof it ran: `--apply` appends one `orchestrator.learnings.session_write_applied` record to `.orchestrator/metrics/events.jsonl` (`appended`, `confirmed`, `contradicted`, `decayed`, `pruned`). No record = no write.
78
88
  h. If no existing file and no new learnings: skip
@@ -19,7 +19,7 @@ Finalize session metrics by reading the wave data accumulated during execution:
19
19
  - `total_agents`: sum of agents across all waves
20
20
  - `total_files_changed`: unique files changed across entire session (from `git diff --stat`)
21
21
  - `agent_summary`: `{complete: N, partial: N, failed: N, spiral: N}`
22
- - **Coordinator-direct fallback (#1321):** when Wave History is empty AND the session ran coordinator-direct (housekeeping — `scripts/lib/session-shape.mjs:364-376` resolves it to ONE coordinator-direct wave that `/go` never hands to wave-executor, so no wave metrics are captured — or the express path), do NOT write `waves: []` / `total_waves: 0`. Write exactly one wave entry `{wave: 1, role: "Housekeeping", agent_count: 0, coordinator_direct: true, files_changed: <N>, quality: "<pass|fail|skip — the close quality-gate result>"}` with `total_waves: 1`, `total_agents: 0`. On this path both that wave's `files_changed` and `total_files_changed` are `git diff --name-only "$SESSION_START_REF..HEAD" | wc -l` (accessor: `plan-verification.md` § SESSION_START_REF accessor). The single definition of this shape is `isCoordinatorDirectHousekeeping(record)` in `scripts/lib/session-schema/filters.mjs` (every wave `role: "Housekeeping"` AND `coordinator_direct: true`). Consumers key on it (eval gate-health, evolve effective-sizing), so write `role` and `coordinator_direct` exactly as shown. `session_type` alone never marks it.
22
+ - **Coordinator-direct fallback (#1321):** when Wave History is empty AND the session ran coordinator-direct (housekeeping — `scripts/lib/session-shape.mjs:364-376` resolves it to ONE coordinator-direct wave that `/go` never hands to wave-executor, so no wave metrics are captured — or the express path), do NOT write `waves: []` / `total_waves: 0`. Write exactly one wave entry `{wave: 1, role: "Housekeeping", agent_count: 0, coordinator_direct: true, files_changed: <N>, quality: "<pass|fail|skip — the close quality-gate result>"}` with `total_waves: 1`, `total_agents: 0`. On this path both that wave's `files_changed` and `total_files_changed` are `git diff --name-only "$SESSION_RANGE" | wc -l` (accessor: `plan-verification.md` § SESSION_START_REF accessor; an empty `$SESSION_RANGE` is a skip, not zero changes — the validator still requires a number there, so name the accessor's reason in the record's `notes` beside it). The single definition of this shape is `isCoordinatorDirectHousekeeping(record)` in `scripts/lib/session-schema/filters.mjs` (every wave `role: "Housekeeping"` AND `coordinator_direct: true`). Consumers key on it (eval gate-health, evolve effective-sizing), so write `role` and `coordinator_direct` exactly as shown. `session_type` alone never marks it.
23
23
  3. Read `.orchestrator/metrics/events.jsonl` **once** and build both event aggregates in a single pass. If the file does not exist, treat both aggregates as zero events (omit both fields per the rules below) — do NOT fail the session close.
24
24
 
25
25
  Filter all lines where `session == <session_id>`, then partition by `event` value:
@@ -59,7 +59,7 @@ Finalize session metrics by reading the wave data accumulated during execution:
59
59
  # corruption.
60
60
  METRICS_ENTRY=$(node --input-type=module -e "
61
61
  const entry = {
62
- session_id: '<branch>-<YYYY-MM-DD>-<HHmm>',
62
+ session_id: '<STATE.md frontmatter session, verbatim>',
63
63
  session_type: '<type>',
64
64
  platform: '<claude|codex>',
65
65
  started_at: '<ISO 8601 from STATE.md frontmatter started_at — already canonical>',
@@ -69,7 +69,7 @@ Finalize session metrics by reading the wave data accumulated during execution:
69
69
  total_agents: <N>,
70
70
  total_files_changed: <N>,
71
71
  agent_summary: {complete: <N>, partial: <N>, failed: <N>, spiral: <N>},
72
- waves: [/* {wave, role, agent_count, files_changed, quality, agent_count_planned?, agent_count_started?, agent_count_completed?, planned_files_count?, over_delivery_ratio?} */],
72
+ waves: [/* {wave, role, agent_count, files_changed, quality, agent_count_planned?, agent_count_started?, agent_count_completed?} */],
73
73
  // effectiveness is CONSTRUCTED EXPLICITLY (#773) — NOT left as an optional
74
74
  // field for the coordinator to remember. Leaving it optional is exactly how
75
75
  // the carryover=0 blind spot recurred (41/41 records read carryover:0).
@@ -101,7 +101,7 @@ Finalize session metrics by reading the wave data accumulated during execution:
101
101
  **Canonical JSONL schema** (for field reference — populated by the snippet above):
102
102
  ```json
103
103
  {
104
- "session_id": "<branch>-<YYYY-MM-DD>-<HHmm>",
104
+ "session_id": "<STATE.md frontmatter session, verbatim>",
105
105
  "session_type": "<type>",
106
106
  "platform": "<claude|codex>",
107
107
  "started_at": "<canonical ISO 8601 from STATE.md>",
@@ -112,7 +112,7 @@ Finalize session metrics by reading the wave data accumulated during execution:
112
112
  "total_files_changed": N,
113
113
  "agent_summary": {"complete": N, "partial": N, "failed": N, "spiral": N},
114
114
  "waves": [
115
- {"wave": 1, "role": "Discovery", "agent_count": N, "files_changed": N, "quality": "pass|fail|skip", "agent_count_planned": N, "agent_count_started": N, "agent_count_completed": N, "planned_files_count": N, "over_delivery_ratio": 0.0},
115
+ {"wave": 1, "role": "Discovery", "agent_count": N, "files_changed": N, "quality": "pass|fail|skip", "agent_count_planned": N, "agent_count_started": N, "agent_count_completed": N},
116
116
  ...
117
117
  ],
118
118
  "discovery_stats": {
@@ -164,7 +164,7 @@ Finalize session metrics by reading the wave data accumulated during execution:
164
164
 
165
165
  > **ISO-8601 canonical format (#540):** `started_at`, `completed_at`, and `lease_acquired_at` MUST match the regex `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/`. The validating writer (`scripts/emit-session.mjs`) rejects any non-canonical form. Use `new Date().toISOString()` (Node-native, always canonical) — never hand-edit fractional digits or timezone suffixes.
166
166
 
167
- > The `session_id` uses `<HHmm>` from the `started_at` timestamp to ensure uniqueness when multiple sessions run on the same branch in one day.
167
+ > The `session_id` is the STATE.md frontmatter `session` value, copied verbatim — the semantic label session-start minted, which `.orchestrator/current-session.json` also carries as `semantic_session_id`. Never construct one (the former `<branch>-<YYYY-MM-DD>-<HHmm>` recipe is wrong): `scripts/emit-session.mjs` adopts `raw_session_id`, the token rollup and the events-derived `session_start_ref` only when the record's `session_id` equals the marker's `semantic_session_id`; any other label is WARNed and those fields are omitted.
168
168
 
169
169
  > **Conditional fields:**
170
170
  > - `discovery_stats`: populated ONLY when `discovery-on-close: true` in Session Config AND Phase 1.5 executed successfully. Source: the stats object returned by the discovery skill (see discovery skill Phase 4.6 for schema). When discovery runs in **embedded mode** (Phases 0-4 only), `user_dismissed`, `issues_created`, and `actioned` per category will always be `0` — embedded mode does not perform user triage (Phase 5) or issue creation (Phase 6).
@@ -172,7 +172,6 @@ Finalize session metrics by reading the wave data accumulated during execution:
172
172
  > - `effectiveness`: ALWAYS populated from Phase 1 plan verification results, and CONSTRUCTED EXPLICITLY in the METRICS_ENTRY snippet (#773) — never deferred to a "remember to add" optional step (that omission is how `carryover: 0` slipped past 41 records). `completion_rate` = `completed / planned_issues` (0.0-1.0, where 0.0 means nothing was completed). **`carryover` counting rule (#773):** `carryover` is the **length of the Phase 1.65 gate carry-list** — `autoCarry` ∪ the middle-band `ask` items the operator LEFT SELECTED ∪ the answered-question `impliesWork: true` candidates — NOT the raw Phase 1.2+1.3 candidate count. On the fail-open skip (gate disabled / headless / AUQ unavailable), EVERY candidate carries, so `carryover` = the full candidate-list length. Count the gate's OUTPUT (what reaches Phase 5 Step 3 filing), not its INPUT.
173
173
  > - `effectiveness.override_ratio` (#730/H5): OPTIONAL nested field = `overridden_findings / max(total_findings_surfaced, 1)` (float 0.0-1.0). Populate ONLY when Phase 2.6 (Broken-Window Budget) ran this session (`broken-window-budget.enabled: true`). OMIT (do NOT write null/0) otherwise — **absent = "not measured"**, `0.0` = "measured, nothing overridden". `overridden_findings` = the summed `count` of the `orchestrator.finding.overridden` events emitted this session; `total_findings_surfaced` = every MED/LOW+ finding surfaced across Phase 1.8 + wave reviewers.
174
174
  > - `waves[].agent_count_planned` / `waves[].agent_count_started` / `waves[].agent_count_completed` (#724/#1115): OPTIONAL per-wave fields, sourced from `wave-loop.md` § Capture wave metrics step 7 — mirror its definitions exactly, do not re-derive them here. `agent_count_planned` = agents named in the session plan for this wave. `agent_count_started` = distinct agents whose `agent-<id>.meta.json` sidecar is present, after any silent-drop re-dispatch — NOT "produced a tool-result" (under background dispatch the launch ack is a result and would count an agent that never ran). `agent_count_completed` = distinct agents whose task-notification (`<status>completed</status>`) arrived. Omit each field when the wave did not measure it — **absent = "not measured"**, never zero-fill; `0` would read as "measured, no agent started", which is the opposite of an unmeasured wave. The two gaps carry the diagnosis: `agent_count_planned > agent_count_started` after re-dispatch is a persistent silent drop, `agent_count_started > agent_count_completed` at wave end is an agent that started and never returned. Both are also logged to STATE.md `## Deviations` by wave-loop.md, so a record and a deviation entry should agree.
175
- > - `waves[].planned_files_count` / `waves[].over_delivery_ratio` (#730/H4): OPTIONAL per-wave fields, populated from STATE.md Wave History headers of the form `(planned <P> files → actual <A>, over-delivery <R>)` (written by wave-executor §3a since #730/H4); omit when absent (pre-#730 sessions / grounding-check: false).
176
175
  > - `waves[].suite_passed` / `waves[].suite_failed` / `waves[].suite_platform` (#944): OPTIONAL per-wave fields. Omit all three when absent — absent = "not measured", `suite_failed: 0` = "measured, zero failures".
177
176
  > **`suite_passed` / `suite_failed`: read the event FIRST, the STATE.md header only as fallback (#966 step 3).** Since #954/#967 the between-waves gate wrapper `scripts/run-quality-gate.mjs` emits `orchestrator.quality_gate.{passed,failed}` with a machine-measured `counts: {passed, failed, total}` AND the `wave_number` it resolved from the `wave-scope.json` sidecar, so per-wave attribution needs no wall-clock window join. Payload fields are flat at the record's top level; for each wave `N` of this session:
178
177
  >
@@ -31,11 +31,12 @@ process.stdout.write(JSON.stringify(tasks));
31
31
  ⚠ Phase 3.2: docs-tasks parse error — <reason>. Manual docs verification required.
32
32
  ```
33
33
 
34
- ## Step 2 — Read SESSION_START_REF
34
+ ## Step 2 — Resolve SESSION_RANGE
35
35
 
36
- Read `session-start-ref` from STATE.md frontmatter. Full accessor and fallback chain are documented in `plan-verification.md § SESSION_START_REF accessor`. Summary:
37
- - Primary: `session-start-ref` field in STATE.md frontmatter.
38
- - Fallback: `git diff --name-only origin/main...HEAD` (no specific base SHA).
36
+ Run the accessor in `plan-verification.md § SESSION_START_REF accessor`; it exports `$SESSION_RANGE`, one complete diff range. Summary:
37
+ - Primary: the pinned `session-start-ref` from STATE.md frontmatter → `<sha>..HEAD`.
38
+ - Field absent: the baseline `resolveBaselineRange()` resolves → `<base>...HEAD` (never a hard-coded `origin/main`).
39
+ - Nothing resolves: `$SESSION_RANGE` is empty and the accessor names `no-baseline-ref` or `unresolvable-ref` — log `⚠ Phase 3.2: no session range (<reason>). Docs verification skipped.` and skip Phase 3.2. Never treat the empty range as "no docs changed".
39
40
 
40
41
  If `git diff` itself fails (network issue, corrupt repo), log:
41
42
  ```
@@ -46,7 +47,7 @@ and skip Phase 3.2. This is an explicit error, not a silent skip.
46
47
  ## Step 3 — Compute changed files
47
48
 
48
49
  ```bash
49
- CHANGED_FILES=$(git diff --name-only "$SESSION_START_REF..HEAD")
50
+ CHANGED_FILES=$(git diff --name-only "$SESSION_RANGE")
50
51
  ```
51
52
 
52
53
  Cache this list for the per-task loop below. If the command exits non-zero, surface the error per the guard above and skip Phase 3.2.
@@ -75,7 +76,7 @@ For each `task` in `docs-tasks`:
75
76
 
76
77
  3. **Matched → inspect diff:**
77
78
  ```bash
78
- git diff "$SESSION_START_REF..HEAD" -- <matched-file>
79
+ git diff "$SESSION_RANGE" -- <matched-file>
79
80
  ```
80
81
  - **Substantive content change** (non-whitespace, non-comment-only lines added/removed): outcome `ok`.
81
82
  - **Whitespace-only or structural-only diff** (no prose or code content changed): outcome `gap`.
@@ -9,19 +9,34 @@
9
9
 
10
10
  > Referenced by Phase 3.2 (Docs Verification) and Phase 1.1a (File-Level Grounding). Canonical definition lives here.
11
11
 
12
- Read `session-start-ref` from STATE.md frontmatter. If the field is missing (older session or persistence disabled), fall back to `git diff --name-only origin/main...HEAD` (compares current HEAD against origin/main rather than a pinned SHA). The fallback is less precise but functional. Always prefer the pinned SHA when available.
12
+ Read `session-start-ref` from STATE.md frontmatter; it is the pinned session-start SHA and always wins. If the field is missing (older session or persistence disabled), RESOLVE the base with `resolveBaselineRange()` (`scripts/lib/vcs-repo-spec.mjs`, #1039 — preferred remote → `<R>/HEAD` → `<R>/{main,master}` → local `main`/`master`), the same chain `computeDrift()` uses. Never assume `origin/main`: in a repo whose remote is not named `origin` or whose default branch is not `main` that literal fails every diff below.
13
+
14
+ The accessor exports two variables. Consumers that DIFF use `"$SESSION_RANGE"` as one complete range — never `"$SESSION_START_REF..HEAD"`, which turned the old fallback into a TWO-dot `origin/main..HEAD` (a diff that also counts everything `main` gained since the branch point). `$SESSION_START_REF` stays the log base (`git log "$SESSION_START_REF..HEAD"`).
13
15
 
14
16
  ```bash
15
- SESSION_START_REF=$(node --input-type=module -e "
17
+ SESSION_SPEC=$(node --input-type=module -e "
16
18
  import {readFileSync} from 'node:fs';
17
19
  import {parseStateMd} from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs';
18
- const fm = parseStateMd(readFileSync('<state-dir>/STATE.md', 'utf8')).frontmatter;
19
- process.stdout.write(fm['session-start-ref'] ?? '');
20
- " 2>/dev/null)
21
- # Fallback when field absent
22
- [ -z "$SESSION_START_REF" ] && SESSION_START_REF="origin/main"
20
+ import {resolveBaselineRange, isQueryFailure} from '${PLUGIN_ROOT}/scripts/lib/vcs-repo-spec.mjs';
21
+ let ref = '';
22
+ try { ref = String(parseStateMd(readFileSync('<state-dir>/STATE.md', 'utf8')).frontmatter['session-start-ref'] ?? '').trim(); } catch {}
23
+ if (/^[0-9a-f]{7,64}$/.test(ref)) process.stdout.write(ref + ' ' + ref + '..HEAD');
24
+ else {
25
+ const r = resolveBaselineRange({ repoRoot: process.cwd() });
26
+ if (r.ok) process.stdout.write(r.base + ' ' + r.range);
27
+ else process.stderr.write('SESSION_RANGE skipped: ' + (isQueryFailure(r.reason) ? 'unresolvable-ref' : 'no-baseline-ref') + ' (' + r.reason + ')\n');
28
+ }
29
+ ")
30
+ SESSION_START_REF=${SESSION_SPEC%% *} # log base: the pinned SHA, else the resolved base ref
31
+ SESSION_RANGE=${SESSION_SPEC#* } # diff range: <sha>..HEAD (pinned), else <base>...HEAD
23
32
  ```
24
33
 
34
+ **Empty `$SESSION_RANGE` is an explicit skip, never "no changes".** The accessor names the reason on stderr, split by `isQueryFailure()`:
35
+ - `no-baseline-ref` — the repo genuinely has no base to measure against (no remote, no `main`/`master`). A real, benign state: report "no session range" and skip each diff-based step below.
36
+ - `unresolvable-ref` — git could not be asked (not a repo, git missing or failing). A degraded measurement: report it as such and skip; never read the empty file list as a clean session.
37
+
38
+ On either skip `$SESSION_START_REF` is empty too, so skip every LOG-based step as well: `git log --oneline "$SESSION_START_REF..HEAD"` then becomes `git log ..HEAD`, which git reads as `HEAD..HEAD` — it prints nothing and exits 0, an empty log indistinguishable from "no commits this session". The pinned value is accepted only as a hex object name (`/^[0-9a-f]{7,64}$/`), so a STATE.md value such as `--output=x` is never handed to git as an option; anything else falls through to the resolved base.
39
+
25
40
  Read back the session plan that was agreed at the start. For EACH planned item:
26
41
 
27
42
  ### 1.1 Done Items
@@ -36,7 +51,7 @@ Read back the session plan that was agreed at the start. For EACH planned item:
36
51
  Compare the files the plan said would be touched against the files actually changed in the session. Catches both **scope creep** (files changed that were not in any agent's prompt scope) and **incomplete coverage** (files in the plan that were never edited).
37
52
 
38
53
  1. **Planned files** = union of all file paths from agent prompt scopes across all waves. Source: STATE.md Wave History, falling back to the original session plan's per-agent "Files:" specs. Glob patterns are expanded against the working tree at session-start time.
39
- 2. **Actual files** = `git diff --name-only $SESSION_START_REF..HEAD`, where `$SESSION_START_REF` comes from the `session-start-ref` field in STATE.md frontmatter. If the field is missing (older session), fall back to `git diff --name-only origin/main...HEAD`.
54
+ 2. **Actual files** = `git diff --name-only "$SESSION_RANGE"` (accessor above: the pinned `session-start-ref`, else the resolved baseline range). Empty `$SESSION_RANGE` → skip 1.1a with the accessor's reason.
40
55
  3. **Compute discrepancies:**
41
56
  - **Touched** = files in both Planned and Actual
42
57
  - **Unplanned (scope creep)** = files in Actual but not in Planned
@@ -46,7 +61,7 @@ Compare the files the plan said would be touched against the files actually chan
46
61
  - Generated/lock files (`pnpm-lock.yaml`, `*.lock`, `dist/**`, `node_modules/**`) are excluded from both planned and actual sets
47
62
  - The `.claude/`, `.codex/`, and `.cursor/` state directories are excluded — they are session artifacts, not code
48
63
 
49
- > **Scope-drift cross-reference:** the S2 warn-only drift tripwire (below) uses its own separately-maintained filter list — `DRIFT_EXCLUDE_PATTERNS` in `scripts/lib/scope-baseline.mjs` — and is NOT derived from the filters above. That list is the shared filter source for both sides of its ratio IN CODE: `writeBaseline()`'s denominator (`countPlannedFiles()`) and `computeDrift()`'s numerator both call the same internal `filterExcluded()` helper (#894 review finding F1 — previously only the numerator was code-filtered; the denominator relied on a coordinator prose instruction to pre-filter before calling `writeBaseline()`, which is why three earlier PRD revisions shipped a tripwire that read a wrong ratio).
64
+ > **Scope-drift cross-reference:** the S2 warn-only drift tripwire (below) uses its own separately-maintained filter list — `DRIFT_EXCLUDE_PATTERNS` in `scripts/lib/scope-baseline.mjs` — and is NOT derived from the filters above. That list is the shared filter source for both sides of its ratio IN CODE: `writeBaseline()`'s denominator (`countPlannedFiles()`) and `computeDrift()`'s numerator both call the same internal `filterExcluded()` helper — the numerator additionally drops UNTRACKED paths under `.orchestrator/` (`isUntrackedRuntimeWrite()`), a rule keyed on tracked status that only the live numerator has (#894 review finding F1 — previously only the numerator was code-filtered; the denominator relied on a coordinator prose instruction to pre-filter before calling `writeBaseline()`, which is why three earlier PRD revisions shipped a tripwire that read a wrong ratio).
50
65
  5. **Report** in the verification output. Also call `computeDrift({ repoRoot, threshold: 2.0 })` (`scripts/lib/scope-baseline.mjs`) and append its result — warn-only, informational, never blocks close:
51
66
  ```
52
67
  File-level grounding:
@@ -56,6 +71,7 @@ Compare the files the plan said would be touched against the files actually chan
56
71
  - Untouched (planned but not edited): N files [list first 5]
57
72
  - Scope drift: filesRatio X.X (Y actual / Z planned, threshold 2.0) — [breached | ok | skipped: <reason>]
58
73
  ```
74
+ Read `skipped: unresolvable-ref` as "a git query the numerator needs failed", not only "the ref is dead": besides a `session-start-ref` lost to a rebase or force-push it also covers a checkout git cannot read, the working-tree or untracked query failing, output over 64 MiB (ENOBUFS) and the 10 s per-query timeout. Run `git rev-parse --verify <session-start-ref>` before concluding the ref is gone. `skipped: no-baseline-ref` means the repo has no base to measure against (no remotes, no `main`/`master`). A `breached` result can also be an over-report: files already dirty at session start, or a same-checkout peer's files, count as drift (see `computeDrift()`'s JSDoc).
59
75
  6. **Append to session metrics** (`grounding` field in the Phase 1.7 JSONL entry):
60
76
  ```json
61
77
  "grounding": {
@@ -26,6 +26,16 @@ rm -f <state-dir>/wave-scope.json
26
26
 
27
27
  This should have been cleaned up by wave-executor after the final wave, but crashed sessions or interrupted executions may leave it behind. A stale scope manifest from a previous session could incorrectly restrict the next session's enforcement hooks.
28
28
 
29
+ Delete THIS session's dispatch ledger the same way (`hooks/pre-task-scope-disjoint.mjs`, one file per session since #1493 — `skills/wave-executor/references/wave-loop-scope-manifest.md` step 7). Never a file named after another session id: that is a peer's live state.
30
+
31
+ ```bash
32
+ SID="${CLAUDE_CODE_SESSION_ID:-}"
33
+ case "$SID" in
34
+ ''|*[!A-Za-z0-9._-]*) ;; # no id on this harness, or not path-safe: leave the file (inert)
35
+ *) rm -f ".orchestrator/wave-dispatch-scopes.$SID.json" ".orchestrator/wave-dispatch-scopes.$SID.lock" ;;
36
+ esac
37
+ ```
38
+
29
39
  ### 3.1 SSOT Files
30
40
  - Update `STATUS.md` / `STATE.md` if they exist (metrics, dates, status)
31
41
  - Update `CLAUDE.md` (or `AGENTS.md` on Codex CLI) if patterns or conventions changed during this session
@@ -35,7 +45,7 @@ This should have been cleaned up by wave-executor after the final wave, but cras
35
45
 
36
46
  > Skip this subsection if `docs-orchestrator.enabled` config is not `true` (default: `false`). Also skip entirely if `docs-orchestrator.mode` is `off`.
37
47
 
38
- Reads `docs-tasks` from STATE.md frontmatter (written by wave-executor Pre-Wave 1b), computes `CHANGED_FILES` via `git diff --name-only "$SESSION_START_REF..HEAD"`, and runs a per-task verification loop (outcome: `ok`/`partial`/`gap`). In `warn` mode logs results non-blocking; in `strict` mode blocks on any gap and presents an AskUserQuestion override prompt. Emits a `### Documentation Coverage (docs-orchestrator)` block for inclusion in the Phase 6 Final Report.
48
+ Reads `docs-tasks` from STATE.md frontmatter (written by wave-executor Pre-Wave 1b), computes `CHANGED_FILES` via `git diff --name-only "$SESSION_RANGE"` (the `plan-verification.md` accessor; an empty range skips with its named reason), and runs a per-task verification loop (outcome: `ok`/`partial`/`gap`). In `warn` mode logs results non-blocking; in `strict` mode blocks on any gap and presents an AskUserQuestion override prompt. Emits a `### Documentation Coverage (docs-orchestrator)` block for inclusion in the Phase 6 Final Report.
39
49
 
40
50
  **See `phase-3-2-docs-verification.md` for full details.**
41
51
 
@@ -169,7 +179,7 @@ Then:
169
179
 
170
180
  Read `skills/session-end/session-metrics-write.md` for JSONL append, vault-mirror invocation, and behavior matrix.
171
181
 
172
- > **Token Rollup (#644):** Before emitting the JSONL record (step 2 of session-metrics-write.md), step 1a calls `rollupSessionTokens({ parentSessionId })` from `scripts/lib/session-token-rollup.mjs` and merges three optional fields onto the in-memory record: `total_token_input`, `total_token_output`, and `subagents_with_tokens` (coverage count). Null totals mean "no token data captured" — not zero cost. The rollup is non-blocking: a missing `subagents.jsonl` or all-null session still writes cleanly with null/0 values.
182
+ > **Token Rollup (#644, #1436):** When the incoming record has no `total_tokens`, `scripts/emit-session.mjs` resolves the session's own raw UUID as described in step 1a of session-metrics-write.md and calls `rollupSessionTokens({ parentSessionId: ownUuid })`, joining `parent_session_id` on that UUID rather than the semantic session ID. It fills only absent fields from the rollup whitelist; diagnostic fields `match_status` and `ledger_records` stay out of the record. Missing or unreadable ledgers, or no records for the own UUID, omit token fields with a WARN stating the reason, never zero cost; null totals are also omitted. The rollup remains non-blocking.
173
183
 
174
184
  ### 3.7a Compute and Write Recommendations (Epic #271 Phase A)
175
185
 
@@ -208,7 +218,7 @@ const boardResult = await mirrorBoard({
208
218
  repoRoot: process.cwd(),
209
219
  explicitStatus: 'closed', // force THIS repo's row to `closed`
210
220
  });
211
- // boardResult.action ∈ { 'written', 'skipped-noop', 'skipped-handwritten', 'skipped-vault-disabled', 'dry-run' }
221
+ // boardResult.action ∈ { 'written', 'skipped-noop', 'skipped-handwritten', 'skipped-vault-disabled', 'skipped-vault-not-canonical', 'dry-run' }
212
222
  ```
213
223
 
214
224
  > **Note (single-repo close path):** with `repos` omitted, `mirrorBoard` builds the repo descriptor itself as `[{ repoRoot, status: explicitStatus }]` — this is the supported single-repo shape, so `explicitStatus: 'closed'` lands on THIS repo's row. (When a caller DOES pass `repos`, each element must be a `{ repoRoot, repoName?, status? }` object, NOT a bare path string — bare strings are silently skipped by `collectRows`.) The board at `<vault-dir>/01-projects/_active-sessions.md` is generator-owned: `mirrorBoard` refuses to touch any file lacking the `session-orchestrator-active-sessions@1` marker, hard-refuses `_overview.md`, and is idempotent (a re-run after the row is already `closed` returns `skipped-noop`). Rows for repos NOT in this update are preserved verbatim by the idempotent merge.
@@ -34,6 +34,8 @@ A worktree is clean iff ALL three conditions hold:
34
34
  > **Authoritative impl:** `scripts/lib/session-end/worktree-cleanup.mjs` — `isWorktreeClean(wtPath, opts)`. Import and call; do NOT re-implement from this doc.
35
35
  >
36
36
  > Algorithm: run `git status --porcelain`; filter blank lines, then discount EXACTLY the one untracked line the promotion marker itself produces (`?? .orchestrator/promoted-from.json` — in a repo where `.orchestrator/` is only partly gitignored, or on a worktree whose branch predates the ignore line, the marker `enterWorktree()` writes would otherwise make every promoted worktree read "dirty"; a modified/staged/renamed/conflicted marker still counts as dirty). If any lines remain → dirty (`false`). Else run `git status --short --branch`; if it matches `/\bahead\b/` → unpushed (`false`). Otherwise `true`. On ANY git error → `false` (conservative PSA-003 default: never auto-remove a worktree we could not verify). Git invocation is via the injection-safe `opts.execFileFn` (default `execFileSync` with an args array — #577 HARDEN-001).
37
+ >
38
+ > `isWorktreeClean()` discounts exactly the marker's own untracked line, never operator work.
37
39
 
38
40
  ### Clean path: auto-remove + WARN (PRD §3 P3 Gherkin row 2)
39
41
 
@@ -57,6 +57,6 @@ Present to the user:
57
57
  - Notes: [any context for next session]
58
58
  ```
59
59
 
60
- > **Test-delta anchor:** the `Δ this session` figures are aggregated from the `test_delta` field of this session's agent reports (added / removed / consolidated test cases); the `tests:src LOC ratio` is `wc -l` over `tests/` divided by `wc -l` over `scripts/` + `hooks/`. A bare `passing/total` count is not a progress signal — a growing suite reads as progress even when the growth is redundant, which is exactly why the delta and the ratio are reported alongside it. When the ratio exceeds the advisory ceiling, recommend that the NEXT session's Quality wave run as a **consolidation wave**: no new test lands without a redundant one being removed in the same change. This is advisory only — it never blocks the close.
60
+ > **Test-delta anchor:** the `Δ this session` figures are aggregated from the `test_delta` field of this session's agent reports (added / removed / consolidated test cases); the `tests:src LOC ratio` is the output of `node scripts/lib/tests-src-ratio.mjs --json` — the single measurement per `.claude/rules/test-value.md` § TV-003; never re-derive it by hand. When the envelope carries `reason: "no-tests-found"` (#1451), report the ratio as not measurable, not as within the corridor. A bare `passing/total` count is not a progress signal — a growing suite reads as progress even when the growth is redundant, which is exactly why the delta and the ratio are reported alongside it. When the ratio exceeds the advisory ceiling, recommend that the NEXT session's Quality wave run as a **consolidation wave**: no new test lands without a redundant one being removed in the same change. This is advisory only — it never blocks the close.
61
61
 
62
62
  > **Documentation Coverage anchor:** If Phase 3.2 ran and produced task verification results (i.e. `docs-orchestrator.enabled: true` and `docs-tasks` were found), the results appear here as a `### Documentation Coverage (docs-orchestrator)` subsection emitted by Phase 3.2 Step 7. The content is written dynamically — it is not pre-populated in this template. When `docs-orchestrator.enabled` is `false` or `docs-tasks` were absent, this subsection is omitted entirely.