session-orchestrator 5.3.0 → 5.4.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 (206) hide show
  1. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
  2. package/.agents/skills/navigator/SKILL.md +22 -0
  3. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  4. package/.agents/skills/test-audit/SKILL.md +21 -0
  5. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  6. package/.claude-plugin/marketplace.json +1 -1
  7. package/.claude-plugin/plugin.json +1 -1
  8. package/.codex-plugin/plugin.json +1 -1
  9. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  10. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  11. package/.codex-plugin/skills/navigator/agents/openai.yaml +5 -0
  12. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  13. package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
  14. package/.cursor/commands/navigator.md +15 -0
  15. package/.cursor/commands/test-audit.md +14 -0
  16. package/.cursor/rules/060-evolve.mdc +27 -12
  17. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  18. package/.cursor/skills/navigator/SKILL.md +13 -0
  19. package/.cursor/skills/test-audit/SKILL.md +12 -0
  20. package/.cursor-plugin/plugin.json +1 -1
  21. package/.orchestrator/policy/blocked-commands.json +1 -1
  22. package/AGENTS.md +9 -31
  23. package/CHANGELOG.md +70 -0
  24. package/README.md +14 -10
  25. package/docs/agent-authoring.md +2 -2
  26. package/docs/baseline.md +10 -1
  27. package/docs/ci-setup.md +57 -4
  28. package/docs/components.md +12 -12
  29. package/docs/events-schema.md +11 -8
  30. package/docs/persona-panel.md +5 -4
  31. package/docs/rule-authoring.md +27 -12
  32. package/docs/scope-collision-guard.md +26 -4
  33. package/docs/session-config-reference.md +55 -6
  34. package/docs/session-config-template.md +11 -2
  35. package/hooks/_lib/hook-import-set.json +27 -23
  36. package/hooks/hooks-codex.json +1 -1
  37. package/hooks/hooks-cursor.json +5 -0
  38. package/hooks/hooks-pi.json +5 -0
  39. package/hooks/hooks.json +6 -1
  40. package/hooks/on-session-start.mjs +53 -0
  41. package/hooks/on-stop.mjs +3 -1
  42. package/hooks/post-tool-batch-wave-signal.mjs +3 -1
  43. package/hooks/pre-bash-memory-propose-audit.mjs +52 -3
  44. package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
  45. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  46. package/hooks/pre-task-scope-disjoint.mjs +559 -72
  47. package/hooks/subagent-telemetry.mjs +96 -19
  48. package/package.json +3 -3
  49. package/pi/prompts/navigator.md +12 -0
  50. package/pi/prompts/test-audit.md +12 -0
  51. package/rules/README.md +6 -1
  52. package/rules/_index.md +3 -3
  53. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  54. package/rules/opt-in-domain/prompt-caching.md +5 -1
  55. package/rules/opt-in-stack/backend-data.md +9 -1
  56. package/rules/opt-in-stack/backend.md +7 -1
  57. package/rules/opt-in-stack/frontend.md +6 -1
  58. package/rules/opt-in-stack/security-web.md +7 -1
  59. package/rules/opt-in-stack/swift.md +5 -1
  60. package/scripts/apply-session-learnings.mjs +492 -0
  61. package/scripts/archive-closed-prds.mjs +2 -1
  62. package/scripts/autopilot.mjs +49 -53
  63. package/scripts/backfill-abandoned-sessions.mjs +1 -1
  64. package/scripts/baseline-archetypes.mjs +1 -1
  65. package/scripts/check-package-manager.mjs +62 -5
  66. package/scripts/cursor-install.mjs +1 -4
  67. package/scripts/emit-session.mjs +296 -2
  68. package/scripts/export-hw-learnings.mjs +22 -3
  69. package/scripts/fleet-checkin.mjs +147 -0
  70. package/scripts/lib/agent-frontmatter.mjs +3 -3
  71. package/scripts/lib/auto-dream.mjs +7 -6
  72. package/scripts/lib/autopilot/flags.mjs +6 -1
  73. package/scripts/lib/autopilot/loop.mjs +10 -6
  74. package/scripts/lib/baseline-archetypes.mjs +41 -22
  75. package/scripts/lib/config/host-paths.mjs +79 -0
  76. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  77. package/scripts/lib/config/reaper.mjs +7 -6
  78. package/scripts/lib/config-schema.mjs +13 -0
  79. package/scripts/lib/config.mjs +71 -10
  80. package/scripts/lib/events.mjs +21 -2
  81. package/scripts/lib/fleet-protocol.mjs +270 -0
  82. package/scripts/lib/gates/gate-baseline.mjs +13 -3
  83. package/scripts/lib/gates/gate-full.mjs +7 -2
  84. package/scripts/lib/gates/gate-helpers.mjs +93 -2
  85. package/scripts/lib/gates/gate-incremental.mjs +23 -22
  86. package/scripts/lib/gates/gate-per-file.mjs +16 -7
  87. package/scripts/lib/instruction-budget-guard.mjs +69 -11
  88. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  89. package/scripts/lib/learnings/io.mjs +154 -26
  90. package/scripts/lib/learnings/schema.mjs +163 -15
  91. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  92. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  93. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  94. package/scripts/lib/named-vault-resolver.mjs +104 -5
  95. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  96. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  97. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  98. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  99. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  100. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  101. package/scripts/lib/orphan-reaper.mjs +43 -1315
  102. package/scripts/lib/owner-yaml.mjs +104 -6
  103. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  104. package/scripts/lib/process-group.mjs +62 -11
  105. package/scripts/lib/project-hygiene.mjs +21 -1
  106. package/scripts/lib/quality-gate.mjs +45 -5
  107. package/scripts/lib/resource-probe.mjs +1 -1
  108. package/scripts/lib/rule-loader.mjs +100 -62
  109. package/scripts/lib/rules-sync.mjs +27 -12
  110. package/scripts/lib/scope-baseline.mjs +3 -4
  111. package/scripts/lib/session-record-repair.mjs +183 -26
  112. package/scripts/lib/session-registry.mjs +27 -2
  113. package/scripts/lib/session-schema/constants.mjs +30 -4
  114. package/scripts/lib/session-schema/validator.mjs +28 -1
  115. package/scripts/lib/session-token-rollup.mjs +245 -52
  116. package/scripts/lib/sessions-canonical.mjs +100 -9
  117. package/scripts/lib/sh-quote.mjs +29 -0
  118. package/scripts/lib/subagents-schema.mjs +30 -3
  119. package/scripts/lib/telemetry/pricing.mjs +118 -71
  120. package/scripts/lib/tests-src-ratio.mjs +140 -23
  121. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  122. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  123. package/scripts/lib/ux-grill/reconcile.mjs +1 -1
  124. package/scripts/lib/validate/check-agents.mjs +1 -1
  125. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  126. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  127. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  128. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  129. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  130. package/scripts/lib/validate-vendored-rules.mjs +206 -81
  131. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  132. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  133. package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
  134. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  135. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  136. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  137. package/scripts/lib/wave-resource-gate.mjs +34 -8
  138. package/scripts/memory-propose.mjs +18 -5
  139. package/scripts/release-session-lock.mjs +2 -1
  140. package/scripts/release.mjs +10 -3
  141. package/scripts/repair-invalid-sessions.mjs +37 -4
  142. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  143. package/scripts/run-quality-gate.mjs +323 -302
  144. package/scripts/site-numbers.mjs +163 -5
  145. package/scripts/sweep-expired-learnings.mjs +56 -5
  146. package/scripts/sweep-expired-rules.mjs +2 -1
  147. package/scripts/validate-plugin.mjs +7 -0
  148. package/scripts/vault-integration-watcher.mjs +2 -1
  149. package/scripts/vault-mirror.mjs +72 -46
  150. package/scripts/wave-scope-binding.mjs +2 -1
  151. package/skills/_shared/config-reading.md +3 -3
  152. package/skills/_shared/fleet-protocol.md +131 -0
  153. package/skills/_shared/state-ownership.md +5 -0
  154. package/skills/autopilot/SKILL.md +25 -11
  155. package/skills/bootstrap/SKILL.md +1 -1
  156. package/skills/bootstrap/_shared-template.md +1 -1
  157. package/skills/bootstrap/fast-template.md +1 -1
  158. package/skills/bootstrap/private-contract.md +2 -1
  159. package/skills/bootstrap/public-fallback.md +6 -3
  160. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  161. package/skills/claude-md-drift-check/SKILL.md +10 -10
  162. package/skills/claude-md-drift-check/checker.mjs +26 -18
  163. package/skills/evolve/SKILL.md +2 -2
  164. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  165. package/skills/memory-cleanup/SKILL.md +1 -1
  166. package/skills/navigator/SKILL.md +193 -0
  167. package/skills/navigator/references/account-check.sh +87 -0
  168. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  169. package/skills/navigator/references/slot-check.sh +114 -0
  170. package/skills/persona-panel/SKILL.md +2 -2
  171. package/skills/persona-panel/persona-format.md +2 -2
  172. package/skills/persona-panel/presets/designer-lens.md +1 -1
  173. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  174. package/skills/persona-panel/presets/pm-lens.md +1 -1
  175. package/skills/plan/mode-retro.md +19 -4
  176. package/skills/quality-gates/SKILL.md +6 -4
  177. package/skills/remote-offload/SKILL.md +4 -2
  178. package/skills/session-end/learning-patterns.md +22 -12
  179. package/skills/session-end/metrics-collection.md +5 -6
  180. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  181. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  182. package/skills/session-end/references/session-summary-template.md +1 -1
  183. package/skills/session-end/session-metrics-write.md +22 -60
  184. package/skills/session-plan/SKILL.md +0 -3
  185. package/skills/session-start/SKILL.md +6 -0
  186. package/skills/session-start/references/operations-contract.md +4 -0
  187. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  188. package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -2
  189. package/skills/test-audit/SKILL.md +152 -0
  190. package/skills/test-audit/references/campaign.md +111 -0
  191. package/skills/test-audit/references/mutate.sh +50 -0
  192. package/skills/wave-executor/SKILL.md +1 -1
  193. package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
  194. package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
  195. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  196. package/templates/personas/README.md +5 -5
  197. package/templates/personas/accounting-compliance.v1.md +1 -1
  198. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  199. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  200. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  201. package/templates/personas/buyer-p3-build.v1.md +1 -1
  202. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  203. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  204. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  205. package/templates/personas/klima-ai-expert.v1.md +1 -1
  206. package/templates/personas/klima-physicist.v1.md +1 -1
