session-orchestrator 3.20.0 → 3.22.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 (202) 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/000-session-orchestrator.mdc +3 -2
  5. package/.cursor/rules/030-wave-execution.mdc +10 -8
  6. package/.cursor/rules/040-discovery.mdc +6 -6
  7. package/.cursor/rules/050-plan.mdc +8 -8
  8. package/CHANGELOG.md +515 -0
  9. package/README.md +16 -11
  10. package/agents/analyst.md +1 -1
  11. package/agents/architect-reviewer.md +1 -1
  12. package/agents/code-implementer.md +4 -2
  13. package/agents/db-specialist.md +1 -1
  14. package/agents/dialectic-deriver.md +1 -1
  15. package/agents/docs-writer.md +1 -1
  16. package/agents/memory-proposal-collector.md +7 -5
  17. package/agents/qa-strategist.md +1 -1
  18. package/agents/security-reviewer.md +1 -1
  19. package/agents/session-reviewer.md +42 -1
  20. package/agents/skill-applied-judge.md +1 -1
  21. package/agents/test-writer.md +1 -1
  22. package/agents/ui-developer.md +1 -1
  23. package/agents/ux-evaluator.md +1 -1
  24. package/commands/eli5.md +33 -0
  25. package/commands/release.md +62 -0
  26. package/commands/test.md +2 -2
  27. package/docs/components.md +6 -5
  28. package/docs/migration-v3.md +9 -6
  29. package/docs/persona-panel.md +3 -1
  30. package/docs/scope-collision-guard.md +167 -0
  31. package/docs/session-config-reference.md +31 -8
  32. package/hooks/_lib/lock-bootstrap.mjs +19 -13
  33. package/hooks/enforce-scope.mjs +103 -3
  34. package/hooks/hooks-codex.json +1 -1
  35. package/hooks/hooks.json +21 -1
  36. package/hooks/on-session-end.mjs +76 -97
  37. package/hooks/on-session-start.mjs +195 -104
  38. package/hooks/on-stop.mjs +127 -12
  39. package/hooks/post-bash-write-verify.mjs +8 -32
  40. package/hooks/pre-auq-clarity.mjs +787 -0
  41. package/hooks/pre-bash-issue-budget.mjs +17 -18
  42. package/hooks/pre-task-scope-disjoint.mjs +1042 -0
  43. package/package.json +3 -1
  44. package/pi/prompts/eli5.md +12 -0
  45. package/pi/prompts/release.md +12 -0
  46. package/scripts/auq-audit.mjs +825 -0
  47. package/scripts/autopilot.mjs +10 -9
  48. package/scripts/emit-session.mjs +42 -0
  49. package/scripts/export-hw-learnings.mjs +61 -2
  50. package/scripts/lib/auq/clarity.mjs +1314 -0
  51. package/scripts/lib/auq/parse.mjs +1006 -0
  52. package/scripts/lib/auq/schema.mjs +1457 -0
  53. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  54. package/scripts/lib/backlog-scan.mjs +106 -15
  55. package/scripts/lib/build-live-signals.mjs +7 -3
  56. package/scripts/lib/ci-status-banner.mjs +267 -77
  57. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
  58. package/scripts/lib/config/vault-integration.mjs +12 -1
  59. package/scripts/lib/dispatcher/rank.mjs +4 -7
  60. package/scripts/lib/gates/gate-full.mjs +3 -3
  61. package/scripts/lib/gates/gate-helpers.mjs +17 -6
  62. package/scripts/lib/git-config-drift.mjs +471 -0
  63. package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
  64. package/scripts/lib/io.mjs +432 -7
  65. package/scripts/lib/issue-budget.mjs +63 -9
  66. package/scripts/lib/learnings/select.mjs +157 -3
  67. package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
  68. package/scripts/lib/mirror-issues-banner.mjs +266 -0
  69. package/scripts/lib/named-vault-resolver.mjs +105 -16
  70. package/scripts/lib/owner-interview.mjs +78 -32
  71. package/scripts/lib/peer-cards/schema.mjs +6 -2
  72. package/scripts/lib/peer-discovery.mjs +73 -22
  73. package/scripts/lib/project-hygiene.mjs +64 -4
  74. package/scripts/lib/reconcile/renderer.mjs +17 -4
  75. package/scripts/lib/reconcile/writer.mjs +69 -30
  76. package/scripts/lib/redact-spans.mjs +89 -0
  77. package/scripts/lib/resource-probe/evaluate.mjs +330 -149
  78. package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
  79. package/scripts/lib/resource-probe.mjs +18 -2
  80. package/scripts/lib/scope-baseline.mjs +77 -17
  81. package/scripts/lib/scope-gate.mjs +658 -0
  82. package/scripts/lib/secret-masker.mjs +262 -0
  83. package/scripts/lib/session-lock.mjs +34 -10
  84. package/scripts/lib/session-registry.mjs +9 -1
  85. package/scripts/lib/spiral-carryover.mjs +23 -2
  86. package/scripts/lib/state-md/mission-status.mjs +164 -58
  87. package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
  88. package/scripts/lib/validate/check-agents.mjs +77 -5
  89. package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
  90. package/scripts/lib/validate/check-commands.mjs +2 -20
  91. package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
  92. package/scripts/lib/validate/check-hooks-symmetry.mjs +48 -0
  93. package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
  94. package/scripts/lib/validate/check-rules.mjs +153 -9
  95. package/scripts/lib/validate/check-skills.mjs +191 -0
  96. package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
  97. package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
  98. package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
  99. package/scripts/lib/validate/check-unwired-features.mjs +219 -11
  100. package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
  101. package/scripts/lib/validate/frontmatter-block.mjs +61 -0
  102. package/scripts/lib/validate/tier-inference.mjs +46 -8
  103. package/scripts/lib/vault-backfill/glab.mjs +91 -58
  104. package/scripts/lib/vault-backfill/manifest.mjs +28 -8
  105. package/scripts/lib/vault-mirror/namespace.mjs +146 -1
  106. package/scripts/lib/vault-mirror/process.mjs +264 -31
  107. package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
  108. package/scripts/lib/vault-status/board-writer.mjs +300 -56
  109. package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
  110. package/scripts/lib/vcs-repo-spec.mjs +680 -30
  111. package/scripts/lib/wave-resource-gate.mjs +67 -73
  112. package/scripts/materialize-wave-scope.mjs +281 -0
  113. package/scripts/print-learnings-index.mjs +30 -3
  114. package/scripts/release.mjs +983 -107
  115. package/scripts/run-quality-gate.mjs +14 -0
  116. package/scripts/site-numbers.mjs +1049 -0
  117. package/scripts/validate-plugin.mjs +64 -0
  118. package/scripts/validate-wave-scope.mjs +286 -12
  119. package/scripts/vault-backfill.mjs +32 -5
  120. package/scripts/vault-mirror.mjs +26 -1
  121. package/skills/_shared/monitor-patterns.md +24 -4
  122. package/skills/_shared/parallel-aware-auq.md +30 -24
  123. package/skills/_shared/parallel-aware-preamble.md +31 -2
  124. package/skills/_shared/state-ownership.md +49 -6
  125. package/skills/bootstrap/SKILL.md +2 -1
  126. package/skills/brainstorm/SKILL.md +18 -18
  127. package/skills/brainstorm/soul.md +12 -0
  128. package/skills/claude-md-drift-check/SKILL.md +9 -1
  129. package/skills/debug/SKILL.md +4 -1
  130. package/skills/discovery/SKILL.md +28 -24
  131. package/skills/discovery/issue-templates.md +4 -4
  132. package/skills/discovery/probes-code.md +2 -2
  133. package/skills/discovery/probes-feature.md +6 -6
  134. package/skills/discovery/probes-infra.md +2 -2
  135. package/skills/discovery/probes-session.md +5 -5
  136. package/skills/dispatcher/SKILL.md +10 -1
  137. package/skills/eli5/SKILL.md +43 -0
  138. package/skills/evolve/SKILL.md +8 -9
  139. package/skills/frontmatter-guard/SKILL.md +9 -1
  140. package/skills/gitlab-ops/SKILL.md +73 -59
  141. package/skills/gitlab-portfolio/SKILL.md +10 -1
  142. package/skills/grill/SKILL.md +6 -6
  143. package/skills/grill/soul.md +16 -0
  144. package/skills/memory-cleanup/SKILL.md +20 -7
  145. package/skills/npm-publish/SKILL.md +23 -51
  146. package/skills/peekaboo-driver/SKILL.md +3 -3
  147. package/skills/persona-panel/SKILL.md +3 -1
  148. package/skills/plan/SKILL.md +18 -16
  149. package/skills/plan/mode-feature.md +1 -1
  150. package/skills/plan/mode-new.md +42 -12
  151. package/skills/plan/soul.md +12 -0
  152. package/skills/reconcile/SKILL.md +3 -3
  153. package/skills/repo-audit/SKILL.md +10 -1
  154. package/skills/session-end/SKILL.md +97 -22
  155. package/skills/session-end/metrics-collection.md +1 -1
  156. package/skills/session-end/phase-3-6-tail.md +37 -2
  157. package/skills/session-end/session-metrics-write.md +4 -10
  158. package/skills/session-plan/SKILL.md +2 -2
  159. package/skills/session-plan/wave-template.md +1 -1
  160. package/skills/session-start/SKILL.md +82 -36
  161. package/skills/session-start/phase-2-5-docs-planning.md +8 -8
  162. package/skills/session-start/phase-4-5-resource-health.md +82 -19
  163. package/skills/session-start/soul.md +110 -0
  164. package/skills/spinout/SKILL.md +5 -1
  165. package/skills/sunset-review/SKILL.md +11 -1
  166. package/skills/test-runner/SKILL.md +2 -2
  167. package/skills/tmux-layout/SKILL.md +7 -2
  168. package/skills/using-orchestrator/SKILL.md +1 -1
  169. package/skills/vault-mirror/SKILL.md +10 -1
  170. package/skills/vault-sync/SKILL.md +10 -1
  171. package/skills/vault-sync/validator.mjs +55 -6
  172. package/skills/wave-executor/wave-loop.md +64 -12
  173. package/skills/write-executable-plan/SKILL.md +6 -6
  174. package/scripts/lib/mission-status-schema.mjs +0 -114
  175. package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
  176. package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
  177. package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
  178. package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
  179. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
  180. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
  181. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
  182. package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
  183. package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
  184. package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
  185. package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
  186. package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
  187. package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
  188. package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
  189. package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
  190. package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
  191. package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
  192. package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
  193. package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
  194. package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
  195. package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
  196. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
  197. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
  198. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
  199. package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
  200. package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
  201. package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
  202. package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
