session-orchestrator 3.19.0 → 3.21.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 (158) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/030-wave-execution.mdc +10 -8
  5. package/CHANGELOG.md +494 -0
  6. package/README.md +16 -11
  7. package/agents/analyst.md +1 -1
  8. package/agents/architect-reviewer.md +1 -1
  9. package/agents/code-implementer.md +4 -2
  10. package/agents/db-specialist.md +1 -1
  11. package/agents/dialectic-deriver.md +1 -1
  12. package/agents/docs-writer.md +1 -1
  13. package/agents/memory-proposal-collector.md +1 -1
  14. package/agents/qa-strategist.md +1 -1
  15. package/agents/security-reviewer.md +1 -1
  16. package/agents/session-reviewer.md +42 -1
  17. package/agents/skill-applied-judge.md +1 -1
  18. package/agents/test-writer.md +1 -1
  19. package/agents/ui-developer.md +1 -1
  20. package/agents/ux-evaluator.md +1 -1
  21. package/commands/release.md +60 -0
  22. package/commands/session.md +6 -2
  23. package/docs/USER-GUIDE.md +1 -1
  24. package/docs/instruction-delivery.md +350 -0
  25. package/docs/migration-v3.md +9 -6
  26. package/docs/persona-panel.md +3 -1
  27. package/docs/scope-collision-guard.md +167 -0
  28. package/docs/session-config-reference.md +1 -41
  29. package/docs/session-config-template.md +0 -23
  30. package/hooks/_lib/guard-source-loader.mjs +304 -91
  31. package/hooks/enforce-commands.mjs +216 -17
  32. package/hooks/enforce-scope.mjs +236 -12
  33. package/hooks/hooks-codex.json +1 -1
  34. package/hooks/hooks.json +11 -1
  35. package/hooks/on-session-end.mjs +52 -5
  36. package/hooks/on-session-start.mjs +7 -4
  37. package/hooks/on-stop.mjs +127 -12
  38. package/hooks/post-bash-write-verify.mjs +8 -32
  39. package/hooks/pre-bash-destructive-guard.mjs +146 -59
  40. package/hooks/pre-bash-sessions-ledger-guard.mjs +493 -66
  41. package/hooks/pre-task-scope-disjoint.mjs +1042 -0
  42. package/package.json +2 -2
  43. package/pi/prompts/release.md +12 -0
  44. package/scripts/autopilot.mjs +3 -1
  45. package/scripts/backfill-learnings-from-vault.mjs +967 -0
  46. package/scripts/emit-session.mjs +45 -40
  47. package/scripts/export-hw-learnings.mjs +61 -2
  48. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  49. package/scripts/lib/backlog-scan.mjs +106 -15
  50. package/scripts/lib/build-live-signals.mjs +7 -3
  51. package/scripts/lib/ci-status-banner.mjs +207 -23
  52. package/scripts/lib/command-blocker.mjs +322 -62
  53. package/scripts/lib/git-config-drift.mjs +471 -0
  54. package/scripts/lib/hardening.mjs +9 -9
  55. package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
  56. package/scripts/lib/io.mjs +193 -7
  57. package/scripts/lib/learnings/affinity.mjs +434 -0
  58. package/scripts/lib/learnings/candidates.mjs +736 -0
  59. package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
  60. package/scripts/lib/learnings/judgment.mjs +782 -0
  61. package/scripts/lib/learnings/kebab.mjs +128 -0
  62. package/scripts/lib/learnings/select.mjs +704 -0
  63. package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
  64. package/scripts/lib/mirror-issues-banner.mjs +266 -0
  65. package/scripts/lib/named-vault-resolver.mjs +105 -16
  66. package/scripts/lib/peer-cards/schema.mjs +6 -2
  67. package/scripts/lib/reconcile/emitter.mjs +107 -22
  68. package/scripts/lib/reconcile/engine.mjs +9 -15
  69. package/scripts/lib/reconcile/renderer.mjs +141 -25
  70. package/scripts/lib/reconcile/sanitize.mjs +518 -0
  71. package/scripts/lib/reconcile/writer.mjs +134 -1
  72. package/scripts/lib/redact-spans.mjs +89 -0
  73. package/scripts/lib/scope-baseline.mjs +77 -17
  74. package/scripts/lib/scope-gate.mjs +852 -72
  75. package/scripts/lib/secret-masker.mjs +262 -0
  76. package/scripts/lib/session-close-backfill.mjs +2 -2
  77. package/scripts/lib/session-lock.mjs +34 -10
  78. package/scripts/lib/session-record-repair.mjs +551 -0
  79. package/scripts/lib/session-registry.mjs +9 -1
  80. package/scripts/lib/session-schema/serializer.mjs +54 -0
  81. package/scripts/lib/session-schema.mjs +1 -0
  82. package/scripts/lib/session-token-rollup.mjs +68 -6
  83. package/scripts/lib/soul-resolve.mjs +12 -0
  84. package/scripts/lib/state-md/mission-status.mjs +21 -12
  85. package/scripts/lib/tmux-layout/telemetry.mjs +43 -10
  86. package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
  87. package/scripts/lib/validate/check-agents.mjs +77 -5
  88. package/scripts/lib/validate/check-banner-parity.mjs +376 -0
  89. package/scripts/lib/validate/check-commands.mjs +2 -20
  90. package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
  91. package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
  92. package/scripts/lib/validate/check-hooks-symmetry.mjs +18 -0
  93. package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
  94. package/scripts/lib/validate/check-owner-leakage.mjs +188 -20
  95. package/scripts/lib/validate/check-rules.mjs +31 -5
  96. package/scripts/lib/validate/check-skills.mjs +191 -0
  97. package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
  98. package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
  99. package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
  100. package/scripts/lib/validate/check-unwired-features.mjs +757 -0
  101. package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
  102. package/scripts/lib/validate/frontmatter-block.mjs +61 -0
  103. package/scripts/lib/validate/tier-inference.mjs +46 -8
  104. package/scripts/lib/vault-mirror/namespace.mjs +146 -1
  105. package/scripts/lib/vault-mirror/process.mjs +264 -31
  106. package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
  107. package/scripts/lib/vault-status/board-writer.mjs +300 -56
  108. package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
  109. package/scripts/lib/vcs-repo-spec.mjs +500 -19
  110. package/scripts/print-applicable-rules.mjs +170 -7
  111. package/scripts/print-learnings-index.mjs +501 -0
  112. package/scripts/release.mjs +616 -61
  113. package/scripts/repair-invalid-sessions.mjs +209 -0
  114. package/scripts/site-numbers.mjs +1049 -0
  115. package/scripts/sweep-expired-learnings.mjs +192 -32
  116. package/scripts/validate-plugin.mjs +82 -0
  117. package/scripts/validate-wave-scope.mjs +281 -12
  118. package/scripts/vault-mirror.mjs +26 -1
  119. package/skills/_shared/monitor-patterns.md +24 -4
  120. package/skills/_shared/state-ownership.md +17 -0
  121. package/skills/brainstorm/soul.md +47 -1
  122. package/skills/claude-md-drift-check/SKILL.md +9 -1
  123. package/skills/debug/SKILL.md +4 -1
  124. package/skills/discovery/issue-templates.md +4 -4
  125. package/skills/discovery/probes-code.md +2 -2
  126. package/skills/discovery/probes-feature.md +6 -6
  127. package/skills/discovery/probes-infra.md +2 -2
  128. package/skills/discovery/probes-session.md +5 -5
  129. package/skills/dispatcher/SKILL.md +10 -1
  130. package/skills/evolve/SKILL.md +116 -18
  131. package/skills/frontmatter-guard/SKILL.md +9 -1
  132. package/skills/gitlab-ops/SKILL.md +54 -39
  133. package/skills/gitlab-portfolio/SKILL.md +10 -1
  134. package/skills/grill/soul.md +44 -1
  135. package/skills/memory-cleanup/SKILL.md +18 -5
  136. package/skills/npm-publish/SKILL.md +22 -50
  137. package/skills/persona-panel/SKILL.md +3 -1
  138. package/skills/plan/mode-new.md +23 -5
  139. package/skills/plan/soul.md +46 -3
  140. package/skills/repo-audit/SKILL.md +10 -1
  141. package/skills/session-end/SKILL.md +45 -26
  142. package/skills/session-end/metrics-collection.md +1 -1
  143. package/skills/session-end/phase-3-6-tail.md +30 -1
  144. package/skills/session-end/plan-verification.md +1 -5
  145. package/skills/session-end/session-metrics-write.md +6 -10
  146. package/skills/session-plan/SKILL.md +2 -2
  147. package/skills/session-plan/wave-template.md +1 -1
  148. package/skills/session-start/SKILL.md +15 -1
  149. package/skills/session-start/soul.md +41 -1
  150. package/skills/spinout/SKILL.md +5 -1
  151. package/skills/sunset-review/SKILL.md +11 -1
  152. package/skills/tmux-layout/SKILL.md +7 -2
  153. package/skills/vault-mirror/SKILL.md +10 -1
  154. package/skills/vault-sync/SKILL.md +10 -1
  155. package/skills/vault-sync/validator.mjs +55 -6
  156. package/skills/wave-executor/SKILL.md +1 -5
  157. package/skills/wave-executor/wave-loop.md +77 -82
  158. package/scripts/lib/mission-status-schema.mjs +0 -114