@@ -3,7 +3,7 @@ name: engineer-lens
3
3
  schema_version: 1
4
4
  version: "1"
5
5
  role: "Engineer lens — evaluates feasibility, complexity, failure modes, and long-term maintainability"
6
- model: claude-opus-4-7
6
+ model: opus
7
7
  tier: domain-expert
8
8
  evaluation_criteria:
9
9
  - "Failure modes are enumerated — what breaks under load, bad input, or partial failure, and how the system responds."
@@ -3,7 +3,7 @@ name: pm-lens
3
3
  schema_version: 1
4
4
  version: "1"
5
5
  role: "Product Manager lens — evaluates user value, outcome-metric clarity, and scope discipline"
6
- model: claude-opus-4-7
6
+ model: opus
7
7
  tier: domain-expert
8
8
  evaluation_criteria:
9
9
  - "The change is tied to a specific, named user or business problem — not a solution in search of one."
@@ -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
 
@@ -16,7 +16,7 @@ Analyze the completed session to extract reusable learnings for future sessions.
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.
86
+ f. **Hand-writing the store is FORBIDDEN** — no shell rewrite, no shell append, no `jq … | tee`. 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.
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
@@ -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
  >
@@ -169,7 +169,7 @@ Then:
169
169
 
170
170
  Read `skills/session-end/session-metrics-write.md` for JSONL append, vault-mirror invocation, and behavior matrix.
