session-orchestrator 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/.agents/skills/session-plan/SKILL.md +1 -1
  2. package/.agents/skills/session-start/SKILL.md +1 -1
  3. package/.agents/skills/ux-grill/SKILL.md +22 -0
  4. package/.claude-plugin/marketplace.json +1 -1
  5. package/.claude-plugin/plugin.json +3 -2
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
  8. package/.codex-plugin/skills/session-start/SKILL.md +1 -1
  9. package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
  10. package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
  11. package/.cursor/commands/ux-grill.md +14 -0
  12. package/.cursor/skills/session-plan/SKILL.md +1 -1
  13. package/.cursor/skills/session-start/SKILL.md +1 -1
  14. package/.cursor/skills/ux-grill/SKILL.md +13 -0
  15. package/.cursor-plugin/plugin.json +1 -1
  16. package/AGENTS.md +2 -1
  17. package/CHANGELOG.md +128 -1
  18. package/README.md +98 -86
  19. package/agents/dialectic-deriver.md +11 -0
  20. package/agents/ux-evaluator.md +1 -1
  21. package/commands/close.md +3 -3
  22. package/commands/go.md +2 -0
  23. package/commands/memory-cleanup.md +4 -3
  24. package/commands/persona-panel.md +1 -1
  25. package/commands/session.md +3 -2
  26. package/commands/ux-grill.md +51 -0
  27. package/docs/README.md +4 -4
  28. package/docs/USER-GUIDE.md +117 -50
  29. package/docs/agent-authoring.md +2 -2
  30. package/docs/baseline.md +55 -1
  31. package/docs/ci-setup.md +1 -1
  32. package/docs/codex-setup.md +9 -0
  33. package/docs/components.md +9 -9
  34. package/docs/cursor-setup.md +1 -0
  35. package/docs/events-schema.md +13 -6
  36. package/docs/github-mirror-protection.md +61 -20
  37. package/docs/instruction-delivery.md +1 -1
  38. package/docs/memory-proposal-flow.md +3 -3
  39. package/docs/migration-v4.md +2 -2
  40. package/docs/migration-v5.md +62 -0
  41. package/docs/owner-config-schema.md +74 -90
  42. package/docs/persona-panel.md +4 -4
  43. package/docs/pi-setup.md +1 -0
  44. package/docs/rule-authoring.md +13 -6
  45. package/docs/scope-collision-guard.md +16 -0
  46. package/docs/session-config-reference.md +55 -22
  47. package/docs/session-config-template.md +9 -5
  48. package/docs/vault-docs-architecture.md +4 -2
  49. package/hooks/_lib/hook-import-set.json +70 -3
  50. package/hooks/_lib/lock-bootstrap.mjs +84 -1
  51. package/hooks/_lib/vcs-create-matcher.mjs +401 -16
  52. package/hooks/enforce-scope.mjs +201 -0
  53. package/hooks/hooks-codex.json +1 -1
  54. package/hooks/hooks-cursor.json +5 -0
  55. package/hooks/hooks.json +7 -2
  56. package/hooks/on-session-start.mjs +171 -49
  57. package/hooks/post-bash-issue-budget-refund.mjs +375 -0
  58. package/hooks/pre-auq-clarity.mjs +70 -18
  59. package/hooks/pre-bash-issue-budget.mjs +170 -26
  60. package/hooks/subagent-telemetry.mjs +106 -20
  61. package/package.json +5 -4
  62. package/pi/prompts/ux-grill.md +12 -0
  63. package/scripts/baseline-archetypes.mjs +28 -0
  64. package/scripts/ci/assert-vitest-green.mjs +4 -2
  65. package/scripts/dialectic-deriver.mjs +32 -8
  66. package/scripts/emit-session.mjs +72 -1
  67. package/scripts/lib/agent-status.mjs +441 -9
  68. package/scripts/lib/auq/schema.mjs +10 -3
  69. package/scripts/lib/auto-dialectic.mjs +0 -68
  70. package/scripts/lib/baseline-archetypes.mjs +439 -0
  71. package/scripts/lib/build-live-signals.mjs +5 -6
  72. package/scripts/lib/ci-status-banner.mjs +29 -6
  73. package/scripts/lib/claude-md-budget-lint.mjs +52 -2
  74. package/scripts/lib/config/issue-budget.mjs +68 -8
  75. package/scripts/lib/config/private-config-dir.mjs +3 -2
  76. package/scripts/lib/config/remote-hosts.mjs +2 -2
  77. package/scripts/lib/config-schema.mjs +79 -0
  78. package/scripts/lib/config.mjs +12 -1
  79. package/scripts/lib/eval/engine.mjs +7 -1
  80. package/scripts/lib/file-lock.mjs +151 -8
  81. package/scripts/lib/git-porcelain.mjs +113 -0
  82. package/scripts/lib/instruction-budget-guard.mjs +415 -47
  83. package/scripts/lib/io.mjs +29 -4
  84. package/scripts/lib/issue-budget-reconcile.mjs +392 -0
  85. package/scripts/lib/issue-budget.mjs +412 -9
  86. package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
  87. package/scripts/lib/learnings/sizing-subject.mjs +44 -0
  88. package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
  89. package/scripts/lib/locks/state-md-lock.mjs +19 -41
  90. package/scripts/lib/maintenance-due-banner.mjs +450 -0
  91. package/scripts/lib/owner-config.example.yaml +29 -46
  92. package/scripts/lib/owner-yaml.mjs +14 -13
  93. package/scripts/lib/peer-cards/merger.mjs +143 -0
  94. package/scripts/lib/pre-dispatch-check.mjs +20 -14
  95. package/scripts/lib/project-hygiene.mjs +81 -30
  96. package/scripts/lib/quality-gate.mjs +27 -71
  97. package/scripts/lib/reconcile/engine.mjs +19 -1
  98. package/scripts/lib/reconcile/writer.mjs +278 -11
  99. package/scripts/lib/resource-probe/evaluate.mjs +19 -21
  100. package/scripts/lib/rules-sync.mjs +34 -4
  101. package/scripts/lib/scope-echo.mjs +346 -0
  102. package/scripts/lib/session-close-backfill.mjs +182 -40
  103. package/scripts/lib/session-end/phase-skip.mjs +85 -86
  104. package/scripts/lib/session-end/tail-runner.mjs +178 -0
  105. package/scripts/lib/session-lock.mjs +62 -2
  106. package/scripts/lib/session-record-repair.mjs +91 -0
  107. package/scripts/lib/session-schema/constants.mjs +6 -0
  108. package/scripts/lib/session-schema/filters.mjs +26 -1
  109. package/scripts/lib/session-schema/validator.mjs +20 -0
  110. package/scripts/lib/session-shape.mjs +558 -0
  111. package/scripts/lib/session-start-probes.mjs +429 -56
  112. package/scripts/lib/session-token-rollup.mjs +95 -10
  113. package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
  114. package/scripts/lib/state-md.mjs +1 -0
  115. package/scripts/lib/subagents-schema.mjs +77 -9
  116. package/scripts/lib/telemetry/pricing.mjs +197 -0
  117. package/scripts/lib/telemetry/sync.mjs +50 -1
  118. package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
  119. package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
  120. package/scripts/lib/tmux-layout/layouts.mjs +62 -4
  121. package/scripts/lib/ux-grill/collect.mjs +1163 -0
  122. package/scripts/lib/ux-grill/compare.mjs +285 -0
  123. package/scripts/lib/ux-grill/manifest.mjs +618 -0
  124. package/scripts/lib/ux-grill/measures.mjs +431 -0
  125. package/scripts/lib/ux-grill/paths.mjs +224 -0
  126. package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
  127. package/scripts/lib/ux-grill/reconcile.mjs +344 -0
  128. package/scripts/lib/ux-grill/run-record.mjs +316 -0
  129. package/scripts/lib/ux-grill/schema.mjs +321 -0
  130. package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
  131. package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
  132. package/scripts/lib/validate/check-unwired-features.mjs +56 -27
  133. package/scripts/lib/vault-mirror/process.mjs +2 -1
  134. package/scripts/lib/vault-status/board-lock.mjs +18 -0
  135. package/scripts/lib/vault-status/board-writer.mjs +8 -0
  136. package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
  137. package/scripts/lib/wave-resource-gate.mjs +23 -27
  138. package/scripts/lib/wave-sizing.mjs +10 -3
  139. package/scripts/materialize-wave-scope.mjs +68 -14
  140. package/scripts/mcp-server.sh +16 -1
  141. package/scripts/print-applicable-rules.mjs +7 -6
  142. package/scripts/print-learnings-index.mjs +3 -2
  143. package/scripts/release.mjs +7 -2
  144. package/scripts/session-shape.mjs +266 -0
  145. package/skills/_shared/config-reading.md +15 -9
  146. package/skills/_shared/private-capability-context.md +89 -0
  147. package/skills/bootstrap/SKILL.md +60 -209
  148. package/skills/bootstrap/_shared-template.md +99 -14
  149. package/skills/bootstrap/deep-template.md +36 -26
  150. package/skills/bootstrap/fast-template.md +44 -8
  151. package/skills/bootstrap/intensity-heuristic.md +10 -4
  152. package/skills/bootstrap/private-contract.md +119 -0
  153. package/skills/bootstrap/public-fallback.md +30 -18
  154. package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
  155. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
  156. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
  157. package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
  158. package/skills/bootstrap/standard-template.md +39 -24
  159. package/skills/claude-md-drift-check/SKILL.md +9 -2
  160. package/skills/claude-md-drift-check/checker.mjs +213 -21
  161. package/skills/discovery/SKILL.md +6 -173
  162. package/skills/discovery/probes/vault-staleness.mjs +35 -5
  163. package/skills/discovery/probes-docs.md +8 -4
  164. package/skills/discovery/probes-supply-chain.md +4 -2
  165. package/skills/discovery/probes-ui.md +8 -4
  166. package/skills/discovery/probes-vault.md +12 -4
  167. package/skills/discovery/references/discovery-interactive-triage.md +139 -0
  168. package/skills/discovery/references/discovery-triage-state.md +54 -0
  169. package/skills/docs-orchestrator/audience-mapping.md +1 -1
  170. package/skills/eval/rubric-v1.md +13 -0
  171. package/skills/evolve/SKILL.md +2 -458
  172. package/skills/evolve/references/evolve-analyze-mode.md +360 -0
  173. package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
  174. package/skills/gitlab-ops/SKILL.md +3 -3
  175. package/skills/grill/SKILL.md +1 -1
  176. package/skills/memory-cleanup/SKILL.md +2 -2
  177. package/skills/plan/mode-new.md +9 -0
  178. package/skills/plan/mode-retro.md +4 -3
  179. package/skills/reconcile/SKILL.md +11 -1
  180. package/skills/session-end/SKILL.md +3 -2
  181. package/skills/session-end/drift-operations.md +20 -5
  182. package/skills/session-end/metrics-collection.md +1 -0
  183. package/skills/session-end/phase-3-2-docs-verification.md +1 -1
  184. package/skills/session-end/phase-3-6-tail.md +27 -67
  185. package/skills/session-end/phase-3-7a-recommendations.md +2 -2
  186. package/skills/session-end/references/phase-2-quality-gate.md +3 -3
  187. package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
  188. package/skills/session-end/references/phase-5-issue-cleanup.md +32 -1
  189. package/skills/session-end/session-metrics-write.md +33 -12
  190. package/skills/session-plan/SKILL.md +46 -180
  191. package/skills/session-plan/references/session-plan-task-classification.md +152 -0
  192. package/skills/session-plan/wave-template.md +8 -15
  193. package/skills/session-start/SKILL.md +41 -7
  194. package/skills/session-start/phase-2-5-docs-planning.md +1 -1
  195. package/skills/session-start/phase-8-5-express-path.md +12 -9
  196. package/skills/session-start/references/operations-contract.md +114 -0
  197. package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
  198. package/skills/session-start/references/phase-4-ssot-environment-check.md +42 -24
  199. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
  200. package/skills/session-start/soul.md +2 -2
  201. package/skills/test-runner/SKILL.md +1 -1
  202. package/skills/test-runner/rubric-v1.md +2 -2
  203. package/skills/tmux-layout/SKILL.md +3 -1
  204. package/skills/ux-grill/SKILL.md +211 -0
  205. package/skills/ux-grill/rubric-v2.md +201 -0
  206. package/skills/ux-grill/soul.md +76 -0
  207. package/skills/wave-executor/SKILL.md +32 -127
  208. package/skills/wave-executor/circuit-breaker.md +3 -1
  209. package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
  210. package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
  211. package/skills/wave-executor/references/wave-loop-dispatch.md +12 -2
  212. package/skills/wave-executor/references/wave-loop-review.md +19 -6
  213. package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
  214. package/templates/_shared/ux-manifest.template.md +149 -0
  215. package/templates/nextjs-minimal/package.json +1 -1
  216. package/templates/node-minimal/package.json +1 -1
  217. package/scripts/lib/multi-provider-build/providers.mjs +0 -64
  218. package/scripts/lib/multi-provider-build/templating.mjs +0 -130
  219. package/scripts/lib/owner-config/coerce.mjs +0 -29
  220. package/scripts/lib/owner-config/constants.mjs +0 -21
  221. package/scripts/lib/owner-config/defaults.mjs +0 -50
  222. package/scripts/lib/owner-config/error.mjs +0 -19
  223. package/scripts/lib/owner-config/index.mjs +0 -13
  224. package/scripts/lib/owner-config/merge.mjs +0 -52
  225. package/scripts/lib/owner-config/validate.mjs +0 -259
  226. package/scripts/lib/owner-config-loader.mjs +0 -170
  227. package/scripts/lib/owner-config.mjs +0 -28
  228. package/scripts/lib/soul-resolve.mjs +0 -130
  229. package/scripts/lib/vault-mirror/render.mjs +0 -8
  230. package/templates/_shared/journey-manifest.md +0 -114