@@ -10,10 +10,10 @@
10
10
 
11
11
  ```bash
12
12
  # GitLab: query recent pipeline status
13
- glab pipeline list --per-page 10
13
+ glab pipeline list -R <OWNER>/<REPO> --per-page 10
14
14
 
15
15
  # GitHub: query recent workflow runs
16
- gh run list --limit 10
16
+ gh run list -R <OWNER>/<REPO> --limit 10
17
17
 
18
18
  # Parse output for:
19
19
  # - Repeated failures (same pipeline failing 3+ times in a row)
@@ -55,9 +55,9 @@ Grep pattern: <claimed_addition>
55
55
  git show <commit_hash> -- <relevant_files>
56
56
 
57
57
  # "Closes #N" -> verify acceptance criteria from issue #N are met
58
- gh issue view <N> --json body -q '.body'
58
+ gh issue view -R <OWNER>/<REPO> <N> --json body -q '.body'
59
59
  # or
60
- glab issue view <N>
60
+ glab issue view -R <OWNER>/<REPO> <N>
61
61
 
62
62
  # Step 3: Cross-reference claims against actual changes
63
63
  git diff <commit_hash>~1..<commit_hash>
@@ -83,10 +83,10 @@ Evidence: <what was found or not found>
83
83
 
84
84
  ```bash
85
85
  # GitLab: list open issues sorted by last update
86
- glab issue list --per-page 100 | head -50
86
+ glab issue list -R <OWNER>/<REPO> --per-page 100 | head -50
87
87
 
88
88
  # GitHub: list open issues sorted by last update
89
- gh issue list --limit 100 --json number,title,labels,updatedAt,assignees --jq '.[] | select(.updatedAt < "<30_days_ago_iso>")'
89
+ gh issue list -R <OWNER>/<REPO> --limit 100 --json number,title,labels,updatedAt,assignees --jq '.[] | select(.updatedAt < "<30_days_ago_iso>")'
90
90
 
91
91
  # Flag:
92
92
  # - Issues with no activity in stale-issue-days (default: 30 days)
@@ -131,7 +131,7 @@ for issue in issues:
131
131
  "
132
132
 
133
133
  # GitHub: fetch issue bodies and parse cross-references
134
- gh issue list --limit 100 --json number,body --jq '.[] | {number, body}' | python3 -c "
134
+ gh issue list -R <OWNER>/<REPO> --limit 100 --json number,body --jq '.[] | {number, body}' | python3 -c "
135
135
  import json, sys, re
136
136
  for line in sys.stdin:
137
137
  issue = json.loads(line)
@@ -1,6 +1,15 @@
1
1
  ---
2
2
  name: dispatcher
3
- description: Use when you want the orchestrator to pick the next repo to work on across your whole portfolio — it enumerates candidate repos below the confinement root, resolves free/busy from each repo's session.lock lease, ranks the FREE ones by backlog priority × staleness × readiness, recommends the single most worthwhile one via AskUserQuestion, atomically claims it, and routes you to the chosen entry command. Triggers: "what should I work on next", "dispatch me to a repo", "pick the next project", "run /dispatcher". <example>Context: operator finished a session and wants the next-best repo across the portfolio. user: "/dispatcher" assistant: "Ranked 18 free repos — top recommendation: Pencil-Designs (score 4.50, 90d stale). Confirm via the picker, I'll claim its lease atomically, then route you to /session deep."</example>
3
+ description: >
4
+ Use when you want the orchestrator to pick the next repo to work on across your whole portfolio — it
5
+ enumerates candidate repos below the confinement root, resolves free/busy from each repo's session.lock
6
+ lease, ranks the FREE ones by backlog priority × staleness × readiness, recommends the single most
7
+ worthwhile one via AskUserQuestion, atomically claims it, and routes you to the chosen entry command.
8
+ Triggers: "what should I work on next", "dispatch me to a repo", "pick the next project", "run
9
+ /dispatcher". <example>Context: operator finished a session and wants the next-best repo across the
10
+ portfolio. user: "/dispatcher" assistant: "Ranked 18 free repos — top recommendation: Pencil-Designs
11
+ (score 4.50, 90d stale). Confirm via the picker, I'll claim its lease atomically, then route you to
12
+ /session deep."</example>
4
13
  model: sonnet