package/CHANGELOG.md CHANGED
@@ -7,6 +7,521 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.22.0] - 2026-08-22
11
+
12
+ Twenty commits (12 `fix`, 5 `docs`, 2 `feat`, 1 `chore`; 150 files, +17,312/−2,575), no
13
+ `BREAKING CHANGE:` footer and no `!` subject. One strand runs through all of it:
14
+ **an instrument that reported confidently and measured the wrong quantity.** Not a
15
+ threshold set too tight, not a rule too strict — the wrong quantity, reported with the
16
+ same certainty as the right one. The host-resource warning fired on **99.0% of 1,477
17
+ measured session starts** and nobody could falsify it, because the two fields it was
18
+ computed from were never persisted. A hygiene probe announced "37 files (11 MB)" while
19
+ sizing the entire directory instead of the 37 — factor ~18 — and proposed deleting
20
+ version-controlled source files, because it never asked git. And the questions this tool
21
+ puts to its operator had never been measured at all: **21 of 72 passed** on first
22
+ measurement, and the single dominant cause was a 12-codepoint header limit that silently
23
+ truncated 30 of them.
24
+
25
+ ### Added
26
+
27
+ - **The operator's questions are now measured before he reads them (#1107).** `scripts/auq-audit.mjs`
28
+ scores every `AskUserQuestion` block and every prose fallback list against eight criteria
29
+ (K1–K8) plus two hard hurdles, all thresholds sourced from one registry — no hard-coded
30
+ number in the scorer. The census found **three populations, not one**: 40 Claude Code blocks,
31
+ 10 Codex prose lists, and 17 that every prior count had missed (9 `.cursor/rules/*.mdc` with
32
+ 10 choice blocks, plus 6 runtime questions inside `.mjs`). Baseline **21 of 72 (29%)**, after
33
+ the pass **72 of 72**. The dominant cause was structural: `header` caps at 12 codepoints and
34
+ the tool truncates silently — **26 of 42 headers exceeded it**, and one NFD-composed header
35
+ measured 12 visible characters as 14, destroying the question. Population A — the operator's
36
+ own path — ran 62% failing against 0% for the Codex and Cursor forms, which carry no header
37
+ at all.
38
+ - **A guard on the guard (#1122 groundwork).** The `AskUserQuestion` PreToolUse hook denies only
39
+ the two hard hurdles and reports the rest to stderr; `check-auq-clarity` is wired into
40
+ `validate-plugin` and blocks on H1/H2 with a measured 0% false-positive rate. Its own wiring is
41
+ proven by fake regression, not asserted.
42
+ - **`/eli5`** — say the last answer again in plain words, same facts, in the order the operator
43
+ needs them.
44
+
45
+ ### Fixed
46
+
47
+ - **The warning that fired on 99.0% of all session starts (#1089).** Measured over **1,477
48
+ `orchestrator.session.started` events across 18 repos**: `ram_free_gb < 2` fired on 84.0%,
49
+ `claude_processes >= 5` on 93.6%, any of the three on **99.0%** — against 4,884 stop events
50
+ with zero OOM markers. Three independent measurement errors, no threshold among them.
51
+ (a) `os.freemem()` on Darwin reports only `Pages free` — median **0.4 GB** on hosts with
52
+ 24–128 GB — so memory is now judged on `memory_pressure_pct_free` and a better signal
53
+ *replaces* a worse one rather than suppressing it. (b) `concurrent-sessions-warn` is
54
+ denominated in sessions and was compared against a process count; measured ratio **6.0:1**.
55
+ Same threshold, right denominator: 93.6% → **4.2%**. (c) One noisy axis could cap a wave;
56
+ now two independent soft signals must agree. The verdict had reached `sessions.jsonl` for
57
+ **15 of 1,734 sessions (0.9%)**, which is why the false alarm survived four months while six
58
+ repos independently wrote it into their learnings store — one at confidence **1.0**.
59
+ Codified as `.claude/rules/host-resources.md` (HR-101..106).
60
+ - **A standing condition is not a second opinion (#1089 follow-up).** The first live run of the
61
+ rebuilt verdict returned `warn | cap 2 | soft: ["cpu","zombies"]`. Zombie processes are idle
62
+ by definition — they cannot cause the load they are paired with — and are always present
63
+ (6, 13 and 9 in three readings minutes apart). They report; they never count toward the
64
+ two-signal rule.
65
+ - **A probe that sized the wrong set and never asked git.** `checkStaleArtifacts` computed its
66
+ byte figure over the whole of `.orchestrator/` instead of the aged subset it names —
67
+ the 37 files weighed **0.68 MB against 11 MB reported**. Independently, it consulted git
68
+ nowhere and therefore proposed pruning seven tracked, runtime-read source files, among them
69
+ the policy file `pre-bash-templates-first.mjs` reads and the two steering documents
70
+ session-start loads. The git exclusion is fail-**silent**, not fail-open: falling back to
71
+ "nothing is tracked" *is* the defect it closes.
72
+ - **Only the raw session ID releases a lock (#1085).** One session carried three identities —
73
+ STATE.md said `session-2`, the lock said `session-3`, issue-budget counted against a third
74
+ UUID — and `findPeers` reported the session's own presence as a foreign peer. The semantic
75
+ ID is now attribution metadata only; a release requires exact raw-ID equality, and a
76
+ semantic hint resolves to a raw ID only against a verified local binding. The companion fix
77
+ to issue-budget removed a data-loss path in which an identity-less invocation wrote its
78
+ fresh state into the shared counter and erased a live session's parked overflow.
79
+ - **The npm receipt is the boundary — and a `throw` lay behind it (#1088).** `publish()` threw
80
+ *after* a successful `npm publish`, during a 12-second registry-propagation poll: published,
81
+ not tagged, not pushed, reported as "failed, retryable". Propagation now returns a typed
82
+ outcome, each post-receipt step gets exactly one attempt, and everything else is collected as
83
+ `post-publish-reconciliation`. Leak detection decides on an extracted path rather than a regex
84
+ over `npm notice` prose (`contest` ≠ `tests`), and `ensureGithubRelease` became tri-state so
85
+ an auth or network answer no longer reads as "no release exists".
86
+ - **`--silent` leaked into the children, and the gate discarded its own diagnosis (a4f93cf).**
87
+ - **The empty sidecar path skipped the collision gate with exit 0 (#1083).** A coordinator that
88
+ wrote only the aggregate form of the two-shape scope declaration ran **six waves and ~27
89
+ dispatches with zero `FILE-SCOPE` injection** — and the absence looked identical to a clean
90
+ run. `scripts/materialize-wave-scope.mjs` is now the canonical writer of both shapes.
91
+ - **The GitLab target comes from the explicit path (#1065).** `glab repo view --output json`
92
+ materialised a full project response to read one field; on 2026-08-17 that response carried a
93
+ runner registration credential into a tool transcript. Project identity is now
94
+ `(host, URL-encoded namespace/project)` with traversal and double-encoding rejection, and the
95
+ same minimisation reached the vault-backfill path.
96
+ - **The restore froze after its first run (#state-md).** Also: a `gitleaks` allowlist that grew
97
+ by one proper name per incident now matches on a word boundary instead.
98
+ - **Four count claims where the checker could only see two (#docs-parity).** `docs/components.md`
99
+ claimed 46 skills and 26 commands against 47 and 27 on disk. The drift checker reported two —
100
+ its patterns are `^##`-anchored — while a census of the same two surfaces in the same file
101
+ found four; the two inside the Mermaid diagram were already self-contradictory before the fix.
102
+
103
+ ### Notes
104
+
105
+ `.claude/rules/host-resources.md` is new and always-on. `.claude/rules/bash-harness-pitfalls.md`
106
+ gained no rule text this line, but two measurement traps were recorded against it in the
107
+ learnings store: `rg -rn` is not `grep -rn` (`-r` is `--replace` and silently substitutes every
108
+ match), and zsh does not word-split an unquoted variable, so a multi-path `git log -- $paths`
109
+ reports zero matches with exit 0 — both produce a plausible wrong number without erroring.
110
+
111
+ ## [3.21.0] - 2026-08-19
112
+
113
+ Twenty-one commits (12 `fix`, 4 `feat`, 4 `docs`, 1 `chore`; 209 files, +28,906/−2,893), no
114
+ `BREAKING CHANGE:` footer and no `!` subject. Three strands — guard mechanics, the public
115
+ site, cross-session messaging — that turned out to be one strand: **a fact maintained in two
116
+ places, and a guard that is green without biting.** The sharpest instances are always the
117
+ quiet ones. **12 of 46 `SKILL.md`** and **14 of 16 `agents/*.md`** carried frontmatter that is
118
+ not YAML while every frontmatter checker in the tree had reported them clean for as long as
119
+ they existed. The v2 vault renderer's branch condition was **structurally false from its first
120
+ commit** and matched **0 of 253** session records. And the scope guard shipped, in the agent's
121
+ own channel, the command that disarms it.
122
+
123
+ ### Added
124
+
125
+ - **Cross-session messaging is wired (#1049, #1050, #1047 — Epic #1048, waves 1–2 of 5).**
126
+ New always-on rule `.claude/rules/cross-session-messaging.md` with CSM-001..005, and the PSA
127
+ decision tree gained a peer-inform branch that hangs **below** the sibling check on the
128
+ foreign-scope leg — so it can mask neither the sibling branch nor the PSA-002 pause. That is
129
+ structural, not prose. The two-axis rationale is re-founded rather than restated: the moat
130
+ was never "peer sessions cannot talk to each other" — two independent sessions in one working
131
+ copy exchanged full round-trips on 2026-08-16 — it is that they share **one working copy**,
132
+ and what crosses the channel is information, never isolation. `READ_ONLY_TOOLS` gained
133
+ `SendMessage` + `ListAgents`, the mechanical precondition for upward agent escalation;
134
+ `validateTierConsistency` filtered on `WRITE_TOOLS` and therefore named **no culprit at all**
135
+ for an unknown tool.
136
+ - **The public site is rebuilt, and it now proves its own numbers (#1043–#1046).** The live
137
+ page had served v3.19.0 against v3.20.0 in the repo for six days, all five legal paths were
138
+ 404, and the `Measured` block — the honesty argument of the whole page — carried five wrong
139
+ numbers.
140
+ - The load-bearing element is a band whose station height **is** the file count a wave may
141
+ write: `--unit: 15px` = one file. Discovery is therefore not a box of height 0 but a stroke
142
+ on the baseline — the moment the limit becomes visibly geometric rather than requested.
143
+ - Mobile had **no** navigation at all (`display:none`, no hamburger, 20 viewports of scroll);
144
+ 6 links are now visible at 390px. Twelve sections in identical scoring and 27 equally loud
145
+ cards became 10 sections with three loud blocks, cutting rendered height to **52.4%**
146
+ (10,350 → 5,428px — the factor is the durable figure, the viewport count is not: 5.98
147
+ viewports holds at 908px window height and becomes 6.03 at 900px). State no longer rides on
148
+ red/green alone but on shape **and** glyph. Cold-load acceptance without `<script>`: 115
149
+ content-bearing elements, 0 invisible, identical height — the page hangs on no condition.
150
+ - **`vercel.json`** carries the deploy configuration **versioned** instead of clicked into a
151
+ dashboard: `outputDirectory: "site"`, CSP/HSTS/`frame-ancestors` headers, www→apex redirect.
152
+ The Vercel Git integration is connected, so a push to the GitHub mirror publishes the site.
153
+ In `scripts/release.mjs` the checklist line `cd site && vercel --prod` is replaced by
154
+ `verifyLiveSite()` with four **distinguished** outcomes (version mismatch / success /
155
+ network error / surface moved), not collapsed onto a flat "not ok".
156
+ - **`scripts/site-numbers.mjs`** — 13 metrics read from the repo and written into
157
+ `data-metric` spans, ending the hand-maintained number block. Its first run against the new
158
+ page caught three real errors, one of them the coordinator's (`skills: page says 47, repo
159
+ says 46` — `skills/_shared/` has no `SKILL.md` and is not a skill) and one a contract defect
160
+ that would have produced `vv3.20.0` at the next release. Wired into `--set-version`,
161
+ deliberately **not** into CI: `sessions` and `learnings` grow every session, so a pipeline
162
+ gate on them would be permanently red.
163
+ - **`/guide`** — the four install paths, the first session with the honest number (7–10
164
+ questions, 2–4 minutes, not the claimed "one question"), and a section on recognising that
165
+ a session failed silently. The reason is a census, not a hunch: **all four issues opened by
166
+ people outside this repo are install or environment failures, not one a feature request**
167
+ (`gh issue list -R Kanevry/session-orchestrator --state all`; authors ≠ owner: #53, #54,
168
+ #62, #63).
169
+ - **`/impressum` and `/datenschutz`.** The privacy policy is not copied but written from the
170
+ measured processing: 0 external requests, no analytics, self-hosted fonts (3 files,
171
+ 55,052 B), Vercel server logs only. The sibling site's Vercel-Analytics sections are absent
172
+ on purpose — `grep` shows they are not active here. Sitemap 1 → 4 URLs.
173
+ - **Two catchers for the untracked-test-dependency class (#1081), because one was structurally
174
+ blind.** A differential run compares test *status*, and a test that swallows a missing file
175
+ and passes in **both** trees produces no difference at all. Statically:
176
+ `scripts/lib/validate/check-untracked-test-deps.mjs`, rules R2 (a statically resolved repo
177
+ root passed into an import closure that names an untracked path) and R4 (cwd-relative read),
178
+ measured 2/2 recall at 0 false positives where the obvious variants sat at 98–99% FP.
179
+ Structurally: `.husky/pre-push` materialises the sha being pushed via `git clone
180
+ --no-hardlinks` (1.04 s; `git archive` is unusable — without `.git`, 21 tests fail with "not a
181
+ git repository") and runs the gate **there**. It came within one function of being worthless:
182
+ `findProjectRoot()` checks `CLAUDE_PROJECT_DIR` and five siblings **before** cwd, so the gate
183
+ would have started in the tmp tree and read the working-tree files anyway.
184
+ - **A tracked census snapshot, `site/_census.json`, with ledger precedence** — per-metric opt-in
185
+ for exactly `sessions`, `learnings`, `counted-sha`, never a blanket fallback, which would have
186
+ made `collect()` blind to "wrong root". `optional: true` was considered and rejected by name:
187
+ it would have let the two tiles fall silently back into the hand-maintained state while
188
+ `--check` still reported the page as current — verbatim the silent-failure class the script
189
+ was written against. A known metric id without a value is now a third contract violation
190
+ (`unresolved`).
191
+ - **`tests/setup/scrub-git-env.mjs`, `scripts/lib/git-config-drift.mjs`, and
192
+ `check-test-git-config-target.mjs`** — the mechanism against this session's own accident (see
193
+ Notes). The setup file removes nine redirect-capable git variables once per worker; that is
194
+ the root, because the two lines that caused the damage passed a **correct** cwd, and `GIT_DIR`
195
+ beats cwd, `-C`, and even the positional of `git init <dir>`. The drift probe covers the
196
+ surface `git status` does not show, and runs with a filtered environment — a set `GIT_DIR`
197
+ would otherwise redirect the probe itself into a foreign repo and call this one clean. The
198
+ lint rule is WARN-only out of measurement, not caution: its first formulation had 11 hits,
199
+ **all** false positives (each a `git init <dir>` where the positional **is** the target); it
200
+ ends at 150/150 with a target.
201
+ - **`scripts/lib/validate/check-skills.mjs` — a real parser over all 46 `SKILL.md`**,
202
+ registered in `validate-plugin.mjs`. The sibling `check-agents.mjs` validated frontmatter
203
+ with line-oriented regexes, which is precisely how the 12 broken blocks stayed invisible:
204
+ an unquoted `description:` containing a `: ` (`"Iron Law: NO FIXES"`) is not YAML, but a
205
+ regex looking for `^description:` sees nothing wrong, and Claude Code's own loader is
206
+ lenient enough that the defect never surfaced at runtime. A `js-yaml` `CORE_SCHEMA` parse
207
+ is now rule R8 and the five field rules hang off it — none of them can be evaluated on a
208
+ block that does not parse. Deliberately **not** implemented: a block-scalar ban (see
209
+ below), and length ceilings on `name`/`description`, because no spec vendored in this repo
210
+ states one and an invented requirement is worse than none.
211
+ - **`scripts/lib/validate/frontmatter-block.mjs` — shared extraction, deliberately unshared
212
+ rules.** The extractor stood verbatim in two checkers, so the next change to the block
213
+ format would have landed in one copy and one gate would have started accepting what the
214
+ other rejects — with no test able to see it, because each gate tested its own copy. The
215
+ module owns the byte range and nothing else, and its header records why the three checkers
216
+ must stay **contradictory**: `check-agents.mjs` bans `description: >` because the agent
217
+ loader cannot read a folded scalar, while `check-skills.mjs` must tolerate it — for
218
+ `SKILL.md` the folded form is the only shape that makes the `: ` collision structurally
219
+ impossible, 35 of 46 files now use it, and porting the agent ban here would red 35 of 46
220
+ and forbid the very fix that made the gate green. The divergence is the requirement, not
221
+ drift.
222
+ - **`scripts/lib/mirror-issues-banner.mjs` — the GitHub-mirror blind spot**, wired into
223
+ `skills/session-start/SKILL.md` Phase 4. VCS auto-detection picks exactly one platform, so
224
+ in a GitLab-origin repo with a public GitHub mirror no code path ever read the mirror's
225
+ issues: everything filed by an external reporter was structurally invisible to every
226
+ session. The probe asks the other side, hard-pinned to `github` (auto-detecting here would
227
+ reproduce the defect it exists to compensate for) and **self-disabling** — no `github`
228
+ remote resolves to `undefined` → `null` → no spawn, no network call. No new Session Config
229
+ key by design: the spec comes from `git remote`, and a key would be a second SSOT drifting
230
+ against it. The return is three-valued, not two — `null` means *never asked* or *asked and
231
+ clean*; a `degraded` field with a closed enum means *the query failed and the state is
232
+ unknown*. Collapsing that third state into `null` is what makes a dead probe
233
+ indistinguishable from a healthy repo, which is the next section's recurring theme.
234
+ - **`atomicWriteWithBackup()` in `scripts/lib/io.mjs`** — the tmp-write/rename/`.bak-<ISO>`
235
+ primitive that `writeJsonAtomicSync` now delegates to, ending a second copy of the same
236
+ sequence (#734).
237
+ - **`check-unwired-features` signal S3 `orphaned-prose-module`** — a config key is not the
238
+ only thing prose can promise; a document can also assert that a module does a job nothing
239
+ calls. S3 fires only where the claim is in the **passive voice with a bare filename and no
240
+ exported symbol**: "…*are validated* against `foo.mjs`" asserts that something happens by
241
+ itself, whereas "dispatch via `runWavePool()`" addresses a reader who will do it. The
242
+ broader check — every export with no non-test importer — was measured and rejected: 1366
243
+ exports, 779 unimported, 93.2% false positives naive and still 81.2% after four exclusion
244
+ rules. A gate that prints 282 lines is switched off in week two, which is this file's own
245
+ disease one level up. Measured 2026-08-14, the cascade narrowed 452 production modules to
246
+ **2**; one of those is deleted below, so the live report now stands at 1. Read a near-empty
247
+ report as designed, not broken — S3 is a relapse guard, not a cleanup tool.
248
+ - **[ADR-0012](docs/adr/0012-pseudonym-map-privacy.md)** — host-local pseudonym map for
249
+ owner-leaky repo namespaces. An ADR rather than a code comment precisely because every
250
+ element reads like removable defensiveness at its call site while being load-bearing for a
251
+ property no test can observe directly: a green suite is fully compatible with the leak.
252
+
253
+ ### Fixed
254
+
255
+ - **The documented Claude Code install path was guaranteed broken.** `claude plugin dir` is not
256
+ a subcommand: `claude plugin dir session-orchestrator` answers `error: unknown command 'dir'`
257
+ and exits 1 (measured on Claude Code 2.1.235; `claude plugin --help` lists 14 subcommands and
258
+ `dir` is none of them). The `||` fallback therefore fired **every** time and pointed at
259
+ `~/.claude/plugins/session-orchestrator`, a directory that does not exist — so `cd` failed,
260
+ `&&` aborted, `npm install` never ran, and the hooks could not find `zx`. That is the cause
261
+ behind external issues #62/#63, whose reporters described the symptom. Replaced by a
262
+ layout-independent resolution through the plugin cache, extracted from the **shipped**
263
+ `site/index.html`, entity-decoded and executed verbatim: exit 0, correct path. Second defect
264
+ in `migration-v3.md` fixed alongside — a marketplace-installed plugin sits in a managed cache,
265
+ not a git checkout, so `git pull` there does nothing; replaced by `/plugin update`. Five sites
266
+ in five files; the two remaining matches for `claude plugin dir` are deliberate quotes on the
267
+ site explaining the breakage.
268
+ - **The v2 vault renderer was dead at birth (#1074).** `total_agents` stands in
269
+ `REQUIRED_FIELDS`, so the branch condition `total_agents === undefined` is structurally false
270
+ — written 4h23m before the first record that could have matched it, and matching **0 of 253**
271
+ records since. The obvious fix would have been **worse than the bug**: `agents` is polymorphic
272
+ — measured over 599 wave objects, **210× a number and 14× an array** — so a naive `??` would
273
+ have written `[object Object]` into the vault. A type-aware alias chain repairs 239 agent, 5
274
+ files and 280 quality cells at changed-existing = 0. The fixtures are now harvested golden
275
+ records; the old `makeV2Entry` invented a record no production writer can produce.
276
+ - **Four blind probes and one active fail-open (#1039).** `backlog-scan` folded "no remote" and
277
+ "query failed" into a silent `null`, from which the mode selector read "contributes 0 delta" —
278
+ a 40-issue backlog with critical labels looked like an empty one. `ci-status-banner` forbade in
279
+ its own comment at ~line 490 the very form it still used. `named-vault-resolver` had one
280
+ `source: 'fallback'` covering two causes (honestly relativised by the agent:
281
+ `resolveNamedVault` has zero production callers, so the defect was real but not yet reachable).
282
+ `scope-baseline` hardcoded `'origin/main...HEAD'` twice, and the damage sat elsewhere than in
283
+ the name: `wave-loop.md:874` renders `skipped: 'unresolvable-ref'` **silently**, so the
284
+ scope-drift tripwire was permanently inert in every non-origin repo and never said so.
285
+ `vault-mirror/namespace` `deriveRepo()` degraded silently to `basename(cwd)` and wrote that as
286
+ repo **identity** into the vault — which then happened for real this session. And
287
+ `harness-audit` category 6 was an active fail-open: outside a git repository it awarded **2/2
288
+ with "no github mirror remote configured — skipped"**; a non-measurable query now fails 0/2,
289
+ full marks only for genuine absence. Census correction: **14** executable origin hardcodings,
290
+ not the 4 the issue named.
291
+ - **The scope guard shipped the instructions for its own disarmament (#1057).** The
292
+ stale-manifest branch ended on ``remove it with `rm -f <path>` and let the coordinator write a
293
+ fresh manifest`` — and that string travels through `emitDeny()` in `permissionDecisionReason`,
294
+ i.e. into the context of the very agent whose edit was just denied. Reproduced end to end from
295
+ the coordinator: `rm -f` is **allowed** by `pre-bash-destructive-guard.mjs` (14 rules, only
296
+ `rm-rf-destructive` bites); with the manifest removed, an in-project `Edit` is ALLOW **and** a
297
+ `Write` to `/etc/` is ALLOW — not only gate 7 (allowedPaths) falls but gate 6 (containment),
298
+ because under the exit-0 protocol the hook then emits no decision at all and exit 0 is not a
299
+ veto. The guard was off for the rest of the session. Not theoretical: a **parallel** session in
300
+ the same working copy received this suggestion for the **live** `wave-scope.json` of this one,
301
+ and declined only on an indicator (mtime 7 minutes old), not on knowledge — at one hour of age
302
+ the same indicator points the other way. The new text names both cases instead of guessing one
303
+ and routes to `blocked` per PSA-001/003/007. `systemMessage` was examined and deliberately not
304
+ used: `io.mjs:329` says the operator sees that field, `hook-development/SKILL.md:108` says it
305
+ is "shown to Claude" — a session-wide guard disarmament does not belong on a channel whose
306
+ reader the repo cannot name consistently.
307
+ - **The owner-leakage scanner was blind to `.html` (#1076) — and its first sighted run found a
308
+ real defect.** The naive fix would have excluded **nothing**, because the sanctioned form
309
+ carries no `www.` while the site uses `www.` throughout; the result would have been a
310
+ permanently red gate including pre-commit. Then the eighth planted defect: seven forms had been
311
+ driven against the new coverage and all seven bit, but all seven sat in the CP1 corridor, and
312
+ CP1 is the **only** one of the eleven rules that runs through `canonicalizeLine()` — revived,
313
+ in other words, for 1 of 11. The eighth is an entity-encoded dot in the **domain**, which a
314
+ browser resolves and the scanner did not. CP2/CP3/CP7/CP8 now additionally check the canonical
315
+ form, and the discriminator is measured rather than guessed: dot-anchored rules gain
316
+ (canonicalisation can never fabricate a dot from a separator) while slash- and slug-anchored
317
+ rules stay raw (canon folds `-` onto `/`, which flips CP4/CP10 false→true and shreds CP6).
318
+ `isAllowlisted()` on the canonical form would have been **wrong** — canon breaks its own
319
+ allowlist — so the design uses an occurrence counter instead: if the canonical form carries
320
+ more domain tokens than the raw one, the surplus came from a decoding and can never be the
321
+ sanctioned publication. Cost repo-wide: 0 new findings.
322
+ - **Admitting `.html` to `check-unicode-safety.mjs` immediately found a live legal defect.**
323
+ `site/datenschutz/index.html:277` carried a `U+00AD` at end of line. A soft hyphen does **not**
324
+ suppress the break — HTML collapses newline plus indentation into a space — so the page
325
+ rendered "Auftragsverarbeitungs vereinbarung", visibly broken, on a legally required page.
326
+ Deleting the `U+00AD` alone would not have fixed it; the space would have remained.
327
+ - **Six findings from the review panel against the site, the most important a false promise
328
+ (#1043/#1044/#1046).** The privacy policy described a network request that does not exist — a
329
+ reload of `leaderboard.json` including `credentials: 'omit'` — while a grep for
330
+ `fetch(|XMLHttpRequest|sendBeacon|WebSocket|EventSource|import(|new Image` across all four
331
+ pages returns 0 hits. The direction is harmless (over- rather than under-declared); the place
332
+ is not, because §10 expressly invites the reader to open the network tab and check. It was the
333
+ single statement on the page a reader could falsify in two minutes — on a page whose thesis is
334
+ checkability. Struck without replacement. The other five: a `TODO` placeholder shipped live in
335
+ public source (`index.html:665`), a `/guide` canonical that contradicted itself three ways,
336
+ missing og/twitter tags on all three subpages (a shared guide link rendered as a bare URL in
337
+ Slack, X and Discord — for a product distributed through shared links, the one classically
338
+ SEO-adjacent item with real return), `offers` struck from the JSON-LD, and an unquantified
339
+ absolute ("Every issue an outside user has ever opened" → "All four issues opened by people
340
+ outside it") two paragraphs above where the number already stood correctly.
341
+ - **A character allowlist in the census generator, and a `ReferenceError` on the path that
342
+ enforces it.** `rewrite()` checked `/[<>]/` against the **old** cell content, never the new
343
+ value. Eleven of the thirteen metrics are digits or hex by construction, but `version` is
344
+ whatever `package.json` says and `readPackageVersion` only checks "non-empty string" — a
345
+ prepared version literal could have closed the `<span>` and opened a tag. The precondition is
346
+ write access to `package.json`, which in this repo's trust model already means full access, so
347
+ this is defence in depth rather than an open hole — but it is the one place where `script-src
348
+ 'unsafe-inline'` would stop being theoretical. A rejection now refuses the **whole** file, not
349
+ just the value, and the allowlist hangs on the write rather than on HTML-span presence (before,
350
+ `"rules": "../../etc/passwd"` landed unchecked in the shipped file). Building it caught a defect
351
+ of its own: the error path called a `writeStderrLine` that never existed in that file — a
352
+ `ReferenceError` exactly where the guard bites. Error paths have to be executed, not read.
353
+ - **Accessibility: one real AA violation, and controls at 1.71:1.** `/guide` had no live region —
354
+ the copy button only changed its visible text, so a screen-reader user got **nothing** on
355
+ success and nothing on the error path, where "Press Ctrl+C" is the only way forward (WCAG
356
+ 4.1.3). Seven copy buttons shared one name and were indistinguishable in a screen reader's
357
+ element list, the one place they are read outside their context; names are now derived from the
358
+ nearest label in **document order** plus the first non-comment line, measured 7 of 7 unique
359
+ against 1 of 7 before. `button.copy` and `a.btn-2` sat at 1.71:1, under the 3:1 a control
360
+ boundary needs (WCAG 1.4.11); raising `--line-2` globally would have been a design change, since
361
+ the same token draws decorative hairlines where 3:1 is loud, so a separate `--line-ui` token
362
+ carries the purpose: measured 3.40:1 / 3.57:1 / 3.57:1 on the rendered element. Two comments
363
+ that claimed too much were corrected — the print comment said "only the two moving 1px lines are
364
+ dropped" while the rule below hides five selectors including the whole six-link navigation.
365
+ - **Page and receipt came from two different measurements.** `site/index.html` said
366
+ 252/135/`6fa214d`, `site/_census.json` said 253/140/`6f6bf58` — both tracked, both publicly
367
+ shipped, `--check` reporting 3 drifted. The earlier instance had the same shape and a
368
+ coordinator cause: a `--write` step run against a file still inside a live agent's file scope —
369
+ the PSA-002 scope collision this repo built `--assert-disjoint` against, one level above the
370
+ wave mechanics. The new coupling test compares the two **committed** artefacts against each
371
+ other rather than against the repo, so "both stale together" is green and only a divergence is
372
+ red; it asserts on drift **or** stale, because `counted-sha` is `provenance: true` and a
373
+ drift-only assertion would have been green against exactly the state it forbids.
374
+ - **The mirror push had a fourth state, and it was fail-open.** Today's own fix distinguished
375
+ three (no remote / push ok / push failed). Outside a git repository, `git remote get-url github`
376
+ fails with `fatal: not a git repository` — by exit code indistinguishable from "no such remote" —
377
+ so the block reported `no 'github' remote configured — skipping (not an error)` on stdout and
378
+ exited **0**: a broken environment reported as a healthy one, inside the fix that was closing
379
+ fail-open. Found by a reviewer explicitly briefed to **refute** the measurements; not by the
380
+ author, not by the test, not by the gate. Fixed with a `git rev-parse --git-dir` guard, all four
381
+ states executed individually in throwaway repos, `bash -n` **and** `sh -n` green (the bash-3.2
382
+ trap from this repo's own rule file).
383
+ - **CI had been red for six consecutive pipelines (#1081).**
384
+ `tests/scripts/site-numbers.test.mjs` calls `collect(REPO_ROOT)` and requires `missing === []`;
385
+ two of the 13 metrics read `.orchestrator/metrics/*.jsonl`, untracked per `.gitignore:40`.
386
+ Locally 23/23, on CI `23 tests | 23 skipped`, because a throwing top-level `beforeAll` takes the
387
+ whole file with it in Vitest — the blast radius was 23, not 9. Measured now: #7276, #7277,
388
+ #7280, #7281, #7286 and #7288 red, green again from #7334 (`glab ci list --per-page 20`).
389
+ - **The new pre-push gate produced three defects of its own before it worked**, each measured
390
+ rather than reasoned. (1) `git clone <path>` points the clone's origin at a **filesystem path**
391
+ while CI's checkout points at the real remote URL, so any code that *parses* that URL behaves
392
+ differently in the two trees — the opposite of the hook's purpose: `deriveRepo()` returned
393
+ `unknown-repo` where working tree and CI both return `session-orchestrator`, and
394
+ `namespace.test.mjs:199` asserts exactly against that. The real origin URL is now carried into
395
+ the clone. (2) A repo-pointing git environment beats both `-C <path>` and cwd, so
396
+ `git -C "$tree" checkout --detach` ran against the **original** repository. (3)
397
+ `GIT_CONFIG_PARAMETERS` propagates into every child once any `-c` was passed anywhere: pushing
398
+ with `git -c core.hooksPath=.husky push` carried that override into the materialised clone,
399
+ where every throwaway repo the gate's own test suite creates then fired the repository's
400
+ **real** pre-commit hooks — `test: fail, total: 0` three times in a row, green the moment the
401
+ `-c` was dropped. Both scrub sites are now namespace sweeps with a justified keep-set instead
402
+ of name lists, because the denylist has shipped with a hole **twice in a row**, each time at
403
+ the sibling form of the name just closed (`GIT_CONFIG_COUNT` without `GIT_CONFIG_PARAMETERS`,
404
+ then the exact mirror image). The source-derived census found **six of nine** unscrubbed
405
+ platform variables where the panel had reported three of four — including `PLUGIN_ROOT` (rung
406
+ **one** of `resolvePluginRoot`, above every `CLAUDE_`/`CODEX_`/`PI_` name the old list already
407
+ unset) and the Cursor plugin root, which is not `CURSOR_PLUGIN_ROOT` but `CURSOR_RULES_DIR`;
408
+ adding the guessed name would have read in review as a complete fix and closed nothing.
409
+ `GIT_EDITOR` and `GIT_EXEC_PATH` are kept by measurement, not hygiene: Claude Code sets
410
+ `GIT_EDITOR=true`, and a blind sweep drops git back to `vi` and hangs the worker.
411
+ - **Two silent instrument errors (#979, #1062).** `scanBacklog` read 50 of 89 open issues and
412
+ reported the window nowhere: `limit 50 => total 50, critical 0, high 10, stale 0` against
413
+ `limit 100 => total 89, critical 1, high 18, stale 5`. All three numbers wrong, and the mode
414
+ selector at Phase 7.5 computed with them — `staleCount 0` meant not one stale issue was visible
415
+ to it. The cause was the window alone, not the `priority::`/`priority:` split. Separately,
416
+ `memory_cleanup_at` hung on a prose instruction that failed once: `stampMemoryCleanup()` had
417
+ zero production callers, every reference was text asking the coordinator LLM to remember. On
418
+ 2026-08-14 a `/memory-cleanup` ran with documented yield, the step was skipped, all three
419
+ session records of that day carried `memory_cleanup_at: null`, and the banner reported "last
420
+ cleanup 29 days ago" against the 3 days of its own notes — with **no** mechanical signal
421
+ anywhere: zero `orchestrator.memory.cleanup*` events in 22,887. `/memory-cleanup` now emits the
422
+ event and `emit-session.mjs` derives the stamp from it.
423
+ - **The host registry lost living sessions (#1047).** `hooks/on-stop.mjs` deleted the registry
424
+ entry and refreshed the lock heartbeat 12 lines further down — same `if` block, at every turn
425
+ end — while `heartbeat()` had had zero production callers since its birth commit. The fix is
426
+ atomic (deletion out **and** heartbeat wired), because either half alone would have been worse
427
+ than the bug: entries would survive but never age, drop out of `detectPeers` after 15 minutes
428
+ and be swept as "stale" after 60. Deregistration moved to `hooks/on-session-end.mjs`. The
429
+ platform census in the comment was wrong at first and corrected by review — pi does deregister,
430
+ Cursor never registers and so cannot leak; Codex is the only affected platform.
431
+ - **`gh repo view -R` broke the GitHub CI banner for every external user of this plugin
432
+ (#1022).** `gh repo view` takes the repository as a **positional** argument and has no
433
+ `-R`/`--repo` flag at all, so the host-pinning added in #872 made `gh` exit 1 with
434
+ `unknown shorthand flag: 'R'` — an error `checkCiStatus`'s outer catch swallowed to
435
+ `null`, leaving the session-start Phase 4 banner silently dead on every GitHub repo. Fixed
436
+ positionally, and the swallow narrowed: a CLI that is *present* but fails now reports
437
+ `degraded` instead of `null`. The asymmetry across the three neighbouring call sites is
438
+ real and must not be unified — `glab repo view` **does** take `-R`, and `gh api`/`glab
439
+ api` take neither `-R` nor a positional, only `--hostname`.
440
+ - **`hooks/on-stop.mjs` printed a 10-frame stack trace at every turn end when `node_modules`
441
+ was absent** (interrupted install, EPERM sandbox, half-synced plugin cache): a static
442
+ `import { $ } from 'zx'` fails at module-load time, with no hint that `npm install` is the
443
+ fix. `zx` is now imported lazily and the failure degrades to one rate-limited stderr line
444
+ per 6h window, mirroring the missing-`node` degradation in `hooks/run-node.sh`.
445
+ (GH Kanevry/session-orchestrator#63.)
446
+ - **A board row inherited one legacy entry's terminal status to every same-named repo,
447
+ permanently (#871).** `_active-sessions.md` rows were keyed by `path.basename(repoRoot)`,
448
+ so two repos with the same directory name under different parents were one row — and both
449
+ are enumerable, since the dispatcher walks to depth 2. Rows are now keyed by a path-derived
450
+ hash, case-folded on APFS/NTFS so `…/Some-Repo` and `…/some-repo` do not split back into
451
+ the duplicate rows #719 had already fixed at the name layer. The key length carries a named
452
+ ceiling and a revisit trigger, not an intention to revisit.
453
+ - **Two vault write channels published unmasked secrets (#974/#1025)** —
454
+ `scripts/lib/vault-status/narrative-mirror.mjs` and `scripts/lib/vault-mirror/process.mjs`,
455
+ both writing tracked, pushed artefacts. The masker's needle set is a function of the
456
+ caller's env, which is not a defect but does mean two runs over the same records mask
457
+ differently: a consumer comparing a written artefact against a fresh candidate must treat
458
+ an already-redacted span as a **wildcard** (`matchesModuloRedaction`), or a later
459
+ partially-populated run rewrites the raw value it had already redacted. The tempting fix —
460
+ persist the needle set — is rejected: it puts a plaintext secrets file on disk to defend
461
+ against secrets on disk. Masking runs **after** `extractNarrative`, never before, because
462
+ masking the raw string first lets `[REDACTED]` land inside the structure and silently
463
+ delete a whole table from the mirrored file.
464
+ - **`agents/*.md` frontmatter is parsed, not pattern-matched.** `check-agents.mjs` gained the
465
+ same `js-yaml` `CORE_SCHEMA` rule, reported alone with no fall-through to the field rules —
466
+ a field rule evaluated on an unparseable block is a guess. Its existing block-scalar ban
467
+ stays: that rule forbids one particular *valid* YAML form, which is a different question
468
+ from whether the block is YAML at all.
469
+ - **Deleted `scripts/lib/mission-status-schema.mjs`** — 4 exports, zero production callers,
470
+ while three prose locations promised its application. Found by S3 above, on the day S3 was
471
+ written.
472
+
473
+ ### Notes
474
+
475
+ - **3.18.0 is missing from npm.** The tag `v3.18.0`, the GitHub release and the CHANGELOG entry
476
+ all exist; the registry has never seen the version — `npm view session-orchestrator versions`
477
+ returns `…, "3.16.0", "3.17.0", "3.19.0", "3.20.0"`. That gap is the incident #978 names and
478
+ the reason `scripts/release.mjs --publish` now creates the annotated tag only **after** a
479
+ registry-verified publish. Nothing in this line republishes it; the version number stays
480
+ skipped rather than reused.
481
+ - **This session damaged its own repository, and the first recovery missed half the damage.** A
482
+ coordinator diagnostic command exported `GIT_DIR` and ran the suite. In the real `.git`: HEAD
483
+ detached, three fixture commits, plus a foreign remote and the fixture identity in
484
+ `.git/config` — which then authored two commits. The first recovery checked HEAD, the index and
485
+ all 1614 files and **overlooked `.git/config` entirely**, because `git status` does not show it;
486
+ an agent measuring something else found it two hours later. History rewriting failed on both
487
+ remotes' branch protection (GitLab "not allowed to force push", GitHub GH006), so authorship
488
+ was corrected via `.mailmap` — which incidentally refutes part of this session's own #1079
489
+ finding: `enforce_admins: false` does **not** make the protection bypassable for force-push.
490
+ One correction to the record: the claim "git runs every hook with `GIT_DIR` set", written in
491
+ `334ea2d`, is **false** — measured on git 2.x / macOS, `pre-push`, `pre-commit`, `commit-msg`
492
+ and `post-commit` all run with `GIT_DIR` unset. The scrub remains right as defence in depth;
493
+ its stated reason was not.
494
+ - The sharpest instances of the frontmatter class are the ones where the check was green. A
495
+ hand-rolled `getDescription` regex in
496
+ `tests/lib/validate/skill-description-quality.test.mjs` terminated on the first folded line,
497
+ measuring **97 characters where the real YAML value is 329** (`session-start`; `autopilot`
498
+ 107 vs 555, `bootstrap` 98 vs 341) — which made its own `>= 250` assertion **vacuous** for
499
+ every skill already using the block-scalar form. It surfaced only because repairing the 12
500
+ unparseable blocks moved files across the threshold. `yaml.load` is now the single reader.
501
+ - **Found and deliberately not fixed here**, because both sit outside this line's scope and
502
+ both are the same shape as the bugs above: `agents/eval-judge.md` is **valid** YAML and
503
+ loses **96%** of its description — 1180 raw characters, 51 visible to a parser — because
504
+ ` #803` opens a YAML comment at column 51, so no parse rule can catch it. And
505
+ `skills/discovery/probes-arch.md:39` calls
506
+ `npx madge --circular --extensions ts,tsx,js,jsx src/` in an all-`.mjs` repo that has no
507
+ `src/` directory: it processes zero files and reports success.
508
+ - **Open, deliberately.** `site/llms-full.txt` still carries its numbers by hand — the generator
509
+ only touches HTML. They were corrected and cross-checked against the generator, but that is
510
+ exactly the defect this line fixes. Revisit trigger: as soon as a second text file carries
511
+ measured values. And the `scanBacklog` fix has no test of its own; it is covered only by the
512
+ 171 existing tests of the touched modules staying green, with the fake regression outstanding.
513
+ - One reported number did not survive review and is recorded here rather than quietly dropped:
514
+ "274 elements checked" for the site's contrast sweep is **not reproducible** — two independent
515
+ checkers measure 260 / 282 / 288 depending on an inclusion rule that was never shipped with the
516
+ number. The **value** 4.81:1 (worst text contrast) is stable across all three counting rules;
517
+ the count is not. A count without its rule is worthless — the same lesson #979 taught once
518
+ already in this line.
519
+ - Gate at `8984224`, re-run for this entry rather than quoted: **587 test files, 14,244 passed
520
+ / 0 failed / 11 skipped** (`npm test`, exit 0), typecheck **404 files OK** (exit 0),
521
+ owner-leakage **1548 files / 0 findings**, unicode-safety 0 findings. The release commit also
522
+ reports `validate-plugin` 172/0 and lint 0 at that sha; both are quoted from it, not
523
+ re-measured here.
524
+
10
525
  ## [3.20.0] - 2026-08-13
11
526
 
12
527
  Memory-pipeline line. The learning store had been accumulating for 233 sessions and delivering
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Session Orchestrator
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
- [![Version](https://img.shields.io/badge/version-3.20.0-blue.svg)](CHANGELOG.md)
4
+ [![Version](https://img.shields.io/badge/version-3.22.0-blue.svg)](CHANGELOG.md)
5
5
  [![npm](https://img.shields.io/npm/v/session-orchestrator.svg)](https://www.npmjs.com/package/session-orchestrator)
6
6
  [![Tests](https://img.shields.io/badge/tests-12%2C000%2B-brightgreen.svg)](docs/telemetry/telemetry-claims.md)
7
7
 
@@ -23,10 +23,14 @@ The same skills and commands run across all four, with platform-adapted hooks an
23
23
  For Claude Code, also install Node dependencies **once** (hooks import `zx`) and restart Claude Code:
24
24
 
25
25
  ```bash
26
- cd "$(claude plugin dir session-orchestrator 2>/dev/null || echo ~/.claude/plugins/session-orchestrator)"
27
- npm install
26
+ # Claude Code has no `plugin dir` subcommand, so resolve the install path from the cache.
27
+ SO_DIR="$(dirname "$(find ~/.claude/plugins/cache -path '*session-orchestrator*' -name package.json 2>/dev/null | head -1)")"
28
+ cd "$SO_DIR" && npm install
28
29
  ```
29
30
 
31
+ If `SO_DIR` comes back empty, the plugin is not installed from a marketplace — check
32
+ `/plugin list` inside Claude Code first.
33
+
30
34
  Setup guides: [Codex](docs/codex-setup.md) · [Cursor IDE](docs/cursor-setup.md) · [Pi](docs/pi-setup.md). Per-IDE notes on `CLAUDE.md` vs `AGENTS.md`: [instruction-file-resolution](skills/_shared/instruction-file-resolution.md).
31
35
 
32
36
  ## What makes it different
@@ -127,17 +131,18 @@ The system is markdown-driven config plus a thin Node runtime — skills, comman
127
131
  - **Cross-session learning is opt-in and inspectable.** Every session writes a record; after 5+ sessions `/evolve analyze` extracts confidence-scored patterns you can read and prune. Nothing is hidden.
128
132
  - **VCS dual support, no lock-in.** Auto-detects GitLab or GitHub from your remote and drives the full lifecycle for both.
129
133
 
130
- ## Recent highlights (v3.20.0)
134
+ ## Recent highlights (v3.22.0)
131
135
 
132
- Every release is additive and backward-compatible. Highlights of the v3.20.0 line:
136
+ Every release is additive and backward-compatible. Highlights of the v3.22.0 line:
133
137
 
134
- - **Learnings finally reach the agents doing the work (#1014)** — 233 sessions of accumulated memory had exactly zero read paths into a dispatched agent. Now a per-agent index, selected from that agent's declared file scope, riding the dispatch prompt the coordinator already writes. Measured at **+0.69%–1.15%** of the prompt an agent already receives, against **+72.3%** for the separate-injection path the delivery doc forbids factor 92. The character cap is derived from that measurement, not chosen, and delivery emits an event so "did it run?" is a grep rather than an inference.
135
- - **Agent-authored text no longer reaches every agent unfiltered (#1015)** — the reconcile renderer wrote it verbatim into `.claude/rules/`, which Claude Code hands to every agent in every session, with no revocation. Machine values now reject, prose is framed and capped. Two premises in the issue itself did not survive verification and were corrected rather than implemented.
136
- - **The learning store is durable again (#1017)** — `/evolve` pruned by rewriting with no archive append; **11 of 13 provenance pointers in generated rules resolved to nothing**. One shared archive-then-rewrite path, and **11 of 11 lost records recovered** from the vault mirror. Dangling pointers 11 0.
137
- - **Semantic dedup + contradiction detection (#1016)** — a bounded, deliberately non-transitive candidate pool and a fail-closed judgment layer in which rendering an approval prompt from an unreadable verdict counts, structurally, as a write.
138
- - **The review panel found the same class inside the fix** — three independent reviewers returned FIX_REQUIRED: this line had hardened one delivery channel and shipped a second, unhardened one beside it. Closed in one cycle, which surfaced three further holes of the same shape. Every new guard is proven by fake regression, not by a green test.
138
+ - **The warning that fired on 99.0% of all session starts (#1089)** — measured over **1,477 session-start events across 18 repos**, against 4,884 stop events with zero OOM markers. Three independent measurement errors, not one threshold among them: `os.freemem()` on Darwin reports only `Pages free` (median **0.4 GB** on hosts with 24–128 GB installed), a threshold named for *sessions* was compared against a *process* count (measured ratio **6.0:1** — same number, right denominator: 93.6% → **4.2%**), and one noisy axis could cap a wave alone. The verdict had reached the ledger for **15 of 1,734 sessions (0.9%)**, which is why the false alarm survived four months while six repos independently wrote it into their learnings store one at confidence **1.0**. A warning that fires on almost everything changes nothing except how fast you learn to ignore it.
139
+ - **The questions this tool asks its operator had never been measured (#1107)** — `scripts/auq-audit.mjs` now scores every choice block against eight criteria and two hard hurdles, all thresholds from one registry. The census found **three populations where every prior count had seen one**: 40 Claude Code blocks, 10 Codex prose lists, and 17 nobody had counted. Baseline **21 of 72 (29%)**, after the pass **72 of 72**. The dominant cause was structural rather than editorial — `header` caps at 12 codepoints and truncates silently, **26 of 42 exceeded it**, and one NFD-composed header measured 12 visible characters as 14 and destroyed its own question.
140
+ - **A probe that sized the wrong set and never asked git** — `checkStaleArtifacts` announced "37 files (11 MB)" while computing the megabytes over the entire directory instead of the 37 it names; they weigh **0.68 MB**, a factor of ~18. Separately it proposed pruning seven **tracked, runtime-read** source files, including the policy file a hook reads on every Bash call. The git exclusion is fail-**silent**, not fail-open: falling back to "nothing is tracked" *is* the defect it closes.
141
+ - **Only the raw session ID releases a lock (#1085)** — one session carried three identities (STATE.md, the lock, and issue-budget each held a different one) and `findPeers` reported the session's own presence as a foreign peer. The companion issue-budget fix removed a data-loss path where an identity-less invocation erased a live session's parked overflow.
142
+ - **The npm receipt is the boundary, and a `throw` lay behind it (#1088)** — `publish()` threw *after* a successful publish during a 12-second propagation poll: published, not tagged, not pushed, reported as "failed, retryable". Each post-receipt step now gets exactly one attempt and the rest is collected as reconciliation. Leak detection decides on an extracted path, not a regex over `npm notice` prose — `contest` is not `tests`.
143
+ - **Six waves ran with zero scope injection, and it looked exactly like a clean run (#1083)** — the scope declaration has two shapes; a coordinator that writes only the aggregate one degrades the chain to signal-free ALLOW. `scripts/materialize-wave-scope.mjs` is now the canonical writer of both.
139
144
 
140
- Previous line (v3.19.0): guard hardening and release mechanicssix wrapper bypasses closed (#982), blocked-commands floor overlay (#972), session-lock ownership proof, and release as one dispatch (#978).
145
+ Previous line (v3.21.0): guard mechanics, the public site, and cross-session messagingwhich turned out to be one strand: a fact maintained in two places, and a guard that is green without biting.
141
146
 
142
147
  Full version history: [CHANGELOG.md](CHANGELOG.md).
143
148
 
package/agents/analyst.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: analyst
3
- description: Use this agent for read-only PRD-quality review. Checks acceptance-criteria specificity, scope drift detection, and completeness of /plan output. <example>Context: /plan feature produced a PRD. user: "Review the PRD before /go." assistant: "I'll dispatch analyst to check acceptance-criteria specificity and scope drift before wave execution." <commentary>Analyst catches vague acceptance criteria before they cause carryover at session end.</commentary></example>
3
+ description: 'Use this agent for read-only PRD-quality review. Checks acceptance-criteria specificity, scope drift detection, and completeness of /plan output. <example>Context: /plan feature produced a PRD. user: "Review the PRD before /go." assistant: "I''ll dispatch analyst to check acceptance-criteria specificity and scope drift before wave execution." <commentary>Analyst catches vague acceptance criteria before they cause carryover at session end.</commentary></example>'
4
4
  model: inherit
5
5
  color: yellow
6
6
  tools: Read, Grep, Glob, Bash
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: architect-reviewer
3
- description: Use this agent for read-only architectural audits between waves. Reviews changed files for module depth, seams, dependency layering, ADR compliance per LANGUAGE.md vocabulary. <example>Context: After Impl-Core wave shipped 8 files. user: "Audit the W2 architecture before proceeding." assistant: "I'll dispatch architect-reviewer to check module depth, seams, and adapter quality before W3." <commentary>Architect-reviewer catches design smells (shallow modules, speculative seams) earlier than Quality-Lite, which only catches lint/typecheck.</commentary></example>
3
+ description: 'Use this agent for read-only architectural audits between waves. Reviews changed files for module depth, seams, dependency layering, ADR compliance per LANGUAGE.md vocabulary. <example>Context: After Impl-Core wave shipped 8 files. user: "Audit the W2 architecture before proceeding." assistant: "I''ll dispatch architect-reviewer to check module depth, seams, and adapter quality before W3." <commentary>Architect-reviewer catches design smells (shallow modules, speculative seams) earlier than Quality-Lite, which only catches lint/typecheck.</commentary></example>'
4
4
  model: inherit
5
5
  color: blue
6
6
  tools: Read, Grep, Glob, Bash