@@ -0,0 +1,360 @@
1
+ # Evolve — Phase 3: Analyze Mode
2
+
3
+ > Reference of the evolve skill, split out of `SKILL.md` (#1246). At the split the body was moved byte-identical; it has been edited since (#1321 — real-session filter, population rule, git-derived fragile-file method), so it is no longer verifiable against the pre-split file.
4
+ > **Sibling-file paths inside this body are relative to the parent directory, not to `references/`** — the body carries no relative markdown links, only backticked file mentions.
5
+ > **Read when `/evolve analyze` (the default mode) runs** — `../SKILL.md` Phase 2's Mode Dispatch routes here. Covers pattern extraction (9 built-in analyzer types plus `evolve.extra-sources`), deduplication, relation judgment (#1016), the AskUserQuestion confirmation gate, the archive-safe write pipeline (Step 3.5), and the C2 auto-repair feeder (Step 3.6).
6
+
7
+ ## Phase 3: Analyze Mode (default)
8
+
9
+ Extract learnings from session history.
10
+
11
+ > **Vault Integration:** If `vault-integration.enabled` is `true` in Session Config, confirmed learnings are mirrored to the configured Obsidian vault after the atomic write (Step 3.5, step 9). See `docs/session-config-reference.md` for the `vault-integration` config block.
12
+
13
+ ### Step 3.1: Read Session Data
14
+
15
+ - Read the entries of `.orchestrator/metrics/sessions.jsonl` (or `<state-dir>/metrics/sessions.jsonl` if the v2 path does not exist — see Phase 1.4 fallback)
16
+ - Parse each JSONL line as JSON (skip unparseable lines)
17
+ - Keep only REAL sessions: drop every `status: "abandoned"` record (the #834 close-backfill stubs). The predicate is `isRealSession` / `filterRealSessions` in `scripts/lib/session-schema/filters.mjs:54` / `:65`. Do NOT key on `_backfill_source` — real, repaired records carry it too (#1296).
18
+ - Sort by `completed_at` descending (most recent first)
19
+ - If no real sessions remain, abort: "No session data available. Complete at least one session before running evolve." **Telemetry on abort (#1200, #1206):** before stopping, emit — same minimal `emit-event.mjs` call as Phase 1.2's abort, and for the same reason: this gate fires before the Step 3.5(5) `sweep-expired-learnings.mjs --prune` call exists to fold the emit into:
20
+
21
+ ```bash
22
+ node scripts/emit-event.mjs --type orchestrator.evolve.completed --payload \
23
+ "$(node -e "process.stdout.write(JSON.stringify({aborted: 'no-session-data', reason: 'No session data available. Complete at least one session before running evolve.'.slice(0,300), duration_ms: DURATION_MS}))")"
24
+ ```
25
+
26
+ ### Step 3.1b: Read Extra Sources (#638)
27
+
28
+ When `evolve.extra-sources` is configured in Session Config (default `[]` ⇒ this step is a no-op), `/evolve` consumes OUT-OF-BAND domain measurement sidecars to surface `domain-regression` learnings.
29
+
30
+ **READ-ONLY contract:** `/evolve` NEVER runs the domain measurement. The measurement (e.g. an eval-learn regression harness) runs elsewhere and writes a sidecar JSON; this step only READS that sidecar's output. Never shell out to produce the sidecar from here.
31
+
32
+ For each configured `extra-sources` entry `{path, kind, learning-type}`:
33
+
34
+ 1. **Read the sidecar** at `path` (parser-validated as repo-relative, with absolute paths and `..` escape segments dropped before this step, then resolved against the repo root). If the file is missing or unreadable, **skip with a WARN** (`evolve: extra-source not found: <path>`) — do not abort the whole run.
35
+ 2. **Schema-gate** the sidecar against the `kind`'s expected shape. For `kind: regression-flags` the schema is `{ flags: [ { metric, baseline, recent, delta } ] }`. If the parsed JSON does not match (missing `flags` array, or a flag missing a required field), **skip with a WARN** (`evolve: extra-source <path> failed regression-flags schema gate`) — never guess at a different shape.
36
+ 3. **Emit one `domain-regression` learning candidate per flag that is PERSISTENT** — i.e. the same `metric` regressed across ≥2 consecutive sessions (cross-reference prior sessions' sidecar reads or the existing learnings store for the same `subject`). A one-off flag is noise; only a persistent regression earns a candidate.
37
+ - `type`: `learning-type` from the entry (registered enum value `domain-regression`)
38
+ - `subject`: the flag's `metric`
39
+ - `insight`: a human-readable regression statement (e.g. "metric `<metric>` regressed: baseline <baseline> → recent <recent> (delta <delta>) persisting across ≥2 sessions")
40
+ - `evidence`: `baseline → recent` (the concrete data points from the sidecar)
41
+ - `confidence` / `expires_at`: derived via the existing confidence + decay infrastructure (Step 3.5), exactly as for the built-in learning types. `domain-regression` carries a 60-day TTL (`LEARNING_TTL_DAYS`).
42
+ 4. Candidates flow into the SAME Step 3.4 AskUserQuestion confirmation + Step 3.5 write path as the built-in learning types — there is no separate write path.
43
+
44
+ ### Step 3.2: Pattern Extraction
45
+
46
+ For each of the 9 built-in analyzer learning types, apply these heuristics.
47
+
48
+ **Population rule (every analyzer, #1321):** work only on the real sessions from Step 3.1. Every learning candidate's `evidence` states the population it was drawn from as `n=<records or waves used>`. Below `n=5`, emit `evolve: WARN <type> n=<k> below min 5` instead of a learning candidate.
49
+
50
+ #### 1. fragile-file (type: `fragile-file`)
51
+
52
+ - `waves[].files_changed` is a COUNT (a number), never a list of paths — do not iterate it (#1321). File identity comes from git, the method of `skills/session-end/learning-patterns.md:13`, run per session over its commit range:
53
+ `git log --name-only --format="" <session_start_ref>..<end_ref> | sort | uniq -c | sort -rn`
54
+ with `<end_ref>` = the record's `end_ref` / `session_end_ref`; when neither exists use `HEAD` plus `--until=<completed_at>`. Records without `session_start_ref` fall back to `git log --name-only --format="" --since=<started_at> --until=<completed_at>`.
55
+ - **Prefer the ref range; label the fallback.** Only 26 of 201 real records carry `session_start_ref` (measured 2026-09-12), so most analyses land on the time-window fallback. That window also picks up commits a PARALLEL session made on the same branch in the same hours — it attributes foreign commits to this session. Use the ref range wherever the record has one, and mark every candidate whose evidence came from the fallback as `window: time (unattributed)` in its `evidence`.
56
+ - Within a session: a file changed in 3+ commits of that session's range is fragile. Here the population rule's `n` counts the **commits in that session's range**, not records — one session is always one record, so counting records would WARN on every within-session check
57
+ - Cross-session: if a file appears in 3+ different sessions' ranges, flag it
58
+ - Subject = file path (relative to project root)
59
+
60
+ #### 2. effective-sizing (type: `effective-sizing`)
61
+
62
+ - Compare `total_agents` and `total_waves` across session types
63
+ - Calculate average agents per wave for each session type. Read a wave's agent count defensively — older records use other keys: `agent_count`, else `agents` when it is a number, else the length of `agents` when it is an array (of descriptions), else `agents_dispatched`. A record with `total_waves: 0` contributes no per-wave ratio — skip it rather than divide by zero. Exclude coordinator-direct waves (`coordinator_direct: true`, which dispatched no agents) from the ratio. In particular, a record whose waves are all coordinator-direct `Housekeeping` waves (predicate `isCoordinatorDirectHousekeeping` in `scripts/lib/session-schema/filters.mjs`, the session-end writer shape since #1321) contributes no per-wave ratio, same as `total_waves: 0`. Counting it would log a false 0.0 agents-per-wave observation.
64
+ - **Group by `(session_type, session_profile)` (#1247), never by `session_type` alone.** `ultradeep` is a PROFILE over `session_type: deep` (7 waves vs. deep's 4-5, STATE.md frontmatter key `session-profile` — see `scripts/lib/state-md.mjs`), not a distinct type, so a bare `session_type` grouping folds a 7-wave ultradeep session and a 5-wave deep session into the same row and silently averages their agents-per-wave and over-delivery numbers together.
65
+ - Subject = `sizingSubject({ session_type, session_profile })` from `scripts/lib/learnings/sizing-subject.mjs` — e.g. `deep-session-sizing` (no profile, byte-identical to the pre-#1247 literal), `deep-ultradeep-session-sizing` (profile `ultradeep`), or `feature-session-sizing`. Always derive via this helper; do not hand-concatenate the subject string.
66
+ - Insight = "Deep sessions average X agents across Y waves" or "Ultradeep sessions average X agents across Y waves" (name the profile in the insight whenever `session_profile` is present, so the two rows read as distinct sizing observations, not variants of one sentence)
67
+ - **Over-delivery ratio aggregation (#730/H4, #794.7; keyed by profile since #1247):** compute the MEDIAN of `waves[].over_delivery_ratio` across the last ~5 `sessions.jsonl` records of the same `(session_type, session_profile)` pair, filtered to waves whose `role` is not `Discovery`/`Finalization` and which carry the field (skip records lacking the field — pre-#730; also skip Discovery/Finalization waves, whose planned set is empty by design). This exclusion clause is intentionally identical to `skills/session-plan/SKILL.md` Step 0.5 "Over-delivery sizing" — keep the two wordings in sync on edit. Fold the median into this candidate's `insight`/`evidence` fields — e.g. `evidence`: `"median_over_delivery_ratio: 1.4 (n=12 waves, session_type=deep, session_profile=ultradeep)"` — so `session-plan` Step 0.5 can read the ratio from the `effective-sizing` learning first, falling back to its own direct `sessions.jsonl` scan only when no such learning exists.
68
+
69
+ #### 3. recurring-issue (type: `recurring-issue`)
70
+
71
+ - Look at `agent_summary` — if `failed` or `partial` > 0 across multiple sessions, flag
72
+ - Check wave `quality` fields — repeated failures indicate recurring issues
73
+ - Subject = issue pattern identifier (e.g., "test-failures-in-wave-execution", "lint-regressions")
74
+
75
+ #### 4. scope-guidance (type: `scope-guidance`)
76
+
77
+ - Cross-reference `effectiveness.planned_issues` vs `effectiveness.completion_rate`
78
+ - **Skip sessions that lack the `effectiveness` field** (early sessions may not have it)
79
+ - If completion_rate is consistently 1.0 with N issues, note "N issues per session works well"
80
+ - If completion_rate < 0.7, note "scope was too large"
81
+ - Subject = `optimal-scope-per-session-type`
82
+
83
+ #### 5. deviation-pattern (type: `deviation-pattern`)
84
+
85
+ > **Ownership Reference:** See `skills/_shared/state-ownership.md`. evolve has read-only access to STATE.md.
86
+
87
+ - Read `<state-dir>/STATE.md` if it exists and check `## Deviations` section
88
+ - Cross-reference with session duration vs planned waves
89
+ - Subject = pattern name (e.g., "scope-creep-in-feature-sessions", "underestimated-complexity")
90
+
91
+ #### 6. stagnation-class-frequency (type: `stagnation-class-frequency`)
92
+
93
+ - Read `stagnation_events` from the most recent 5 sessions in `sessions.jsonl` (skip sessions lacking the field — they predate #84).
94
+ - For each `(file, error_class)` pair appearing in ≥2 sessions, extract a candidate:
95
+ - Subject = `<file>:<error_class>` (e.g., `skills/wave-executor/wave-loop.md:edit-format-friction`)
96
+ - Insight = "File <X> has <error_class> stagnation in <N> recent sessions — candidate for pre-edit grounding (#85)."
97
+ - Evidence = "<N> sessions with stagnation_events for this file/class"
98
+ - These learnings feed #85 (pre-edit grounding injection) when it ships — high-frequency pairs trigger grounding.
99
+
100
+ #### 7. hardware-pattern (type: `hardware-pattern`)
101
+
102
+ > **v3.1.0 / Sub-Epic #160 (C2, issue #171).** Keyed on `host_class` rather than project — surfaces hardware-bound problems that affect the user across every repo on the same machine. Complements the project-keyed types above.
103
+
104
+ - Read `.orchestrator/metrics/events.jsonl` (session + wave events) and the registry `sweep.log` at `~/.config/session-orchestrator/sessions/sweep.log`. Both are optional — missing files produce no candidates.
105
+ - Invoke `scripts/lib/hardware-pattern-detector.mjs` → `detectHardwarePatterns({events, sweepLogEntries, thresholds})`. Thresholds come from Session Config `resource-thresholds` when present, falling back to `DEFAULT_THRESHOLDS`.
106
+ - Five detection signals (aggregated per `(signal, host_class)` pair, ≥2 occurrences required):
107
+ - **oom-kill** — `orchestrator.turn.stopped` (or its deprecated alias `orchestrator.session.stopped`, which `hooks/on-stop.mjs` still emits with `deprecated: true` until **2027-03-06**) with `exit_code: 137` or OOM-marker in `error`. Both names are accepted for the deprecation window because every OOM record already on disk carries only the legacy name; the detector's set lives in `OOM_TERMINAL_EVENTS` (`scripts/lib/hardware-pattern-detector.mjs`) and drops the alias on that date.
108
+ - **heartbeat-gap** — registry sweep-log entries with `gap_minutes` above `resource-thresholds.zombie-threshold-min`
109
+ - **concurrent-session-pressure** — session-start events with `peer_count ≥ concurrent-sessions-warn`
110
+ - **disk-full** — events whose `error` matches `ENOSPC` / "no space left"
111
+ - **thermal-throttle** — events whose `resource_snapshot.cpu_load_pct` crosses `cpu-load-max-pct`
112
+ - Each candidate is piped through `candidateToLearning()` → `validateLearning()`. Default `scope` is `private` (in-repo only). To promote to `public`, the user runs `npm run share:hw-learnings -- --promote` (C3 export). This anonymizes each `private` hardware-pattern entry, validates via the privacy contract, and appends a `public` twin to `learnings.jsonl` (original preserved). Use `--dry-run` to preview without writing.
113
+ - Subject convention: `<signal>::<host_class>` (e.g., `oom-kill::macos-arm64-m3pro`). The `::` separator avoids colliding with project-keyed subjects.
114
+ - Confidence starts at 0.5 like other learning types, but decay is slower in practice: hardware stays the same longer than code. This is an emergent property of the existing expire-after-N-days policy applied to a mostly-stable `host_class` — no special-casing needed.
115
+ - **Presentation in step 3.5** (see below): render hardware-patterns in a dedicated section titled `## Hardware Patterns (keyed on host_class)` after the project-keyed patterns. This makes the source of the learning obvious to the user at confirmation time.
116
+
117
+ #### 8. autopilot-effectiveness (type: `autopilot-effectiveness`)
118
+
119
+ > **v3.2 Autopilot / Sub-Epic #271 (issue #298).** Compares manual vs. autopilot session outcomes per mode (housekeeping, feature, deep) so the loop can learn whether walk-away runs preserve quality. Complements the project-keyed and hardware-keyed types above.
120
+
121
+ - Read `.orchestrator/metrics/autopilot.jsonl` (one record per autopilot loop run) **and** `.orchestrator/metrics/sessions.jsonl` (manual + autopilot session outcomes). Both are optional — missing files produce no candidates.
122
+ - Invoke `scripts/lib/evolve/autopilot-effectiveness.mjs` → `analyze(autopilotRuns, sessions)`. The module pairs records by `mode` and compares completion-rate, carryover-rate, kill-switch frequency, and quality-gate pass-rate between the two populations.
123
+ - **Data-gating contract:** the analyzer requires **≥20 paired manual+autopilot runs per mode** before emitting any candidates. Below that threshold the function returns `[]` (empty input contract) — evolve simply skips this type for that mode and reports nothing. This prevents premature conclusions from small samples (#297 calibration depends on the same threshold).
124
+ - Subject convention: `<mode>-manual-vs-autopilot` (e.g., `housekeeping-manual-vs-autopilot`, `feature-manual-vs-autopilot`, `deep-manual-vs-autopilot`). One subject per mode that crosses threshold.
125
+ - Insight = "Autopilot <mode> sessions complete at <X>% vs. manual <Y>% (Δ <Z>pp across N pairs)" or analogous carryover/kill-switch framing when those signals dominate.
126
+ - Confidence starts at 0.5 like other learning types; lifecycle ±0.15 / -0.20 via the existing dedupe-and-update infrastructure in Step 3.3 — no special-casing.
127
+ - Each candidate is piped through `candidateToLearning()` → `validateLearning()` exactly like the other types. Default `scope` is `private` (autopilot RUN data is per-host until the user opts in to share). (refs #298)
128
+
129
+ #### 9. autonomy-verdict (type: `autonomy-verdict`)
130
+
131
+ > **Dispatcher Autonomy / P3.5 (issue #683).** Synthesizes per-repo or per-scope autonomy readiness from autopilot run outcomes plus advisory skill-judge signals. Complements `autopilot-effectiveness`: type 8 asks whether autopilot preserves quality by mode; this type asks whether a repo/scope is ready for more dispatcher autonomy.
132
+
133
+ - Read `.orchestrator/metrics/autopilot.jsonl`, `.orchestrator/metrics/sessions.jsonl`, and `.orchestrator/metrics/skill-judgments.jsonl`. All are optional — missing files produce no candidates.
134
+ - Invoke `scripts/lib/evolve/autonomy-verdict.mjs` → `analyze(autopilotRuns, sessions, skillJudgments, { repo | scope })`. The analyzer reuses the type-8 mode rollups and combines them with counted skill-judge `applied`/`completed` signals.
135
+ - **Data-gating contract:** the analyzer requires **≥1 autopilot run and ≥1 canonical advisory skill-judge judgment** (`schema_version: 1`, `event: "judged"`, `advisory: true`) before emitting a candidate. Below that threshold it returns `[]` so `/evolve analyze` stays quiet during cold-start.
136
+ - Subject convention: `<repo-or-scope>-autonomy-readiness` (e.g., `session-orchestrator-autonomy-readiness`).
137
+ - Insight frames the readiness verdict (`ready`, `watch`, or `not-ready`), the combined score, and the signal counts. Evidence includes the normalized scope, verdict, autopilot summary, and skill-judge summary.
138
+ - Confidence is derived in the analyzer from signal volume, judge confidence, and score separation, then flows through the existing dedupe-and-update infrastructure in Step 3.3. Default `scope` is `private` because autopilot and skill-judge data are host/session-local. (refs #683)
139
+
140
+ ### Step 3.2b: Zero Patterns Check
141
+
142
+ If no patterns were extracted across all built-in analyzers and configured extra sources, report: "No patterns found in session history. This can happen with very few sessions or sessions that lack detailed wave/agent data." and skip to end (do not proceed to AskUserQuestion).
143
+
144
+ ### Step 3.3: Deduplicate Against Existing Learnings
145
+
146
+ For each extracted pattern, check if a learning with same `type` + `subject` already exists in `learnings.jsonl`:
147
+
148
+ - **If exists:** propose confidence update (+0.15 if confirmed by new evidence, -0.2 if contradicted)
149
+ - **If new:** propose as new learning with confidence 0.5
150
+
151
+ This match is **exact string equality on `type` + `subject`** — it is blind to two records that say the same thing in different words, and it cannot detect a contradiction at all. The `-0.2 if contradicted` branch above has therefore had no producer since it was written. Step 3.3b is that producer.
152
+
153
+ ### Step 3.3b: Relation Judgment (#1016)
154
+
155
+ > **Cadence: once per candidate.** Step 3.2b's zero-patterns check and Step 3.4's single AUQ are once-per-run; Step 3.5's write is once-per-run. This step is the only per-candidate one in Phase 3 — the pool build happens once, the judgment runs for each pattern that seeds a pool.
156
+
157
+ > **Runs in `/evolve`, never in a wave.** The pool build is O(N²) over the candidate + corpus union (~13 ms at N=100 records; the viability boundary is ~N=2000). `/evolve` is operator-invoked and off the dispatch hot path — that is the whole reason this lives here and not in `skills/wave-executor/`. Do not invoke it from a wave prompt, an inter-wave checkpoint, or a hook.
158
+
159
+ Skip this step entirely when `.orchestrator/metrics/learnings.jsonl` is absent or holds fewer than 2 entries — with no corpus there is no relation to judge.
160
+
161
+ 1. **Pool.** Call `buildCandidatePools(records, { now })` from `scripts/lib/learnings/candidates.mjs`, passing the union of this run's extracted candidates and the on-disk corpus. It returns `{pools, duplicates, stats}`: `duplicates` are the exact-`learning_key` groups (already certain — no judgment needed), and each `pools[]` entry is `{seed, candidates}` where `candidates[].record` is a bounded, per-seed, non-transitive neighbour set. No clustering, no transitive closure: a neighbour of a neighbour is not a neighbour.
162
+
163
+ 2. **Judge, per candidate that seeds a pool.** `buildJudgmentInput({candidate, neighbours})` then `judgeCandidate(input, { judge })`, both from `scripts/lib/learnings/judgment.mjs`. `buildJudgmentInput` returns `null` for a candidate with no usable `id` — skip that candidate, do not judge it. `judge` is the injected verdict provider: on Claude Code the coordinator reads the `input` envelope and returns the JSON object its `output_contract` field describes. There is no subagent type for this — do not dispatch one (#614: a read-only agent that must write its own sidecar never fires).
164
+
165
+ 3. **Apply, through the one choke point.** `applyVerdict(verdict, effects)` is the only place a judgment may become an effect. In `/evolve` every effect handler is a *proposal recorder*, never a writer: `refine` / `supersede` / `merge` record a proposed change, and `proposeContradiction` records a contradiction pair. `applyVerdict` resolves all four handlers before invoking any of them, so an unwired handler refuses the whole batch rather than applying the decisions that happened to come first.
166
+
167
+ 4. **Fail closed.** `verdict.ok === false` (any of the eight failure modes — unparseable, partial, phantom_id, self_reference, empty, timeout, enum_violation, duplicate_target) means **no relation was read**, not "no relation exists". The candidate keeps its Step 3.3 exact-match verdict and nothing about it is surfaced as a relation. Never fall back to a default decision, never repair-retry a malformed verdict, and never render an unreadable judgment to the operator — surfacing a relation IS the claim, so a voided judgment must not reach the AUQ at all.
168
+
169
+ 5. **Route into the existing gate.** Every surviving decision becomes an OPTION in Step 3.4's AskUserQuestion, never an action:
170
+ - `contradict` → a contradiction pair, presented as its own category beside "duplicate". If the operator selects it, it feeds the `-0.2 if contradicted` branch in Step 3.3 above, applied by Step 3.5(3) — which deliberately does NOT reset `expires_at`.
171
+ - `supersede` / `merge` → an omit-the-loser (or replace-both-with-one) proposal. If selected, the operator's next generation simply omits those ids and Step 3.5(5) archives them — never a hand-delete. The merged record must carry both sources' provenance in its own `evidence`.
172
+ - `refine` → an edit proposal against the existing record's `insight` / `evidence`.
173
+ - `skip` / `abstain` → nothing is surfaced.
174
+
175
+ **The brandmauer holds here, unchanged (#693 FA2/FA3).** The judgment computes; it never writes. Every `.claude/rules/` write and every `learnings.jsonl` write stays behind the operator's Step 3.4 selection and Step 3.5's `--prune` invocation.
176
+
177
+ **Named ceiling (revisit trigger).** A `supersede` or `merge` executed through Step 3.5(5) is tagged `_archive_reason: "superseded"` with a `_superseded_by` tombstone **only when the two records share `type` + non-empty `subject`** — that is `pruneLearnings()`'s own consolidation pass. A cross-wording pair (the exact case this step exists to find) does not share a subject, so its loser is archived `pruned` instead: still in the corpus, still resolvable by id, but the archive record does not name its replacement. Revisit when the CLI grows per-record drop routing, or when an archive audit needs to answer "what replaced this?" for cross-wording merges.
178
+
179
+ ### Step 3.4: Present Findings via AskUserQuestion
180
+
181
+ Present extracted patterns to the user for confirmation. Use AskUserQuestion with `multiSelect: true`:
182
+
183
+ > On Codex CLI where AskUserQuestion is unavailable, present as a numbered Markdown list.
184
+
185
+ ```
186
+ AskUserQuestion({
187
+ questions: [{
188
+ question: "Which of the patterns extracted from this session's history should be saved?",
189
+ header: "Speichern?",
190
+ options: [
191
+ {
192
+ label: "[type] subject",
193
+ description: "insight | evidence: ... | confidence: 0.5 (new) or +0.15 (update)"
194
+ },
195
+ ...
196
+ {
197
+ label: "Skip all",
198
+ description: "Do not save any learnings this time"
199
+ }
200
+ ],
201
+ multiSelect: true
202
+ }]
203
+ })
204
+ ```
205
+
206
+ If user selects "Skip all" or selects nothing, abort gracefully: "No learnings saved."
207
+
208
+ ### Step 3.5: Write Confirmed Learnings
209
+
210
+ For confirmed learnings, use atomic rewrite strategy:
211
+
212
+ 1. Read ALL existing lines from `.orchestrator/metrics/learnings.jsonl` (if exists) into memory. If not found, check `<state-dir>/metrics/learnings.jsonl` as a legacy fallback. If legacy data is found, it will be migrated to the v2 path on write (step 5).
213
+ 2. Apply confidence updates for confirmed existing learnings:
214
+ - Increment confidence by +0.15
215
+ - Cap at 1.0
216
+ - Reset `expires_at` using `deriveExpiresAt(now, type)` unless the candidate supplies a more specific expiry
217
+ 3. Apply confidence decrements for contradicted learnings (-0.2) — do NOT reset `expires_at` for contradicted learnings (let them decay naturally)
218
+ 4. Append new learnings with the **canonical schema_version:1 shape** — every field is required (#303):
219
+ - `schema_version`: **1** (integer, ALWAYS — never omit)
220
+ - `id`: UUID v4 string generated via `node -e "const {randomUUID}=require('crypto');process.stdout.write(randomUUID())"` or `uuidgen | tr '[:upper:]' '[:lower:]'`. MUST be a non-empty UUID string. **Never omit** — missing `id` causes 100% mirror-skip (#303).
221
+ - `type`: one of `fragile-file`, `effective-sizing`, `recurring-issue`, `scope-guidance`, `deviation-pattern`, `stagnation-class-frequency`, `hardware-pattern`, `autopilot-effectiveness`, `autonomy-verdict`, `domain-regression` (#638 — only when sourced from `evolve.extra-sources`, see Step 3.1b)
222
+ - `subject`: the pattern subject
223
+ - `insight`: human-readable description of the pattern. **MUST be `insight`** — do NOT use `description` or `recommendation` (legacy alias keys that vault-mirror cannot read; see #303).
224
+ - `evidence`: specific data points that support the pattern
225
+ - `confidence`: use the candidate's derived confidence when supplied (e.g., `autonomy-verdict`); otherwise 0.5 for new learnings
226
+ - `source_session`: **non-empty kebab-slug string** identifying the session from which the pattern was extracted (e.g. `main-2026-04-27-1942`). MUST be a string — never an object, array, number, or null. If multiple sessions contributed, use the earliest. If unknown, use `"unknown"` (the string). **Never** pass `String(<object>)` — that yields `"[object Object]"` and breaks the YAML mirror downstream (#307). Optional pre-write validation: `jq -e 'select(.source_session | type == "string" and length > 2)'`.
227
+ - `created_at`: current ISO 8601 date
228
+ - `expires_at`: preserve the candidate's derived expiry when supplied; otherwise derive from `LEARNING_TTL_DAYS[type]` via `deriveExpiresAt()` (falling back to the schema default) rather than hard-coding a 30-day horizon
229
+ - `file_paths` (optional): repo-relative path(s) scoping the learning to specific files/directories. Required for a learning to ever become `/reconcile`-eligible (issue #900; see `docs/rule-authoring.md` § "Learning Type-Taxonomy, TTL & Provenance Standard"). For a `fragile-file` candidate, `file_paths: [subject]` is mechanically derivable — `subject` already IS the file path.
230
+ 5. **Write the next generation through the archive-safe pipeline — NEVER a `>` redirect (#1017).**
231
+
232
+ Steps 6–8 (prune, consolidate, rewrite) are **not prose you execute by hand**. They are
233
+ `pruneLearnings()` in `scripts/lib/learnings/expiry-sweep.mjs`, the same module (and the same
234
+ crash-safe ordering, KEEP-batch probe, and `.bak-<ISO>` snapshot) the expiry sweep uses. Until
235
+ #1017, this step said "write entire result back with `>`" — with no archive append at all, which
236
+ deleted 11 of 13 `learning-id` provenance targets referenced by rendered `.claude/rules/*.md`.
237
+ Do not hand-roll a `jq | ... > learnings.jsonl` pass; it bypasses every #721 safety net.
238
+
239
+ Write the full next-generation entry set (existing entries **with** the step-2/3 confidence
240
+ updates, **plus** the step-4 new learnings) as JSONL to a temp sidecar **via the Write tool**
241
+ (not a shell `>` redirect — the destructive-command guard blocks it), then invoke the
242
+ `--prune` subcommand of the sweep CLI. **This call is also `/evolve`'s ONLY
243
+ `orchestrator.evolve.completed` success emit (#1206)** — export `N` (Step 3.5(4)'s
244
+ new-learnings count), `M` (Step 3.5(2)'s reinforced-existing count) and `DURATION_MS`
245
+ (elapsed ms since the Phase 1 marker) as real shell variables before running this line;
246
+ `${N:-0}`-style expansion means an un-exported variable degrades to a safe `0` rather than
247
+ an argument error:
248
+
249
+ ```bash
250
+ NEXT=".orchestrator/metrics/.learnings-next.jsonl" # written by the step above
251
+ node scripts/sweep-expired-learnings.mjs --prune --apply --json --entries "$NEXT" \
252
+ --appended "${N:-0}" --boosted "${M:-0}" --duration-ms "${DURATION_MS:-0}" \
253
+ --repo-root "$(pwd)" && rm -f "$NEXT"
254
+ ```
255
+
256
+ `--file` / `--archive` default to the canonical store + archive paths — pass them only when
257
+ operating on a non-default pair. The command prints ONE JSON line; capture it as `$PRUNE` and
258
+ report its `{scanned, kept, archived, byReason}` in the final summary — `$PRUNE.archived` is
259
+ also the `pruned` counter the emit above just wrote, so there is nothing left to compute for
260
+ the telemetry after this line. Preview first with `--prune --dry-run --json` (same counts,
261
+ zero writes, **no telemetry emit** — dry-run never claims a completed run) whenever the next
262
+ generation was hand-assembled.
263
+
264
+ > **This step is `/evolve`'s only store-write path, and (since #1206) its only
265
+ > `orchestrator.evolve.completed` success emit.** Until #1017 the store write lived here as
266
+ > an inline `node --input-type=module -e` block, and until #1206 the telemetry emit was a
267
+ > SEPARATE `emit-event.mjs` call further down this file — both were a mechanism hiding inside
268
+ > prose: no `--help`, no exit-code contract, no test, and (for the emit) forgettable
269
+ > independently of the write it reported on. Do not re-inline either, and do not hand-roll a
270
+ > `jq | ... > learnings.jsonl` pass — that bypasses every #721 safety net.
271
+
272
+ **Exit codes are the no-op rule.** `0` = applied (or a clean no-op). `1` = input error: the
273
+ sidecar is absent, carries a malformed line, or holds no records — the store and the archive
274
+ were **not touched**;
275
+ re-write the sidecar and re-run. `2` = the prune itself failed inside the lib. On any non-zero
276
+ exit, surface the error and stop — never retry with a shell rewrite, and never delete `$NEXT`
277
+ (the `&&` above already withholds the `rm`, so the assembled generation survives for a retry).
278
+
279
+ `pruneLearnings()` — the function the subcommand calls — performs steps 6 + 7 + 8 mechanically
280
+ and archives **every** record that
281
+ leaves the store, tagged with `_archived_at` + an `_archive_reason` from the closed enum
282
+ `expired | pruned | superseded | merged`:
283
+
284
+ - **6. Prune** — `expires_at` < now → `expired`; `confidence <= 0.0` → `pruned`.
285
+ - **7. Consolidate duplicates (NULL-SUBJECT SAFE)** — same `type` + non-empty `subject`: the
286
+ highest-confidence entry wins; each loser is archived `superseded` with a
287
+ `_superseded_by: <winning id>` tombstone. Entries with null/empty/missing `subject` are NEVER
288
+ collapsed — each is keyed by its unique `id` and always preserved (issue #284).
289
+ - **8. Rewrite** — via `rewriteLearnings()`: full schema validation, a `.bak-<ISO>` snapshot
290
+ (keep-3 rotation), then an atomic tmp+rename. Any id you drop from the temp sidecar without
291
+ an explicit reason is archived `pruned` automatically — the store can no longer lose a record
292
+ silently, whatever the next generation omits.
293
+
294
+ No `graceDays` here, deliberately: `/evolve` re-stamps `expires_at` on every reinforced learning
295
+ in steps 2–3 of THIS run, strictly before the prune, so an entry still expired at prune time is
296
+ one the analyzer just declined to reinforce. (The sweep's 14-day grace exists to protect entries
297
+ from being archived *before* that reinforcement pass runs — a hazard that cannot occur here.)
298
+
299
+ Report the returned `{scanned, kept, archived, byReason}` alongside the counts in the final
300
+ summary line. On a non-zero exit, do NOT retry with a shell rewrite — surface the error. The
301
+ old "read back the first line to confirm valid JSON" check is redundant here: `rewriteLearnings()`
302
+ round-trip-validates EVERY line before any byte reaches disk (#662), and the `malformed` guard
303
+ above rejects an unparseable sidecar before the store is touched at all.
304
+ 6. **Vault mirror (conditional):** Check `$CONFIG."vault-integration".enabled` via jq. If the field is missing or `false`, skip this step entirely — skill behavior is unchanged.
305
+
306
+ If `enabled` is `true`:
307
+
308
+ a. Check `$CONFIG."vault-integration".mode`. If `mode` is `off`, skip the mirror invocation (treat as disabled). If `mode` is absent, default to `warn`.
309
+
310
+ b. Resolve the vault directory: use `$CONFIG."vault-integration"."vault-dir"` if non-null, otherwise fall back to the `$VAULT_DIR` environment variable. If neither is set, emit a warning and skip.
311
+
312
+ c. Invoke the mirror script. Derive a synthetic `EVOLVE_SESSION_ID` so the vault-mirror auto-commit phase (#31) produces a traceable commit subject (`chore(vault): mirror evolve-<date> — N learnings + 0 sessions`). Pass `--vault-name` when `vault-integration.vault-name` is set in Session Config:
313
+ ```bash
314
+ EVOLVE_SESSION_ID="evolve-$(date -u +%Y-%m-%d-%H%M)"
315
+ EVOLVE_VAULT_NAME=$(echo "$CONFIG" | jq -r '."vault-integration"."vault-name" // empty')
316
+ node "$PLUGIN_ROOT/scripts/vault-mirror.mjs" \
317
+ --vault-dir "<vault-dir>" \
318
+ --source .orchestrator/metrics/learnings.jsonl \
319
+ --kind learning \
320
+ --session-id "$EVOLVE_SESSION_ID" \
321
+ ${EVOLVE_VAULT_NAME:+--vault-name "$EVOLVE_VAULT_NAME"}
322
+ ```
323
+
324
+ d. Handle the exit code according to `mode`:
325
+ - `warn` (default): on non-zero exit, surface a warning in evolve output (e.g. "Warning: vault mirror failed — learnings saved locally but not mirrored.") but do NOT fail the skill.
326
+ - `strict`: on non-zero exit, fail the skill immediately and report the error to the user.
327
+
328
+ e. On success (exit 0), report: "Mirrored N learnings to `<vault-dir>/40-learnings/`."
329
+
330
+ Report: "Saved N new learnings, updated M existing. Total active: K."
331
+
332
+ **Telemetry (#1200, #1206):** already emitted by `scripts/sweep-expired-learnings.mjs --prune`
333
+ at Step 3.5(5) above — no separate action here. `appended`/`boosted`/`duration_ms` are whatever
334
+ `$N`/`$M`/`$DURATION_MS` carried into that call, and `pruned` is `$PRUNE.archived` (the sweep
335
+ CLI's own returned total). `promoted` is always `0` from THIS call site: promotion to `public`
336
+ scope is the separate `npm run share:hw-learnings -- --promote` CLI, never invoked by
337
+ `/evolve analyze` itself — see `docs/events-schema.md`.
338
+
339
+ ### Step 3.6: C2 Auto-Repair Feeder (opt-in — #647)
340
+
341
+ > **Default OFF (advisory-only).** With no `skill-evolution:` block in Session Config, this step surfaces repair candidates as ADVICE only — it applies nothing and opens no MR. This mirrors the opt-in precedent of `slopcheck` (#520) and `verification-auto-fix` (#521): the engine is dark unless explicitly enabled.
342
+
343
+ After confirmed learnings are written (Step 3.5), the actionable subset can feed the C2 tiered auto-repair engine (Epic #643 / issue #647). This is a pointer section — the modules own the logic; do not duplicate it here.
344
+
345
+ **`skill-evolution:` is a DISTINCT sibling of the pre-existing `evolve:` block.** `evolve:` (`extra-sources`) tunes learning EXTRACTION (Step 3.1b); `skill-evolution:` tunes repair AUTONOMY. They are parsed by different modules and never share keys — do not conflate them. The `skill-evolution:` block is parsed by `scripts/lib/config/skill-evolution.mjs` (`_parseSkillEvolution`) and surfaced at `$CONFIG['skill-evolution']` (wired in `scripts/lib/config.mjs`). Shape: `{ autonomy: 'off'|'advisory'|'autonomous-gated', 'evidence-floor': number, judge: boolean }`, default `autonomy: 'off'`. Do NOT add `skill-evolution:` as a column-0 key to any consolidated Session Config parity block — it is a standalone top-level block (claude-md-drift-check Check-6 enforces parity only on the `## Session Config` keys).
346
+
347
+ **Candidate intake.** Pass the post-Step-3.5 learnings (and, when available, the `claude-md-drift-check` result) to `extractCandidates({ learnings, driftResult, evidenceFloor: $CONFIG['skill-evolution']['evidence-floor'], now })` from `scripts/lib/skill-evolution/candidate-intake.mjs`. It is a pure transform — only actionable, non-expired learnings whose `confidence ≥ evidence-floor` AND whose insight is prescriptive AND resolves to a repo-relative path become `RepairCandidate`s.
348
+
349
+ **Gate per artifact type.** Each candidate's `target_path` is classified by `classifyTarget(target_path, { repoRoot })` from `scripts/lib/skill-evolution/blast-radius-classifier.mjs` (the heart of the design; path-traversal-safe, fail-closed):
350
+
351
+ | Target type | Gate | Posture |
352
+ |---|---|---|
353
+ | plugin-skill (`skills/…`) | none | **always-mr** — never autonomous |
354
+ | local-skill (`.claude/skills/…`) | none | **always-mr** — never autonomous |
355
+ | local-config (ROOT `CLAUDE.md` / `AGENTS.md` Session Config) | config-validation | **autonomous-gated** |
356
+ | anything else | none | always-mr (fail-closed) |
357
+
358
+ Only ROOT-instruction Session Config edits are eligible for autonomous apply, and only when ALL of: `runConfigValidationGate({ repoRoot })` (`scripts/lib/skill-evolution/config-validation-gate.mjs`) is GREEN (parse-config + config-schema + claude-md-drift-check) **AND** `evidence ≥ evidence-floor` **AND** `autonomy: autonomous-gated`. Skill repairs are MR-only by construction.
359
+
360
+ **Invocation contract (this foundation slice = ADVISORY surfacing).** The single orchestrator that ties intake → classify → gate → route → stamp together is `runRepairEngine({ repoRoot, config, learnings, driftResult, dryRun })` from `scripts/lib/skill-evolution/engine.mjs` — it returns `{ outcomes, summary }` and applies the full gate-per-artifact-type decision matrix internally (`autonomy: off` ⇒ every outcome is advisory-only). In the default/advisory posture, `/evolve` SURFACES candidates and their classification only — it does not apply or open MRs. Apply is gated on the config-validation gate above; MR-opening (`openRepairMr({ candidate, diff, repoRoot, dryRun })` from `scripts/lib/skill-evolution/mr-opener.mjs`) is gated on `autonomy != off`. Candidate de-dup / `processed_at` lifecycle is owned by `scripts/lib/skill-evolution/idempotency.mjs`. When `autonomy: off` (default), report the surfaced candidates as advice and stop.
@@ -0,0 +1,139 @@
1
+ # Evolve — Phase 6: Dialectic Mode
2
+
3
+ > Reference of the evolve skill, split out of `SKILL.md` (#1246). Body moved **byte-identical**; only this header is new.
4
+ > **Sibling-file paths inside this body are relative to the parent directory, not to `references/`** — none needed rewriting: the moved body carries no relative markdown links, only backticked file mentions, which were deliberately left untouched so the bytes stay verifiable against the pre-split file.
5
+ > **Read when `/evolve dialectic` runs** — `../SKILL.md` Phase 2's Mode Dispatch routes here. Covers argument parsing (`--apply`/`--dry-run`/`--model`/`--budget-tokens`), the 4-source data load via `runDialecticDeriver()`, dispatching the `dialectic-deriver` agent, the diff/apply gate, and telemetry error handling.
6
+
7
+ ## Phase 6: Dialectic Mode
8
+
9
+ Single-pass LLM derivation of USER.md + AGENT.md (peer cards from #503) updates from current learnings + sessions + steering files. Dry-run-default per #506 EARS contract.
10
+
11
+ **Telemetry start marker (#1200):** note the current wall-clock time at Phase 6 entry (`DURATION_MS` in the Step 6.4/6.5 emits below is the elapsed milliseconds since this marker) — same placeholder convention as `skills/session-end/SKILL.md`'s `orchestrator.handover.gated` emits.
12
+
13
+ ### Step 6.0: Argument Parsing
14
+
15
+ Parse `$ARGUMENTS` for trailing flags after the `dialectic` keyword:
16
+
17
+ | Flag | Default | Behavior |
18
+ |---|---|---|
19
+ | `--apply` | `false` | Write diff to USER.md/AGENT.md via merger.mjs; without it = dry-run |
20
+ | `--dry-run` | `true` | Explicit dry-run (default); mutually exclusive with --apply |
21
+ | `--model <name>` | from Session Config `dialectic.model` (default `haiku`) | Override LLM |
22
+ | `--budget-tokens <N>` | from Session Config `dialectic.budget-tokens` (default 8000) | Token budget |
23
+
24
+ Mutex check: `--apply` + `--dry-run` together = error "flags mutually exclusive".
25
+
26
+ ### Step 6.1: Pre-checks
27
+ - Bootstrap gate (Phase 0) — already executed
28
+ - Persistence check (Phase 1.2) — already executed
29
+ - Cadence check: if invoked via session-end Phase 3.6.7 auto-trigger, the trigger has already pre-checked cadence. For manual invocation, skip cadence — manual always runs.
30
+
31
+ ### Step 6.2: Data Load
32
+ Read all 4 input sources via `runDialecticDeriver()` from `scripts/dialectic-deriver.mjs` (see W2 I1):
33
+ 1. Top-N learnings from `.orchestrator/metrics/learnings.jsonl` (default 50, sorted by confidence DESC)
34
+ 2. Last-K sessions from `.orchestrator/metrics/sessions.jsonl` (default 10, sorted by completed_at DESC)
35
+ 3. Peer cards via `readPeerCards(repoRoot)` from `scripts/lib/peer-cards/reader.mjs` — returns `{user, agent}` or null
36
+ 4. Project steering files (CLAUDE.md / AGENTS.md Session Config block + narratives)
37
+
38
+ Graceful degradation: any null/empty source is acceptable. If ALL inputs empty → return `{status: 'empty-input'}`.
39
+
40
+ ### Step 6.3: Dispatch the Deriver Agent
41
+
42
+ Construct a `dispatchAgent` function that uses the harness Agent tool to invoke the `dialectic-deriver` agent (see `agents/dialectic-deriver.md`):
43
+
44
+ ```javascript
45
+ const dispatchAgent = async ({ model, prompt, maxTokens }) => {
46
+ // Coordinator uses Agent tool with subagent_type: "session-orchestrator:dialectic-deriver"
47
+ // and the model parameter to invoke the right tier
48
+ const result = await Agent({
49
+ description: "Dialectic-deriver LLM pass",
50
+ subagent_type: "session-orchestrator:dialectic-deriver",
51
+ model,
52
+ prompt,
53
+ });
54
+ return { text: result.text, usage: result.usage ?? { input_tokens: 0, output_tokens: 0 } };
55
+ };
56
+
57
+ > **Why `maxTokens` is not passed to Agent():** the Claude Code harness `Agent()` tool does not currently accept a `max_tokens` parameter. Output-token budget is therefore enforced via prompt text (see line 414 in `skills/session-end/SKILL.md`: "with budget ${budget-tokens} input + 4000 output tokens"). The dispatchAgent contract declares `maxTokens` as the canonical interface; the evolve skill destructures it for forward-compat but routes enforcement through the prompt body. When the harness adds a max_tokens hint, this dispatchAgent becomes the single update point.
58
+
59
+ const result = await runDialecticDeriver({
60
+ dispatchAgent,
61
+ repoRoot: process.cwd(),
62
+ model: argv.model ?? config.dialectic?.model ?? 'haiku',
63
+ budget: { input: argv['budget-tokens'] ?? config.dialectic?.['budget-tokens'] ?? 8000, output: 4000 },
64
+ dryRun: !argv.apply,
65
+ allowEmptying: argv['allow-emptying'] ?? false,
66
+ });
67
+ ```
68
+
69
+ ### Step 6.4: Diff Output & Apply Gate
70
+ - If dry-run (default): present diff inline; write to `.orchestrator/dialectic-pending.md` (atomic tmp+rename); EXIT. Suggestion: "Re-run with `/evolve --dialectic --apply` to apply." <!-- path-check: example -->
71
+ - If `--apply`: call **`mergeDerivedBody(existingBody, result.diff[target])`** from `scripts/lib/peer-cards/merger.mjs` for each card target, then `writePeerCard(repoRoot, 'user', mergedUserCard)` and `writePeerCard(repoRoot, 'agent', mergedAgentCard)` from `scripts/lib/peer-cards/writer.mjs`. Update the `updated:` frontmatter.
72
+
73
+ **Why `mergeDerivedBody` and not `mergePeerCard` directly (#1310):** the deriver emits a FULL BODY STRING per target (`agents/dialectic-deriver.md` § Output format); `mergePeerCard` consumes a SECTION MAP keyed by sentinel name. `mergeDerivedBody` is the adapter between the two — it splits the proposed body at `## ` headings and maps each heading to a sentinel section. `mergePeerCard` stays available as the section-map primitive. Handling per heading class, all of it in `mergeDerivedBody`'s return value:
74
+
75
+ | Heading in the proposed body | Section name | Merge effect | Surfaced as |
76
+ |---|---|---|---|
77
+ | Matches an existing managed section's own `## ` heading | that section's EXISTING name (read from the card, NOT re-slugified) | REPLACE | `mapping[].origin === 'existing'` |
78
+ | No existing section | slugified heading (`[a-z0-9-]+`, collisions suffixed `-2`) | APPEND | `mapping[].origin === 'new'` |
79
+ | Existing managed section the proposal omits | — | KEPT (no auto-delete, per `mergePeerCard` semantics) | — |
80
+ | Text before the first `## ` heading | — | NOT applied | `preamble` + a `{ type: 'unmapped-preamble' }` entry in `conflicts[]` |
81
+
82
+ Existing names are read back out of the card rather than re-derived because the live names are not a pure function of their headings — measured 2026-09-11 in `.orchestrator/peers/AGENT.md`: `## Guard and protocol-migration discipline` → `guard-and-protocol-migration`. Re-slugifying would APPEND a duplicate section instead of replacing one.
83
+
84
+ **Present `conflicts[]` before writing.** A non-empty `conflicts[]` (`duplicate-section`, `orphan-begin`, `unmapped-preamble`) is operator-visible content that the merge did not place — report it beside the delta line rather than writing silently.
85
+ - Report: `Dialectic-derived: M deltas to USER.md, N deltas to AGENT.md. Dry-run | Applied. Tokens: in=<X> out=<Y>.`
86
+
87
+ **Telemetry (#1200, #1206) — emitted by `scripts/dialectic-deriver.mjs`, not skill prose.**
88
+ The dry-run branch needs no action here: `runDialecticDeriver()` already emitted the success
89
+ form (`mode: 'dry-run'`) internally at Step 6.2, using `countManagedSections(diff)` on the SAME
90
+ diff this step presents — in dry-run the diff IS the final artefact, so the event and the
91
+ artefact are computed from the same value. Dry-run counting rule (#1319): `<!-- BEGIN MANAGED -->`
92
+ sentinels if present, otherwise the `## ` headings outside code fences, otherwise 1 for any
93
+ non-empty body (0 for an empty one). The two modes therefore count different things on the
94
+ same `user_deltas` / `agent_deltas` fields: dry-run counts sections of the PROPOSED body,
95
+ apply counts sections the merge actually `replaced + appended`. The **apply** branch is the one case that pipeline
96
+ cannot record on its own: the merge above happens here, one layer up, so call
97
+ `recordDialecticRun()` (the sibling export beside `emitEvolveCompleted` in
98
+ `scripts/lib/learnings/evolve-telemetry.mjs`) immediately after the `writePeerCard()` calls,
99
+ using each target's `mergePeerCard()` `stats` for the deltas:
100
+
101
+ ```javascript
102
+ await recordDialecticRun({
103
+ repoRoot,
104
+ status: 'ok',
105
+ mode: 'apply',
106
+ userDeltas: userMergeStats.replaced + userMergeStats.appended,
107
+ agentDeltas: agentMergeStats.replaced + agentMergeStats.appended,
108
+ tokensIn: result.usage?.input_tokens,
109
+ tokensOut: result.usage?.output_tokens,
110
+ durationMs: DURATION_MS,
111
+ });
112
+ ```
113
+
114
+ ### Step 6.5: Error Handling
115
+ - `status: 'unknown-model'` → fail with clear error (already thrown by validateModel)
116
+ - `status: 'budget-exceeded'` → emit `{status:'budget-exceeded', used:N, budget:M}`, do NOT truncate
117
+ - `status: 'would-empty-card'` → warn + require `--allow-emptying` flag
118
+ - `status: 'empty-input'` → exit clean with message "dialectic: skipped (no input)"
119
+ - subagent crash → log ⚠, exit cleanly (do NOT write to `.orchestrator/dialectic-pending.md`) <!-- path-check: example -->
120
+
121
+ **Telemetry (#1200, #1206) — emitted by `scripts/dialectic-deriver.mjs` for THREE of the five
122
+ outcomes.** `budget-exceeded`, `would-empty-card`, and `empty-input` are `runDialecticDeriver()`
123
+ RETURN values, so the module records them itself, mechanically, at the exact return point —
124
+ nothing to do here for those three. The remaining two are THROWN, not returned, and can only be
125
+ caught one layer up:
126
+
127
+ - `unknown-model` — `validateModel()` throws synchronously before `runDialecticDeriver()` can
128
+ record anything about the call.
129
+ - `subagent-crash` — a `dispatchAgent`/`Agent()` failure propagates out of
130
+ `runDialecticDeriver()` uncaught (it has no status of its own for this case).
131
+
132
+ Catch both here and call the SAME `recordDialecticRun()` used in Step 6.4's apply branch,
133
+ passing the literal slug as `status` (the abort form: `{aborted: status, duration_ms}`):
134
+
135
+ ```javascript
136
+ await recordDialecticRun({ repoRoot, status: 'unknown-model' /* or 'subagent-crash' */, durationMs: DURATION_MS });
137
+ ```
138
+
139
+ Cross-reference: PRD #506 AC1-AC4 + EARS gates. Vault Integration: dialectic does NOT mirror to vault (#506 scope — peer cards are repo-local by design; vault mirror is for cross-repo sessions/learnings).
@@ -111,7 +111,7 @@ done
111
111
  **Taxonomy convention — `priority` REVERSED to scoped `::` (supersedes #727 for this one axis).**
112
112
 
113
113
  - **`priority::<level>` is canonical.** #727's stated rationale was that "this repo mirrors to GitHub, which has no scoped-label semantics … while a migration would break every existing label reference and issue." Both halves were checked on 2026-07-25 and neither holds:
114
- - **Issues are not mirrored at all.** `aiat-poc-infra/docs/github-mirror-runbook.md:1,5` describes a git **push-mirror** with GitHub as "read-only downstream"; `docs/gitlab-team-org-2026-06-21.md:45` confirms there is no two-way GitLab issue sync. Nothing crosses the boundary that a label rename could break.
114
+ - **Issues are not mirrored at all.** `aiat-poc-infra/docs/github-mirror-runbook.md:1,5` describes a git **push-mirror** with GitHub as "read-only downstream"; the external team-organization audit `docs/gitlab-team-org-2026-06-21.md:45` confirms there is no two-way GitLab issue sync. Nothing crosses the boundary that a label rename could break. <!-- path-check: example -->
115
115
  - **GitHub already uses the scoped form.** `gh api "repos/AIAT-AIandBusinessgrowth/aiat-barrierefrei-engine/labels"` returns `priority::high`, `priority::low`, `priority::med`, `priority::medium` across 77 open issues, and **zero** `priority:high`. Same pattern on `aiat-doc-vlm`. GitHub treats `::` as an ordinary string; it merely does not enforce mutual exclusion.
116
116
  - Volume agrees independently: **416 `priority::` against 249 `priority:` and 7 bare** at the time of the decision. Chasing the minority spelling would mean re-labelling the majority.
117
117
  Producers were migrated FIRST (this change); the label-data migration follows separately, because migrating data before producers means the divergence returns within a day.
@@ -340,8 +340,8 @@ Bash calls when the current session contains no prior `Read` on a matching templ
340
340
  **When this matters:** before you or a subagent opens an MR, PR, or issue via CLI, a
341
341
  matching template must have been read in the current session:
342
342
 
343
- - GitHub: `.github/PULL_REQUEST_TEMPLATE.md` / `.github/ISSUE_TEMPLATE*`
344
- - GitLab: `.gitlab/merge_request_templates/Default.md` / `.gitlab/issue_templates/*`
343
+ - GitHub: `.github/pull_request_template.md` / `.github/ISSUE_TEMPLATE*`
344
+ - GitLab: `.gitlab/merge_request_templates/Default.md` / `.gitlab/issue_templates/*` <!-- path-check: example -->
345
345
 
346
346
  Accepted template paths are configured in `.orchestrator/policy/templates-policy.json`
347
347
  (versioned, operator-editable). Default behaviour:
@@ -33,7 +33,7 @@ Read `soul.md` in this skill directory before anything else. It defines WHO you
33
33
  Establish *what* you are grilling and ground yourself in the *code* before asking the user anything.
34
34
 
35
35
  1. **Resolve the target.** Parse `$ARGUMENTS`:
36
- - A file path (e.g. `docs/prd/2026-06-09-export.md`, `STATE.md`, a spec) → read it in full.
36
+ - A file path (e.g. `docs/prd/2026-06-09-export.md`, `STATE.md`, a spec) → read it in full. <!-- path-check: example -->
37
37
  - A topic/slug or empty → grill the plan or idea already present in the current conversation. If there is no plan in context, ask the user — via AUQ — to state the plan in one or two sentences before continuing.
38
38
  2. **Ground in the codebase.** Read the project's domain language if present (`CONTEXT.md`, `.orchestrator/steering/*.md`, relevant `docs/adr/*`), then Grep/Glob the areas the plan touches. Build a short mental model of what the code *actually* does today. This is what lets you run the code-contradiction tactic.
39
39
  3. **State the target back.** In 1–2 plain-text sentences, summarize what you understand the plan to be and what you've grounded it against. This catches a wrong target before you waste a grill on it.
@@ -35,8 +35,8 @@ This skill accepts two optional flags. Default (no flag) runs the interactive 4-
35
35
 
36
36
  | Flag | Behavior |
37
37
  |---|---|
38
- | `--dry-run` | Run Phases 1-3 read-only; instead of mutating MEMORY.md / topic files, write a complete-body MEMORY.md proposal (single fenced block — never a unified-diff) to `.orchestrator/pending-dream.md` (atomic). Exit 0. |
39
- | `--apply-pending` | Read `.orchestrator/pending-dream.md`; refuse if older than 14 days (`stale`) or if MEMORY.md changed since the producing --dry-run (`stale-index`, #788); apply diff; delete pending file; print `auto-dream applied: -<X> lines, +<Y> entries`. Exit 0. |
38
+ | `--dry-run` | Run Phases 1-3 read-only; instead of mutating MEMORY.md / topic files, write a complete-body MEMORY.md proposal (single fenced block — never a unified-diff) to `.orchestrator/pending-dream.md` (atomic). Exit 0. <!-- path-check: example --> |
39
+ | `--apply-pending` | Read `.orchestrator/pending-dream.md`; refuse if older than 14 days (`stale`) or if MEMORY.md changed since the producing --dry-run (`stale-index`, #788); apply diff; delete pending file; print `auto-dream applied: -<X> lines, +<Y> entries`. Exit 0. <!-- path-check: example --> |
40
40
 
41
41
  Flags are mutually exclusive — passing both is an error. Absence of both = legacy interactive mode (Phases 1-4 below).
42
42
 
@@ -40,6 +40,15 @@ Agent({ subagent_type: "Explore", description: "Check ecosystem for conflicts",
40
40
  5. **Core problem being solved** — Open-ended. Claude suggests structure if answer is vague.
41
41
  6. **GitLab group** — Select the GitLab host explicitly, then discover available groups dynamically. Run `ls $BASELINE_PATH/templates/` for project types, and check for a groups config in `$BASELINE_PATH/config/` or run `glab api --hostname "$GITLAB_HOST" "groups?per_page=100&min_access_level=10"` to discover GitLab groups — read each entry's `full_path` field. (`glab` has no `group` subcommand at all — invoking one exits 1 with `Unknown command "group"`.) Present findings via AskUserQuestion.
42
42
 
43
+ ### Optional private capability context — after Wave 1
44
+
45
+ Before Wave 2 research, apply [Private capability context](../_shared/private-capability-context.md)
46
+ only when the owner has explicitly supplied or authorized a local catalog lookup
47
+ for an explicitly private/internal planning audience. Use eligible findings to
48
+ inform the existing shared-patterns research and reuse alternatives; keep the
49
+ archetype research and questions below. With no authorized context, or with a
50
+ public/unknown audience, skip this optional step without a prompt or a lookup.
51
+
43
52
  ### Wave 2 — Technical Details (5 questions, dynamic per archetype)
44
53
 
45
54
  **Pre-wave agents:**
@@ -20,7 +20,7 @@ Gather ALL data before asking questions. No user input. Present a dashboard when
20
20
 
21
21
  ### 1.1 Session Metrics
22
22
 
23
- Read `.orchestrator/metrics/sessions.jsonl`. Extract per entry:
23
+ Read `.orchestrator/metrics/sessions.jsonl` and keep only REAL sessions: drop every `status: "abandoned"` record (the #834 close-backfill stubs; predicate `isRealSession` in `scripts/lib/session-schema/filters.mjs:54` — do NOT key on `_backfill_source`, real records carry it too). Count the dropped stubs separately (#1296). Extract per real entry:
24
24
 
25
25
  - `session_type`, `started_at`, `completed_at`, `duration_seconds`
26
26
  - `effectiveness.completion_rate`, `effectiveness.planned_issues`, `effectiveness.completed`, `effectiveness.carryover`
@@ -28,10 +28,11 @@ Read `.orchestrator/metrics/sessions.jsonl`. Extract per entry:
28
28
  - `waves[].role`, `waves[].agent_count`, `waves[].files_changed`, `waves[].quality`
29
29
 
30
30
  Compute aggregates:
31
- - Total sessions, average duration, session type distribution (deep/feature/housekeeping)
31
+ - Total sessions (real only), average duration, session type distribution (deep/feature/housekeeping)
32
+ - `N abandoned stubs excluded` — its own dashboard line, never folded into Total sessions
32
33
  - Average completion rate, total carryover rate (carryover / planned across all sessions)
33
34
  - Agent success rate (complete / total dispatched), spiral rate (spiral / total)
34
- - Most common wave roles, average files changed per wave
35
+ - Most common wave roles, average files changed per wave — `waves[].files_changed` is not one shape across the ledger: a number (use it), an array (use its length), or null/absent (MISSING — exclude the wave from the average, never count it as 0). Report the n behind the average: `avg files/wave X (n=<waves with a value> of <all waves>)`
35
36
 
36
37
  If the file does not exist or is empty, note "No session metrics available" and continue.
37
38