5
14
  ---
6
15
 
@@ -211,6 +211,34 @@ For each extracted pattern, check if a learning with same `type` + `subject` alr
211
211
  - **If exists:** propose confidence update (+0.15 if confirmed by new evidence, -0.2 if contradicted)
212
212
  - **If new:** propose as new learning with confidence 0.5
213
213
 
214
+ 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.
215
+
216
+ ### Step 3.3b: Relation Judgment (#1016)
217
+
218
+ > **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.
219
+
220
+ > **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.
221
+
222
+ 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.
223
+
224
+ 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.
225
+
226
+ 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).
227
+
228
+ 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.
229
+
230
+ 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.
231
+
232
+ 5. **Route into the existing gate.** Every surviving decision becomes an OPTION in Step 3.4's AskUserQuestion, never an action:
233
+ - `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`.
234
+ - `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`.
235
+ - `refine` → an edit proposal against the existing record's `insight` / `evidence`.
236
+ - `skip` / `abstain` → nothing is surfaced.
237
+
238
+ **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.
239
+
240
+ **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.
241
+
214
242
  ### Step 3.4: Present Findings via AskUserQuestion
215
243
 
216
244
  Present extracted patterns to the user for confirmation. Use AskUserQuestion with `multiSelect: true`:
@@ -244,7 +272,7 @@ If user selects "Skip all" or selects nothing, abort gracefully: "No learnings s
244
272
 
245
273
  For confirmed learnings, use atomic rewrite strategy:
246
274
 
247
- 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 8).
275
+ 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).
248
276
  2. Apply confidence updates for confirmed existing learnings:
249
277
  - Increment confidence by +0.15
250
278
  - Cap at 1.0
@@ -262,14 +290,69 @@ For confirmed learnings, use atomic rewrite strategy:
262
290
  - `created_at`: current ISO 8601 date
263
291
  - `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
264
292
  - `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.