171
171
 
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.
172
+ > **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
173
 
174
174
  ### 3.7a Compute and Write Recommendations (Epic #271 Phase A)
175
175
 
@@ -208,7 +208,7 @@ const boardResult = await mirrorBoard({
208
208
  repoRoot: process.cwd(),
209
209
  explicitStatus: 'closed', // force THIS repo's row to `closed`
210
210
  });
211
- // boardResult.action ∈ { 'written', 'skipped-noop', 'skipped-handwritten', 'skipped-vault-disabled', 'dry-run' }
211
+ // boardResult.action ∈ { 'written', 'skipped-noop', 'skipped-handwritten', 'skipped-vault-disabled', 'skipped-vault-not-canonical', 'dry-run' }
212
212
  ```
213
213
 
214
214
  > **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.
@@ -29,68 +29,15 @@
29
29
 
30
30
  > **#701.2 DOC NOTE — `completed_at >= started_at` guard:** This invariant is enforced mechanically by `scripts/emit-session.mjs`. The writer applies `clampTimestampsMonotonic()` (from `scripts/lib/session-schema/timestamps.mjs`) before `validateSession()`, clamping any inversion of `completed_at < started_at` to `started_at` and recording forensics in `_clamped: true` / `_original_completed_at`. Previously-inverted entries (e.g. `main-2026-06-21-session-4`) are already corrected. **No per-session coordinator action is needed** — the writer enforces the invariant at write time. Do not add defensive clamping logic here; the canonical guard lives in `emit-session.mjs`.
31
31
 
32
- 1a. **Token Rollup (#644, extended #1244)** — before emitting the JSONL record, aggregate token usage from `subagents.jsonl` and merge the rollup fields onto the in-memory `$METRICS_ENTRY` JSON object. The join key is the session's UUID (`session_id` / `parent_session_id` on subagents.jsonl — the UUID form, not the semantic slug).
32
+ 1a. **Token Rollup (#644, #1244 — mechanical since #1436)** — no coordinator action. `scripts/emit-session.mjs` (step 2) runs `rollupSessionTokens()` itself whenever the incoming record carries no `total_tokens`: it resolves the session's raw UUID from `.orchestrator/current-session.json` (adopted only when its `semantic_session_id` equals the record's `session_id` and no process-local session id contradicts it), joins the sibling `subagents.jsonl` on `parent_session_id`, and fills `total_tokens`, `total_token_input`, `total_token_output`, the three cache buckets, `total_cost_usd`, `subagents_with_tokens`, `matched_records`, `cost_records_priced`, `cost_records_total` and `_token_schema` for every key the record does not already carry — plus `raw_session_id`. No matching records, an unreadable ledger or a foreign marker omits the fields with a WARN on stderr, never a fabricated `0`. An explicit key on `$METRICS_ENTRY` still wins. The same UUID also backs `session_start_ref` when STATE.md has none (the `head_sha` of the session's own `orchestrator.session.started` event).
33
33
 
34
- **This prose step is the ONLY path by which the hook's token fix reaches `sessions.jsonl`** — `rollupSessionTokens()` has no other production caller, so skipping or partially copying this step leaves the entire #1244 cache-token fix inert at the session level while the per-agent ledger looks correct.
35
-
36
- **Semantics:** `null` totals mean "no token data was captured for this session" — this is NOT the same as zero cost. Do NOT coerce null to 0 when displaying or summing across sessions. The same holds for `total_cost_usd`: `null` means "at least one agent ran on a model the price table does not know", never "$0".
34
+ **Semantics:** `null` totals mean "no token data was captured for this session" — this is NOT the same as zero cost. Do NOT coerce null to 0 when displaying or summing across sessions. The same holds for `total_cost_usd`: it is omitted whenever `cost_records_priced < cost_records_total` (#1475) — at least one cost-relevant subagent record could not be priced (a model the price table does not know, a token-bearing turn naming no model, a transcript that yielded no tokens, e.g. one over the hook's size cap, or an agent with a start record whose stop found no transcript of its own, mostly `workflow-subagent`) — and also when `cost_records_total` is 0, because there was nothing to price. It is never "$0" for an unknown cost and never a partial sum over the v2 agents the ledger can see. Phantom stops (#939, no start record) are no agents and never count. The two counters are persisted beside it, so a missing cost says whether part of the session was unpriceable or nothing was priceable. Blind spots remain: an agent whose start record fell outside the hook's tail window looks like a phantom, an agent with neither start nor stop record is invisible, and a session spanning the 2026-09-09 schema boundary is priced over its v2 records only (its v1 records are neither priced nor counted as candidates — 4 distinct fleet sessions carry such a cost, 2026-10-01). A subagent whose turns span several models carries a per-model `models_usage` breakdown in `subagents.jsonl` (#1470) and is priced part by part. The emit-session warning names the reason via `match_status` (ledger absent, ledger with no readable records, or records present but none for the own raw UUID); none of these means zero cost.
37
35
 
38
36
  **Provenance (#949):** the rollup sums ONLY records carrying `subagent_transcript_found: true` — the flag the producer sets when it read the subagent's own transcript. Pre-#949 records carry the PARENT transcript's running totals and are excluded, so a session made up entirely of them now reports `null` rather than a fabricated sum (73 historical sessions, 96,148,781 phantom tokens, measured 2026-08-11). Two consequences for readers: totals already written into `sessions.jsonl` before 2026-08-11 were produced by the unfiltered recipe and are a series break, not a trend; and `matched_records` counts start records and phantom stops alike, so it is NOT the denominator for a coverage ratio — use `subagents_with_tokens` against the session's real agent count.
39
37
 
40
38
  **Schema boundary (#1244, 2026-09-09):** from `schema_version: 2` a subagent record's `token_input` is BILLABLE PROMPT VOLUME (uncached + cache_read + cache_creation); v1 records held raw uncached input only and are therefore EXCLUDED from every total and reported as `legacy_v1_records`. Sessions spanning the boundary are a second series break — do not trend across it.
41
39
 
42
- Example (coordinator pseudo-code — adapt to your shell/JS context):
43
-
44
- ```js
45
- // Available from scripts/lib/session-token-rollup.mjs
46
- import { rollupSessionTokens } from '../../scripts/lib/session-token-rollup.mjs';
47
-
48
- const rollup = rollupSessionTokens({ parentSessionId: SESSION_UUID });
49
- // rollup: { total_token_input, total_token_output, subagents_with_tokens, matched_records,
50
- // total_token_input_uncached, total_token_cache_read, total_token_cache_creation,
51
- // total_cost_usd, cost_records_priced, cost_records_total, legacy_v1_records,
52
- // _token_schema }
53
- // Merge into the record — every field below is optional in the session schema (additive).
54
- metricsEntry.total_token_input = rollup.total_token_input; // number | null
55
- metricsEntry.total_token_output = rollup.total_token_output; // number | null
56
- metricsEntry.subagents_with_tokens = rollup.subagents_with_tokens; // number (0 when no coverage)
57
- metricsEntry.total_token_input_uncached = rollup.total_token_input_uncached; // number | null
58
- metricsEntry.total_token_cache_read = rollup.total_token_cache_read; // number | null
59
- metricsEntry.total_token_cache_creation = rollup.total_token_cache_creation; // number | null
60
- metricsEntry.total_cost_usd = rollup.total_cost_usd; // number | null (null = unknown model)
61
- metricsEntry._token_schema = rollup._token_schema; // 2
62
- ```
63
-
64
- Report `cost_records_priced / cost_records_total` and `legacy_v1_records` in the session summary when either is non-zero — an unpriced or excluded remainder is what makes a cost figure honest.
65
-
66
- Or, from a bash context, call the rollup via a helper node invocation and `jq`-merge the fields into `$METRICS_ENTRY` before step 2:
67
-
68
- ```bash
69
- ROLLUP_JSON=$(node -e "
70
- import('$(dirname "$0")/../scripts/lib/session-token-rollup.mjs').then(m => {
71
- const r = m.rollupSessionTokens({ parentSessionId: process.env.SESSION_UUID });
72
- process.stdout.write(JSON.stringify(r));
73
- });
74
- " 2>/dev/null) || ROLLUP_JSON='{}'
75
-
76
- # Merge token fields into METRICS_ENTRY (null for fields absent from rollup)
77
- METRICS_ENTRY=$(printf '%s' "$METRICS_ENTRY" | jq \
78
- --argjson r "${ROLLUP_JSON:-{}}" \
79
- '. + {
80
- total_token_input: ($r.total_token_input // null),
81
- total_token_output: ($r.total_token_output // null),
82
- subagents_with_tokens: ($r.subagents_with_tokens // 0),
83
- total_token_input_uncached: ($r.total_token_input_uncached // null),
84
- total_token_cache_read: ($r.total_token_cache_read // null),
85
- total_token_cache_creation: ($r.total_token_cache_creation // null),
86
- total_cost_usd: ($r.total_cost_usd // null),
87
- _token_schema: ($r._token_schema // 2)
88
- }')
89
- ```
90
-
91
- **If the rollup call fails** (e.g., `subagents.jsonl` absent, parse error), set the numeric totals to `null` / `0` and continue — the rollup is non-blocking. A session without token data still writes cleanly.
92
-
93
- 2. Append the prepared JSONL entry (from Phase 1.7, now including token fields from step 1a) via the validating writer `scripts/emit-session.mjs` (issue #249):
40
+ 2. Append the prepared JSONL entry (from Phase 1.7; the writer itself adds the token fields — step 1a) via the validating writer `scripts/emit-session.mjs` (issue #249):
94
41
  ```bash
95
42
  printf '%s' "$METRICS_ENTRY" | node "$PLUGIN_ROOT/scripts/emit-session.mjs" --file .orchestrator/metrics/sessions.jsonl
96
43
  EMIT_EXIT=$?
@@ -135,11 +82,23 @@
135
82
  ```bash
136
83
  VM_ENABLED=$(echo "$CONFIG" | jq -r '."vault-integration".enabled // false')
137
84
  VM_MODE=$(echo "$CONFIG" | jq -r '."vault-integration".mode // "warn"')
138
-
139
- if [[ "$VM_ENABLED" == "true" && "$VM_MODE" != "off" ]]; then
140
- # Resolve vault directory: config field takes precedence, env var as fallback
85
+ VM_HOST_OVERRIDE=$(echo "$CONFIG" | jq -r '."vault-integration"."host-override" // empty')
86
+
87
+ if [[ -n "$VM_HOST_OVERRIDE" && ( "$VM_ENABLED" != "true" || "$VM_MODE" == "off" ) ]]; then
88
+ # SO#1448: this host switched the mirror off on purpose (env
89
+ # SO_VAULT_INTEGRATION or owner.yaml `vault-integration:`). Say so, instead of
90
+ # running into vault-mirror's exit 2 (missing-vault-dir) and a strict block.
91
+ echo "vault-mirror: bewusst aus auf diesem Host ($VM_HOST_OVERRIDE)"
92
+ elif [[ "$VM_ENABLED" == "true" && "$VM_MODE" != "off" ]]; then
93
+ # Resolve vault directory: config field (host-resolved, see vault-dir-source)
94
+ # takes precedence; $VAULT_DIR is only the bash fallback when it is empty.
141
95
  VM_DIR=$(echo "$CONFIG" | jq -r '."vault-integration"."vault-dir" // empty')
142
- : "${VM_DIR:=$VAULT_DIR}"
96
+ VM_DIR_SOURCE=$(echo "$CONFIG" | jq -r '."vault-integration"."vault-dir-source" // "committed"')
97
+ if [[ -z "$VM_DIR" && -n "${VAULT_DIR:-}" ]]; then
98
+ VM_DIR="$VAULT_DIR"
99
+ VM_DIR_SOURCE="env VAULT_DIR"
100
+ fi
101
+ echo "vault-dir=$VM_DIR (Quelle: $VM_DIR_SOURCE)"
143
102
 
144
103
  # Quality-gate thresholds (PRD F1.2). Defaults match
145
104
  # scripts/vault-mirror.mjs (400 chars / 0.5 confidence). The nested key
@@ -242,9 +201,12 @@
242
201
 
243
202
  | `enabled` | `mode` | Result |
244
203
  |-----------|---------|--------|
204
+ | `false` via host override (`host-override` set) | any | Skip; print `vault-mirror: bewusst aus auf diesem Host (<quelle>)` — never blocks |
245
205
  | `false` or missing | any | Skip entirely — no-op, no output |
246
206
  | `true` | `off` | Skip entirely — no-op, no output |
247
207
  | `true` | `warn` | Run mirror; on failure surface a warning but do NOT block close |
248
208
  | `true` | `strict` | Run mirror; on failure block session close with an error message |
249
209
 
210
+ > **Host-local overrides.** `enabled`/`mode` above are the values AFTER the host switch: env `SO_VAULT_INTEGRATION=off|warn|strict` > owner.yaml `vault-integration: { enabled: false | mode: … }` > committed. The host may only lower the committed level, never raise it; `."vault-integration"."host-override"` names the tier that lowered it (`env:SO_VAULT_INTEGRATION`, `owner.yaml`) and is `null` otherwise. `."vault-integration"."vault-dir-source"` (`env`, `match`, `owner`, `committed`) says where the vault path came from. See `docs/session-config-reference.md` § Vault Integration.
211
+
250
212
  > **Hand-written note protection:** `vault-mirror.mjs` checks for a `_generator: session-orchestrator-vault-mirror@1` marker before overwriting any existing file. When it skips an existing hand-written note it emits a JSON line `{"action":"skipped-handwritten","path":"<path>","kind":"<kind>","id":"<id>"}` — the step above surfaces this output so the user can see the result. Action names: `created`, `updated`, `skipped-noop`, `skipped-handwritten`, `skipped-collision-resolved`, `skipped-invalid` (entry failed required-field validation, or the mapper crashed rendering an otherwise-parseable record — the latter case carries `reason: "mapper-crash"`, #718), `skipped-quality-low` (entry failed quality gate — PRD F1.2; line carries a `reason` field).
@@ -140,7 +140,6 @@ Extract these fields for planning:
140
140
  - **Effective sizing**: use historical sizing data to inform Step 3 complexity scoring
141
141
  - **Recurring issues**: pre-populate risk mitigation with known issue patterns
142
142
  - **Scope guidance**: validate planned scope against historical session capacity
143
- - **Over-delivery sizing (#730/H4)**: read the over_delivery_ratio of recent same-session_type waves — from `effective-sizing` learnings if present, else directly from the last ~5 sessions.jsonl records' `waves[].over_delivery_ratio` (skip records lacking the field — pre-#730; also skip Discovery/Finalization waves, whose planned set is empty by design). If the median ratio R > 1.3, the fleet historically under-briefs file scope: inflate the Step 3 "Files to change" estimate by R before scoring the complexity tier; note it under Project Intelligence Applied.
144
143
 
145
144
  For each agreed task/issue:
146
145
  1. Read the VCS issue description and acceptance criteria
@@ -319,8 +318,6 @@ Score the session scope to determine optimal agent counts per wave. Skip only fo
319
318
 
320
319
  > **Cross-module scope** counts top-level source directories (e.g., `src/auth/`, `src/api/`, `lib/utils/`). Nested subdirectories under the same parent count as one directory. Non-source directories (docs, config, scripts) don't count unless they contain modified production code.
321
320
 
322
- > **Over-delivery adjustment (#730/H4):** when Step 0.5 surfaced a historical over-delivery ratio R > 1.3 for this session_type, score the "Files to change" row against ceil(briefed_files × R) rather than the raw briefed count — agents historically deliver R× the briefed scope, so the raw count under-sizes the wave.
323
-
324
321
  ### Complexity Tiers
325
322
 
326
323
  | Tier | Score | Description |
@@ -350,6 +350,12 @@ Invokes `buildLiveSignals` (single SSOT for the signals shape) then `selectMode(
350
350
 
351
351
  **See `phase-7-5-mode-selector.md` for full details.**
352
352
 
353
+ ## Phase 7.6: Fleet Check-in (#1462)
354
+
355
+ > Skip silently when `persistence: false` (no ledger for the event). Runs BEFORE the first writing action; everything not stated here is in `skills/_shared/fleet-protocol.md`.
356
+
357
+ Build the check-in JSON (fields per the reference; `session` from `.orchestrator/current-session.json`, `modus` = session type, `kandidaten` = the issue numbers you intend to claim) and pipe it into `node "${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-$PLUGIN_ROOT}}/scripts/fleet-checkin.mjs"` — it validates, stores `checkin/<session_id>.json` (0600) under `~/.config/navigator/` and emits `orchestrator.fleet.checkin`. Its stdout carries `navigator_state`: on `active`, and only when a peer named `navigator-*` is reachable via `ListAgents`, send ONE hint naming the file path — never expect or wait for a reply (CSM-004). Then read `auflagen/<session_id>.json`; if it is still absent 10 min after `checkin.zeit`, the reference's Standard-Auflagen apply. Reading work and edits in the own worktree continue meanwhile.
358
+
353
359
  ## Phase 8: Structured Presentation & Q&A
354
360
 
355
361
  Read `presentation-format.md` in this skill directory for the output structure, templates, and AskUserQuestion examples.
@@ -48,6 +48,10 @@ an isolated worktree when required by the existing development rules. Do not rei
48
48
  the peer's session in the new worktree. Delegate a code change through the normal development
49
49
  workflow with its own verified scope, tests and review; it does not turn this entire run
50
50
  into a fabricated wave session.
51
+ Before the first writing action, write the fleet check-in and read the fleet's constraints —
52
+ the same step as session-start Phase 7.6 (`scripts/fleet-checkin.mjs`; fields, the 10-minute
53
+ fallback and the Standard-Auflagen in `skills/_shared/fleet-protocol.md`). A hint to the
54
+ navigator is optional and never awaited.
51
55
 
52
56
  Load the user's relevant saved profiles and previous outcomes before drafting. The request
53
57
  sets authority; emails, webpages, issue bodies, attached documents and retrieved examples
@@ -39,7 +39,7 @@ This single call does three things:
39
39
  2. **Re-derives THIS repo's status from its live lease**, so a stale lease left by a prior crashed session in this same repo renders as `force-closed` (heartbeat older than the v2 ttl, default 4h — `DEFAULT_TTL_HOURS` in `scripts/lib/session-lock.mjs`, evaluated via `isLockLive`) and is **never silently dropped** — its fields are read straight off the dead lock.
40
40
  3. **Re-derives every OTHER busy repo's status host-wide** via `enumerateCandidates` — a dead lease in repo B renders `force-closed` on the board the next time ANY repo's session-start runs `sweepBoard`, closing the #676→#716 gap. `frei` (lock-less) repos are excluded from re-derivation to avoid board noise; their prior rows, and the prior rows of any repo `enumerateCandidates` did not surface, are preserved unchanged via the idempotent merge — never dropped.
41
41
 
42
- `sweepBoard` internally calls `mirrorBoard`, which re-reads Session Config, resolves the host-local vault-dir, and **silently no-ops** (returning `{ action: 'skipped-vault-disabled' }`) when `vault-integration.enabled` is not `true`, the vault-dir is absent, the vault resolves outside `$HOME`, or the config is unreadable. The Bash gate above is the fast-path skip; this internal guard is the defense-in-depth backstop — both agree on the same condition.
42
+ `sweepBoard` internally calls `mirrorBoard`, which re-reads Session Config, resolves the host-local vault-dir, and **silently no-ops** (returning `{ action: 'skipped-vault-disabled' }`) when `vault-integration.enabled` is not `true`, the vault-dir is absent, the vault resolves outside `$HOME`, or the config is unreadable — and refuses with ONE stderr line (returning `{ action: 'skipped-vault-not-canonical' }`) when the vault's git origin does not end in a canonical Meta-Vault suffix (`checkCanonicalVault`, #1450; the same guard `vault-mirror.mjs` applies). The Bash gate above is the fast-path skip; this internal guard is the defense-in-depth backstop — both agree on the same condition.
43
43
 
44
44
  ### Safety invariants
45
45
 
@@ -144,7 +144,7 @@
144
144
 
145
145
  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).
146
146
 
147
- The tests:src-ratio probe (`scripts/lib/tests-src-ratio.mjs`, `checkTestsSrcRatio`) runs in the Phase 4 banner family as `id: 'tests-src-ratio'` in `scripts/lib/session-start-probes.mjs` `PROBES`. 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.
147
+ The tests:src-ratio probe (`scripts/lib/tests-src-ratio.mjs`, `checkTestsSrcRatio`) runs in the Phase 4 banner family as `id: 'tests-src-ratio'` in `scripts/lib/session-start-probes.mjs` `PROBES`. It returns `null` (silent no-op) when the ratio is inside the TV-003 corridor, when the ratio is undefined because the repo has no src code, when `repoRoot` is missing or does not exist, or when the measurement throws. It returns a warn in two cases: the ratio exceeds the ceiling, or the ratio is not measurable — 0 test files counted while test-like files are tracked (`reason: 'no-tests-found'`, `ratio: null`, message "tests:src ratio nicht messbar", #1451). The result shape is `{ severity: 'warn', message, ratio: number|null, ceiling, reason? }` — `reason` is set only on the not-measurable warn. Render `result.message` alongside the other banners.
148
148
 
149
149
  **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.
150
150
 
@@ -156,7 +156,7 @@
156
156
  - **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.
157
157
  - **Healthy repo**: silent (no banner).
158
158
 
159
- 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.
159
+ Use `result.mechanical` when proposing session scope: findings with `fixable: true` (aged artifacts, ignored ballast, a missing CI audit step — an audit that cannot be verified because `.gitlab-ci.yml` pulls jobs via `include:` is reported `fixable: false` instead) 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.
160
160
 
161
161
  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.
162
162