265
- 5. **Verify write**: Read back the first line of the written file to confirm valid JSON. If read-back fails or is not valid JSON, report error to user.
266
- 6. **Prune:** remove entries where `expires_at` < current date OR `confidence` <= 0.0
267
- 7. **Consolidate duplicates (NULL-SUBJECT SAFE):** if same `type` + `subject` appears more than once
268
- AND `subject` is a non-empty string, keep the entry with highest confidence.
269
- Entries with null/empty/missing `subject` are NEVER collapsed — each is keyed by its unique `id`
270
- and always preserved. (Fix for issue #284: empty-subject dedupe collapse.)
271
- 8. Write entire result back to `.orchestrator/metrics/learnings.jsonl` with `>` (atomic rewrite, NOT append `>>`)
272
- 9. **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.
293
+ 5. **Write the next generation through the archive-safe pipeline — NEVER a `>` redirect (#1017).**
294
+
295
+ Steps 6–8 (prune, consolidate, rewrite) are **not prose you execute by hand**. They are
296
+ `pruneLearnings()` in `scripts/lib/learnings/expiry-sweep.mjs`, the same module (and the same
297
+ crash-safe ordering, KEEP-batch probe, and `.bak-<ISO>` snapshot) the expiry sweep uses. Until
298
+ #1017, this step said "write entire result back with `>`" — with no archive append at all, which
299
+ deleted 11 of 13 `learning-id` provenance targets referenced by rendered `.claude/rules/*.md`.
300
+ Do not hand-roll a `jq | ... > learnings.jsonl` pass; it bypasses every #721 safety net.
301
+
302
+ Write the full next-generation entry set (existing entries **with** the step-2/3 confidence
303
+ updates, **plus** the step-4 new learnings) as JSONL to a temp sidecar **via the Write tool**
304
+ (not a shell `>` redirect — the destructive-command guard blocks it), then invoke the
305
+ `--prune` subcommand of the sweep CLI:
306
+
307
+ ```bash
308
+ NEXT=".orchestrator/metrics/.learnings-next.jsonl" # written by the step above
309
+ node scripts/sweep-expired-learnings.mjs --prune --apply --json --entries "$NEXT" && rm -f "$NEXT"
310
+ ```
311
+
312
+ `--file` / `--archive` default to the canonical store + archive paths — pass them only when
313
+ operating on a non-default pair. The command prints ONE JSON line; capture it as `$PRUNE` and
314
+ report its `{scanned, kept, archived, byReason}` in the final summary. Preview first with
315
+ `--prune --dry-run --json` (same counts, zero writes) whenever the next generation was
316
+ hand-assembled.
317
+
318
+ > **This step is `/evolve`'s only store-write path.** Until #1017 the invocation lived here as
319
+ > an inline `node --input-type=module -e` block, which is a mechanism hiding inside prose: no
320
+ > `--help`, no exit-code contract, no test. Do not re-inline it, and do not hand-roll a
321
+ > `jq | ... > learnings.jsonl` pass — that bypasses every #721 safety net.
322
+
323
+ **Exit codes are the no-op rule.** `0` = applied (or a clean no-op). `1` = input error: the
324
+ sidecar is absent, carries a malformed line, or holds no records — the store and the archive
325
+ were **not touched**;
326
+ re-write the sidecar and re-run. `2` = the prune itself failed inside the lib. On any non-zero
327
+ exit, surface the error and stop — never retry with a shell rewrite, and never delete `$NEXT`
328
+ (the `&&` above already withholds the `rm`, so the assembled generation survives for a retry).
329
+
330
+ `pruneLearnings()` — the function the subcommand calls — performs steps 6 + 7 + 8 mechanically
331
+ and archives **every** record that
332
+ leaves the store, tagged with `_archived_at` + an `_archive_reason` from the closed enum
333
+ `expired | pruned | superseded | merged`:
334
+
335
+ - **6. Prune** — `expires_at` < now → `expired`; `confidence <= 0.0` → `pruned`.
336
+ - **7. Consolidate duplicates (NULL-SUBJECT SAFE)** — same `type` + non-empty `subject`: the
337
+ highest-confidence entry wins; each loser is archived `superseded` with a
338
+ `_superseded_by: <winning id>` tombstone. Entries with null/empty/missing `subject` are NEVER
339
+ collapsed — each is keyed by its unique `id` and always preserved (issue #284).
340
+ - **8. Rewrite** — via `rewriteLearnings()`: full schema validation, a `.bak-<ISO>` snapshot
341
+ (keep-3 rotation), then an atomic tmp+rename. Any id you drop from the temp sidecar without
342
+ an explicit reason is archived `pruned` automatically — the store can no longer lose a record
343
+ silently, whatever the next generation omits.
344
+
345
+ No `graceDays` here, deliberately: `/evolve` re-stamps `expires_at` on every reinforced learning
346
+ in steps 2–3 of THIS run, strictly before the prune, so an entry still expired at prune time is
347
+ one the analyzer just declined to reinforce. (The sweep's 14-day grace exists to protect entries
348
+ from being archived *before* that reinforcement pass runs — a hazard that cannot occur here.)
349
+
350
+ Report the returned `{scanned, kept, archived, byReason}` alongside the counts in the final
351
+ summary line. On a non-zero exit, do NOT retry with a shell rewrite — surface the error. The
352
+ old "read back the first line to confirm valid JSON" check is redundant here: `rewriteLearnings()`
353
+ round-trip-validates EVERY line before any byte reaches disk (#662), and the `malformed` guard
354
+ above rejects an unparseable sidecar before the store is touched at all.
355
+ 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.
273
356
 
274
357
  If `enabled` is `true`:
275
358
 
@@ -390,20 +473,26 @@ If user selects "Boost confidence", "Reduce confidence", "Delete specific learni
390
473
 
391
474
  ### Step 4.4: Apply Changes
392
475
 
393
- Use the same atomic rewrite strategy as Phase 3, Step 3.5:
476
+ Use the same archive-safe pipeline as Phase 3, Step 3.5 — **never** a hand-rolled `>` rewrite (#1017):
394
477
 
395
478
  1. Read all lines from `learnings.jsonl`
396
479
  2. Apply the selected operation to selected learnings:
397
480
  - **Boost:** +0.15 confidence (cap 1.0), reset expires_at to +`learning-expiry-days`
398
481
  - **Reduce:** -0.2 confidence
399
- - **Delete:** remove selected entries
482
+ - **Delete:** omit the selected entries from the next generation — do NOT delete them by hand.
483
+ `pruneLearnings()` detects every **record** that left the store — reconciled by `id`, or by a
484
+ content fingerprint when a record carries no usable `id` — and archives it with
485
+ `_archive_reason: "pruned"`, so a `learning-id` referenced by a rendered rule stays resolvable.
400
486
  - **Extend:** reset expires_at to current date + `learning-expiry-days`
401
- 3. Prune entries where `expires_at` < current date OR `confidence` <= 0.0
402
- 4. Consolidate duplicates (same `type` + non-empty `subject`): keep highest confidence.
403
- Null-subject entries are preserved individually (keyed by `id`). See SKILL.md #284 fix note.
404
- 5. Write entire result back with `>` (atomic rewrite)
487
+ 3. Steps 3–5 of the old prose (prune / consolidate / rewrite) are `pruneLearnings()` — run the
488
+ **exact** Step 3.5(5) invocation, writing the post-operation entry set to the `--entries`
489
+ sidecar. It prunes
490
+ (`expires_at` < now → `expired`; `confidence <= 0.0` → `pruned`), consolidates duplicates
491
+ (same `type` + non-empty `subject`, highest confidence wins, loser archived `superseded` with
492
+ `_superseded_by`; null-subject entries preserved individually per #284), and rewrites through
493
+ `rewriteLearnings()` with its `.bak-<ISO>` snapshot.
405
494
 
406
- Report: "Updated N learnings. Total active: K."
495
+ Report: "Updated N learnings. Total active: K. Archived: A (<byReason>)."
407
496
 
408
497
  ---
409
498
 
@@ -534,13 +623,22 @@ Cross-reference: PRD #506 AC1-AC4 + EARS gates. Vault Integration: dialectic doe
534
623
  - **ALWAYS** use uuid-v4 for new learning IDs (generate via `uuidgen` or equivalent bash command)
535
624
  - **ALWAYS** preserve a candidate-supplied `expires_at`; otherwise derive it from `LEARNING_TTL_DAYS[type]` via `deriveExpiresAt()` rather than hard-coding `learning-expiry-days`
536
625
  - **ALWAYS** present findings to user before writing — no silent writes
537
- - **ALWAYS** use atomic rewrite (read all, modify, write all with `>`) — never append with `>>`
626
+ - **ALWAYS** route store writes through `pruneLearnings()` / `rewriteLearnings()` — never a shell
627
+ `>` rewrite and never an append `>>`. Those helpers own the schema validation, the `.bak-<ISO>`
628
+ snapshot, and the atomic tmp+rename; a hand-rolled redirect owns none of them (#721, #1017)
629
+ - **ALWAYS** let a removed entry land in `learnings-archive.jsonl` — a record may leave the STORE,
630
+ but it may never leave the CORPUS. Rendered `.claude/rules/*.md` cite `learning-id` as provenance;
631
+ a hard delete turns that citation into a dangling pointer (#1017 measured 11 of 13 dead)
538
632
  - **ALWAYS** cap confidence at 1.0 — never exceed
539
633
 
540
634
  ## Anti-Patterns
541
635
 
542
636
  - **DO NOT** write learnings without user confirmation — always present via AskUserQuestion first (on Codex CLI where AskUserQuestion is unavailable, present as a numbered Markdown list)
543
- - **DO NOT** append to `learnings.jsonl` — always use atomic rewrite (read all, modify, write all)
637
+ - **DO NOT** append to `learnings.jsonl` with `>>`, and **DO NOT** rewrite it with `>` — call
638
+ `pruneLearnings()` (Step 3.5(5)); a shell redirect bypasses validation, backup, and the archive
639
+ - **DO NOT** hard-delete a learning. Every record that leaves the store is archived with an
640
+ `_archive_reason` (`expired` | `pruned` | `superseded` | `merged`) and, for the last two, a
641
+ `_superseded_by` / `_merged_into` tombstone naming its replacement
544
642
  - **DO NOT** create duplicate learnings — always check type + subject match first
545
643
  - **DO NOT** set confidence above 1.0 or forget to cap it
546
644
  - **DO NOT** fabricate patterns — only extract from actual session data with verifiable evidence
@@ -1,6 +1,14 @@
1
1
  ---
2
2
  name: frontmatter-guard
3
- description: Injects the canonical vault frontmatter schema snippet into agent prompts before any vault-write task, preventing malformed YAML frontmatter in Obsidian notes. <example>Context: wave-executor is about to dispatch a vault-mirror agent that writes learning notes under ~/Projects/vault/40-learnings/. user: "dispatch vault-write agent" assistant: "Injecting frontmatter-guard snippet into agent prompt (vault scope detected). Required fields: id, type, created, updated. Enum type: note|daily|project|person|reference|idea|learning|session." <commentary>The wave-executor pre-dispatch hook calls detectVaultTaskScope() — the fileScope contains /Projects/vault/40-learnings/ so the guard triggers and the snippet is prepended to the agent system prompt.</commentary></example>
3
+ description: >
4
+ Injects the canonical vault frontmatter schema snippet into agent prompts before any vault-write task,
5
+ preventing malformed YAML frontmatter in Obsidian notes. <example>Context: wave-executor is about to
6
+ dispatch a vault-mirror agent that writes learning notes under ~/Projects/vault/40-learnings/. user:
7
+ "dispatch vault-write agent" assistant: "Injecting frontmatter-guard snippet into agent prompt (vault
8
+ scope detected). Required fields: id, type, created, updated. Enum type:
9
+ note|daily|project|person|reference|idea|learning|session." <commentary>The wave-executor pre-dispatch
10
+ hook calls detectVaultTaskScope() — the fileScope contains /Projects/vault/40-learnings/ so the guard
11
+ triggers and the snippet is prepended to the agent system prompt.</commentary></example>
4
12
  model: inherit
5
13
  ---
6
14
 
@@ -6,7 +6,15 @@ model: haiku
6
6
  model-preference: sonnet
7
7
  model-preference-codex: gpt-5.4-mini
8
8
  model-preference-cursor: claude-sonnet-4-6
9
- description: Use this skill when performing VCS operations on GitLab or GitHub repositories — creating, updating, or closing issues and MRs, applying label taxonomy, running `glab`/`gh` CLI commands, or resolving project IDs dynamically. Acts as the single source of truth for CLI command syntax and label conventions; consuming skills reference this rather than duplicating logic. Triggers: "create a GitLab issue", "list open MRs", "apply priority label", "how do I resolve the project ID", "what's the carryover issue template". <example>Context: session-end needs to file a carryover issue for an incomplete task. user: "/close" assistant: "Creating carryover issue via glab with the Carryover Template from gitlab-ops — labels: carryover, priority::high."</example>
9
+ description: >
10
+ Use this skill when performing VCS operations on GitLab or GitHub repositories — creating, updating, or
11
+ closing issues and MRs, applying label taxonomy, running `glab`/`gh` CLI commands, or resolving project
12
+ IDs dynamically. Acts as the single source of truth for CLI command syntax and label conventions;
13
+ consuming skills reference this rather than duplicating logic. Triggers: "create a GitLab issue", "list
14
+ open MRs", "apply priority label", "how do I resolve the project ID", "what's the carryover issue
15
+ template". <example>Context: session-end needs to file a carryover issue for an incomplete task. user:
16
+ "/close" assistant: "Creating carryover issue via glab with the Carryover Template from gitlab-ops —
17
+ labels: carryover, priority::high."</example>
10
18
  ---
11
19
 
12
20
  # VCS Operations Reference
@@ -57,9 +65,9 @@ Never hardcode project IDs. Resolve them at runtime — and re-resolve live each
57
65
 
58
66
  ```bash
59
67
  # GitLab — get numeric project ID
60
- glab repo view --output json | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])"
68
+ glab repo view -R <OWNER>/<REPO> --output json | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])"
61
69
 
62
- # GitHub — get owner/name identifier
70
+ # GitHub — get owner/name identifier (gh repo takes the repo POSITIONALLY; it rejects -R)
63
71
  gh repo view --json nameWithOwner -q '.nameWithOwner'
64
72
  ```
65
73
 
@@ -153,33 +161,35 @@ GitHub has no native issue-blocking relation at all — the body-ordering-note f
153
161
 
154
162
  ## Common CLI Commands
155
163
 
164
+ **Directive — consult this only for a command NOT listed below; every example here already complies.** Each repo-scoped `glab`/`gh` invocation carries `-R <OWNER>/<REPO>` (`glab` also accepts `GROUP/SUBGROUP/REPO` or a full remote URL — `resolveRepoSpec()` in `scripts/lib/vcs-repo-spec.mjs` produces the right spec per platform); without the flag the target is whatever the ambient cwd remote happens to be, which is the wrong project in a sibling worktree, an `/autopilot` child, or a fork. Exactly four exceptions, each probed against the binaries: `glab api`/`gh api` (no `--repo` exists — pin the host with `--hostname` from `resolveRepoHost()` instead), `gh repo <*>` (rejects `-R`; takes the repository positionally), a `glab repo` call that already names the repository positionally, and — conditionally, not subcommand-wide — `gh pr checks|view|diff|ready|merge|comment`, where `-R` is legal ONLY alongside the `<number>|<url>|<branch>` positional: `gh pr checks -R <OWNER>/<REPO> <BRANCH>` carries the flag, while a positional-less `gh pr checks -R <OWNER>/<REPO> --watch` exits 1 with `argument required when using the` `--repo` `flag` — so name the PR or drop the flag, and never derive this from `--help`, which lists `-R` under INHERITED FLAGS with no such qualifier.
165
+
156
166
  ### GitLab (glab)
157
167
 
158
168
  ```bash
159
169
  # Issues
160
- glab issue list --per-page 50 # All open issues
161
- glab issue list --label "status:ready" --per-page 10 # Ready to work on
162
- glab issue list --label "priority::high" --per-page 10 # High priority
163
- glab issue list --closed --per-page 10 # Recently closed
164
- glab issue view <IID> # View issue details
165
- glab issue view <IID> --comments # With comments
166
- glab issue create --title "title" --label "priority::high,status:ready"
167
- glab issue update <IID> --label "status:in-progress" # WARNING: --label REPLACES the full set — see caveat below
168
- glab issue close <IID> # then VERIFY: glab issue view <IID> must show state=closed
169
- glab issue note <IID> -m "Comment text" # Add comment
170
+ glab issue list -R <OWNER>/<REPO> --per-page 50 # All open issues
171
+ glab issue list -R <OWNER>/<REPO> --label "status:ready" --per-page 10 # Ready to work on
172
+ glab issue list -R <OWNER>/<REPO> --label "priority::high" --per-page 10 # High priority
173
+ glab issue list -R <OWNER>/<REPO> --closed --per-page 10 # Recently closed
174
+ glab issue view -R <OWNER>/<REPO> <IID> # View issue details
175
+ glab issue view -R <OWNER>/<REPO> <IID> --comments # With comments
176
+ glab issue create -R <OWNER>/<REPO> --title "title" --label "priority::high,status:ready"
177
+ glab issue update -R <OWNER>/<REPO> <IID> --label "status:in-progress" # WARNING: --label REPLACES the full set — see caveat below
178
+ glab issue close -R <OWNER>/<REPO> <IID> # then VERIFY: re-read the issue; it must show state=closed
179
+ glab issue note -R <OWNER>/<REPO> <IID> -m "Comment text" # Add comment
170
180
 
171
181
  # MRs
172
- glab mr list # Open MRs
173
- glab mr create --fill --draft # Create draft MR
174
- glab mr merge <MR_IID> # Merge MR
182
+ glab mr list -R <OWNER>/<REPO> # Open MRs
183
+ glab mr create -R <OWNER>/<REPO> --fill --draft # Create draft MR
184
+ glab mr merge -R <OWNER>/<REPO> <MR_IID> # Merge MR
175
185
 
176
186
  # Pipelines
177
- glab pipeline list --per-page 5 # Recent pipelines
178
- glab pipeline status <ID> # Pipeline details
187
+ glab pipeline list -R <OWNER>/<REPO> --per-page 5 # Recent pipelines
188
+ glab pipeline status -R <OWNER>/<REPO> <ID> # Pipeline details
179
189
 
180
- # API (reads host from git remote automatically)
181
- glab api "projects/$(glab repo view --output json | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")/issues?state=opened&per_page=50"
182
- glab api "projects/$(glab repo view --output json | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")/milestones?state=active"
190
+ # API (no --repo exists here — the endpoint path IS the target; pin the host with --hostname)
191
+ glab api "projects/$(glab repo view -R <OWNER>/<REPO> --output json | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")/issues?state=opened&per_page=50"
192
+ glab api "projects/$(glab repo view -R <OWNER>/<REPO> --output json | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")/milestones?state=active"
183
193
  ```
184
194
 
185
195
  **Label update caveat (PUT-replaces, not additive):** `glab issue update --label` (and the underlying GitLab labels API) PUT-REPLACES the entire label set — it does not add to the existing set. To change a single label you must pass the FULL desired label list, or use the dedicated add/remove operations, which are themselves unreliable across `glab` versions. Preferred safe pattern: use `--label` (adds) together with `--unlabel` (removes) on `glab issue update` when your installed `glab` version supports both; otherwise read the current labels first, compute the full new set, and PUT once. The same PUT-replace semantics apply to `glab mr update --label`.
@@ -194,27 +204,27 @@ glab api "projects/$(glab repo view --output json | python3 -c "import json,sys;
194
204
 
195
205
  ```bash
196
206
  # Issues
197
- gh issue list --limit 50 # All open issues
198
- gh issue list --label "status:ready" --limit 10 # Ready to work on
199
- gh issue list --label "priority::high" --limit 10 # High priority
200
- gh issue list --state closed --limit 10 # Recently closed
201
- gh issue view <NUMBER> # View issue details
202
- gh issue view <NUMBER> --comments # With comments
203
- gh issue create --title "title" --label "priority::high,status:ready"
204
- gh issue edit <NUMBER> --add-label "status:in-progress"
205
- gh issue close <NUMBER>
206
- gh issue comment <NUMBER> --body "Comment text" # Add comment
207
+ gh issue list -R <OWNER>/<REPO> --limit 50 # All open issues
208
+ gh issue list -R <OWNER>/<REPO> --label "status:ready" --limit 10 # Ready to work on
209
+ gh issue list -R <OWNER>/<REPO> --label "priority::high" --limit 10 # High priority
210
+ gh issue list -R <OWNER>/<REPO> --state closed --limit 10 # Recently closed
211
+ gh issue view -R <OWNER>/<REPO> <NUMBER> # View issue details
212
+ gh issue view -R <OWNER>/<REPO> <NUMBER> --comments # With comments
213
+ gh issue create -R <OWNER>/<REPO> --title "title" --label "priority::high,status:ready"
214
+ gh issue edit -R <OWNER>/<REPO> <NUMBER> --add-label "status:in-progress"
215
+ gh issue close -R <OWNER>/<REPO> <NUMBER>
216
+ gh issue comment -R <OWNER>/<REPO> <NUMBER> --body "Comment text" # Add comment
207
217
 
208
218
  # PRs
209
- gh pr list --state open # Open PRs
210
- gh pr create --fill --draft # Create draft PR
211
- gh pr merge <NUMBER> # Merge PR
219
+ gh pr list -R <OWNER>/<REPO> --state open # Open PRs
220
+ gh pr create -R <OWNER>/<REPO> --fill --draft # Create draft PR
221
+ gh pr merge -R <OWNER>/<REPO> <NUMBER> # Merge PR
212
222
 
213
223
  # Workflows (CI equivalent)
214
- gh run list --limit 5 # Recent workflow runs
215
- gh run view <RUN_ID> # Run details
224
+ gh run list -R <OWNER>/<REPO> --limit 5 # Recent workflow runs
225
+ gh run view -R <OWNER>/<REPO> <RUN_ID> # Run details
216
226
 
217
- # API
227
+ # API (no --repo exists here — the endpoint path IS the target; pin the host with --hostname)
218
228
  gh api "repos/{owner}/{repo}/issues?state=open&per_page=50"
219
229
  gh api "repos/{owner}/{repo}/milestones?state=open"
220
230
  ```
@@ -265,6 +275,9 @@ What should be achieved and why.
265
275
  ### Context for next session
266
276
  [relevant context, file paths, decisions made]
267
277
 
278
+ ### Revisit-Trigger
279
+ [the concrete condition or event that reopens this — e.g. "when <metric/state> passes <threshold>", "at the next <session type/release>". A deferral with no named trigger is not a deferral — never a bare "later"/"low prio"/"TBD".]
280
+
268
281
  ### Open Questions
269
282
  _(optional — include only when unanswered questions remain in STATE.md `## Open Questions` at close; omit this section entirely otherwise)_
270
283
  - [ ] [unanswered question 1] (source: W<N>/<agent>, prio: high|medium|low)
@@ -274,6 +287,8 @@ _(optional — include only when unanswered questions remain in STATE.md `## Ope
274
287
  Relates to #ORIGINAL_IID
275
288
  ```
276
289
 
290
+ `### Revisit-Trigger` is **mandatory** for the `/close` carryover template above: a carryover deferred without a concrete, checkable reopen condition is a rot risk — "later" reliably means "never". (The SPIRAL/FAILED escalation carryover built by `scripts/lib/spiral-carryover.mjs` is a deliberately separate, machine-triaged template and carries no trigger field.)
291
+
277
292
  ### Discovery Finding
278
293
 
279
294
  ```markdown
@@ -362,8 +377,8 @@ Read .gitlab/merge_request_templates/Default.md
362
377
  Read .github/PULL_REQUEST_TEMPLATE.md
363
378
 
364
379
  # 2. Then create — hook now passes
365
- glab mr create --title "..." --description "..."
366
- gh pr create --title "..." --body "..."
380
+ glab mr create -R <OWNER>/<REPO> --title "..." --description "..."
381
+ gh pr create -R <OWNER>/<REPO> --title "..." --body "..."
367
382
  ```
368
383
 
369
384
  ### Cross-References
@@ -1,6 +1,15 @@
1
1
  ---
2
2
  name: gitlab-portfolio
3
- description: Use when you need a single-pane cross-repo health view across all vault-registered GitLab and GitHub projects. Discovers repos from `_overview.md` frontmatter in `<vault>/01-projects/*/`, aggregates open issues, MRs, critical labels, and stale signals via parallel `glab`/`gh` calls, then writes an idempotent `_PORTFOLIO.md` dashboard. Runs automatically at session-start Phase 2 when `gitlab-portfolio.enabled=true`. Triggers: "show portfolio status", "refresh the portfolio dashboard", "which repos have critical issues", "run /portfolio". <example>Context: session-start, gitlab-portfolio.enabled=true, vault has 5 registered repos. user: "/session deep" assistant: "Portfolio: 3 critical issues across 2 repos — run /portfolio for details. Dashboard written to vault/01-projects/_PORTFOLIO.md."</example>
3
+ description: >
4
+ Use when you need a single-pane cross-repo health view across all vault-registered GitLab and GitHub
5
+ projects. Discovers repos from `_overview.md` frontmatter in `<vault>/01-projects/*/`, aggregates open
6
+ issues, MRs, critical labels, and stale signals via parallel `glab`/`gh` calls, then writes an
7
+ idempotent `_PORTFOLIO.md` dashboard. Runs automatically at session-start Phase 2 when
8
+ `gitlab-portfolio.enabled=true`. Triggers: "show portfolio status", "refresh the portfolio dashboard",
9
+ "which repos have critical issues", "run /portfolio". <example>Context: session-start,
10
+ gitlab-portfolio.enabled=true, vault has 5 registered repos. user: "/session deep" assistant:
11
+ "Portfolio: 3 critical issues across 2 repos — run /portfolio for details. Dashboard written to
12
+ vault/01-projects/_PORTFOLIO.md."</example>
4
13
  model: sonnet
5
14
  ---
6
15
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  You are the Interrogator — a staff engineer who pressure-tests a plan, design, or PRD by playing devil's advocate. Where `/brainstorm` is a cooperative Design Facilitator that *narrows* an ambiguous design space, you are the adversarial stress test that tries to *break* a plan the user already believes in. You don't collect wishes; you hunt contradictions. You don't expand scope; you expose the assumptions hiding inside it.
6
6
 
7
- You respond in {{owner.language}} when that matches the user's language. You meet people at their abstraction level — product language with stakeholders, interface and data-model language with engineers.
7
+ You answer in the operator's language: `owner.language` in `~/.config/session-orchestrator/owner.yaml`, falling back to `en` when that file is missing, unreadable, or the key is absent — and following the operator's own language the moment he writes in another one. You meet people at their abstraction level — product language with stakeholders, interface and data-model language with engineers.
8
8
 
9
9
  The user invited the grilling. Relentlessness is the service they asked for, not rudeness. Be sharp, be specific, never be a yes-man — but every challenge points at the plan, never at the person.
10
10
 
@@ -40,6 +40,49 @@ Apply these continuously throughout the grill — they are the substance of the
40
40
 
41
41
  **Apply the tactics that bite.** Not every tactic fits every target — a tooling or meta plan may have no glossary to collide with, a greenfield idea may have no code to contradict yet. Run the tactics that have real material; never manufacture a conflict to tick a box. A forced question violates the fewer-sharper-questions discipline as surely as a skipped real one does.
42
42
 
43
+ ## Output Levels
44
+
45
+ The active level is `efficiency.output-level` in `~/.config/session-orchestrator/owner.yaml`. If that file is missing, unreadable, or the key is absent, the level is `full`. Apply the matching block below for the whole grill.
46
+
47
+ **How to read a budget.** A *turn* is every chat line you author between the user's last answer and your next question — the evidence you read out of the code, the contradiction you name, the one sentence of recommendation reasoning. Raw Grep/Read output does not count; your narration of it does. A budget is a ceiling, not a target: under is fine, over is a defect. You meet it by WITHHOLDING, never by dropping — no contradiction disappears, it just gets stated in fewer words.
48
+
49
+ This is the tightest of the orchestrator's budgets by design. A turn is structurally one sentence plus one question; a budget generous enough to hold five questions would license exactly the volley § One question at a time forbids. The grill summary file, if the user asks for one, carries no budget — it is the artifact, not the conversation.
50
+
51
+ **Escalation (all levels).** When the operator writes `expand <topic>` (German: `mehr zu <Abschnitt>`), print that topic's full detail immediately, without re-asking and without the budget applying to that one response.
52
+
53
+ **Never traded for brevity (all levels).** No budget may be met by cutting any of the following. Where a budget and one of them collide, the budget yields:
54
+ - input validation, and the reporting of invalid input;
55
+ - error handling, error messages, and failure disclosure — a swallowed error is never "concise";
56
+ - security findings, warnings, and destructive-action confirmations (PSA-003);
57
+ - accessibility of the output itself — no meaning carried by colour or emoji alone, no bare unlabelled numbers, no table whose header you dropped to save a line;
58
+ - anything the operator explicitly asked to see;
59
+ - a kill assumption's four fields — *Fails-if*, *Evidence-this-week*, *Kill-criterion*, *Cheapest-test* (Tactic 5) — and the Tiger / Paper Tiger / Elephant sort (Tactic 6). Those are the findings themselves, not narration about them; a three-field workup is an incomplete answer, not a short one.
60
+
61
+ ### output-level: ultra
62
+ - Meaning: telegraphic — the evidence, the contradiction, the question. No narration.
63
+ - Budget: ≤6 lines per turn; ≤1 line per tactic finding; ≤1 line of recommendation reasoning before the tool call. A kill-assumption or pre-mortem turn is exempt (see the never-traded list) but stays at one line per field.
64
+ - Shape: quote the code as `<file>:<line> — <what it does>`, then the collision, then the question. Never restate the user's answer back at them.
65
+ - Escalation: `expand <topic>` — see § Escalation above.
66
+
67
+ ### output-level: full
68
+ - Meaning: terse but complete — framing trimmed, evidence preserved. This is the default.
69
+ - Budget: ≤14 lines per turn; ≤3 lines per tactic finding; ≤2 lines of recommendation reasoning before the tool call.
70
+ - Shape: the steelman in one line, then the attack, then the question. Every claim about behaviour keeps its `<file>:<line>` — that citation IS the evidence; what gets trimmed is the commentary on it.
71
+ - Escalation: `expand <topic>` — see § Escalation above.
72
+
73
+ ### output-level: lite
74
+ - Meaning: verbose — the reasoning behind each challenge is spelled out. Chosen for learning, not for speed.
75
+ - Budget: ≤35 lines per turn; ≤8 lines per tactic finding. Still a ceiling — `lite` is not "unbounded".
76
+ - Shape: explain which tactic you are applying and why it bites here, name the branches of the decision tree you are deferring, define unfamiliar terms on first use.
77
+ - Escalation: `expand <topic>` — see § Escalation above.
78
+
79
+ ### Companion dials
80
+
81
+ Same file, same lookup, same fallback-to-default rule:
82
+
83
+ - `efficiency.preamble` — `minimal` (default): at most one clause before a tool call, and only when the next step is non-obvious; never "Let me read the model." immediately followed by reading it. `verbose`: one sentence before each Grep/Read naming the contradiction you expect to find.
84
+ - `tone.style` — `direct` (this soul's baseline: name the contradiction plainly and make the user resolve it), `neutral` (state the collision without advocacy; still recommend when asked), `friendly` (same content, softer framing; never softer facts). No setting makes you a yes-man — the challenge always lands, only its wording moves.
85
+
43
86
  ## Values
44
87
 
45
88
  - **Skeptical by default** — a plausible claim is not a verified one; the cheap challenge now saves the expensive correction later
@@ -296,11 +296,24 @@ After completing all four phases, report:
296
296
 
297
297
  This advances the auto-dream cadence marker (`readDreamSignals` → `lastCleanupAt` in `scripts/lib/auto-dream.mjs`) so `shouldDispatchAutoDream` does not fire a false nudge on the next session.
298
298
 
299
- **Signalling contract (coordinator responsibility at session-end Phase 3.7):**
300
- - Set `ranMemoryCleanupThisSession = true` when `/memory-cleanup` ran this session in ANY mode or with ANY outcome.
301
- - Pass this flag to `stampMemoryCleanup()` from `scripts/lib/memory-cleanup-stamp.mjs` before emitting the session record (see `skills/session-end/session-metrics-write.md` § 1-pre).
302
- - Do NOT distinguish between "applied changes" and "healthy no-op" — both count.
303
- - Do NOT set `memory_cleanup_at` to `null`; simply omit the field when cleanup did not run.
299
+ **Signalling contract — emit the event, do not rely on remembering.**
300
+
301
+ As the LAST step of every completed run — dry-run, apply-pending, or healthy no-op alike — emit the completion event. This is mandatory and it is the whole mechanism; there is no second, prose-only path that also works:
302
+
303
+ ```bash
304
+ node scripts/emit-event.mjs \
305
+ --type orchestrator.memory.cleanup_completed \
306
+ --payload "{\"semantic_session_id\":\"<the session: value from STATE.md frontmatter>\",\"mode\":\"<dry-run|apply-pending|no-op>\"}"
307
+ ```
308
+
309
+ `scripts/emit-session.mjs` then DERIVES `memory_cleanup_at` from that event at session-close time via `deriveMemoryCleanupSignal()` (`scripts/lib/memory-cleanup-stamp.mjs`), matching events whose `timestamp` falls inside the session's own `[started_at, completed_at]` window. Nothing downstream depends on the coordinator recalling that a cleanup happened.
310
+
311
+ - **Do NOT distinguish "applied changes" from "healthy no-op"** — both count as a run, so both emit. Put the distinction in `mode`, never in whether you emit.
312
+ - **Do NOT hand-append to `events.jsonl`.** Route through `emit-event.mjs` → `emitEvent()`; hand-rolled appenders drift from the canonical record shape (the `stop` vs `orchestrator.session.stopped` divergence, #609).
313
+ - **`semantic_session_id` is the semantic id** (`main-2026-08-17-session-1`), not the UUID — `sessions.jsonl` `session_id` lives in that same space, and the matcher compares against it. Omitting the field is tolerated (the event is then claimed on the time window alone), but supplying it is what makes attribution exact when two sessions overlap.
314
+ - **An explicit `memory_cleanup_at` already on the record WINS** over derivation and is never overwritten. That path exists for backfills and tests, not for normal operation.
315
+
316
+ **Why this is mechanical and not a prose instruction:** it used to be one. On 2026-08-14 a `/memory-cleanup` ran and produced a documented yield, the coordinator did not execute the prose step, and all three session records of that day carried `memory_cleanup_at: null` — so the session-start banner reported "last cleanup 29 days ago" while the operator's own notes said 3. `stampMemoryCleanup()` had zero production callers at the time; every reference to it was an instruction asking an LLM to remember. Same failure class as the STATE.md write-race that Epic #583 replaced with a lock: Disziplin statt Mechanik.
304
317
 
305
318
  ## Anti-Patterns
306
319